diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index d1ed40ac054..726532b8178 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:6.7.0" + implementation "io.micronaut.build.internal:micronaut-gradle-plugins:6.7.1" implementation "org.tomlj:tomlj:1.1.1" implementation "me.champeau.gradle:japicmp-gradle-plugin:0.4.2" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3ffd3dcf3e3..13159175e3d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,17 +36,17 @@ logbook-netty = "2.16.0" log4j = "2.22.1" micronaut-aws = "4.5.0" micronaut-groovy = "4.3.0" -micronaut-session = "4.2.0" +micronaut-session = "4.3.0" micronaut-sql = "5.3.0" micronaut-test = "4.1.1" -micronaut-validation = "4.4.4" +micronaut-validation = "4.5.0" micronaut-rxjava2 = "2.3.0" -micronaut-rxjava3 = "3.2.1" +micronaut-rxjava3 = "3.3.0" micronaut-reactor = "3.3.0" neo4j-java-driver = "5.17.0" selenium = "4.9.1" slf4j = "2.0.13" -smallrye = "6.2.6" +smallrye = "6.3.0" spock = "2.3-groovy-4.0" spotbugs = "4.7.1" systemlambda = "1.2.1" 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 af2e1c5ff96..ce124e06a83 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 @@ -63,6 +63,12 @@ public final class HttpVersionSelection { true ); + private static final HttpVersionSelection WEBSOCKET_1 = new HttpVersionSelection( + HttpVersionSelection.PlaintextMode.HTTP_1, + true, + new String[]{HttpVersionSelection.ALPN_HTTP_1}, + false); + private final PlaintextMode plaintextMode; private final boolean alpn; private final String[] alpnSupportedProtocols; @@ -100,6 +106,17 @@ public static HttpVersionSelection forLegacyVersion(@NonNull HttpVersion httpVer } } + /** + * Get the {@link HttpVersionSelection} to be used for a WebSocket connection, which will enable + * ALPN but constrain the mode to HTTP 1.1. + * + * @return The version selection for WebSocket + */ + @NonNull + public static HttpVersionSelection forWebsocket() { + return WEBSOCKET_1; + } + /** * Construct a version selection from the given client configuration. * 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 6457d8a43e6..bdeaacd690f 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 @@ -144,6 +144,7 @@ public class ConnectionManager { private final HttpClientConfiguration configuration; private volatile SslContext sslContext; private volatile /* QuicSslContext */ Object http3SslContext; + private volatile SslContext websocketSslContext; private final NettyClientCustomizer clientCustomizer; private final String informationalServiceId; @@ -165,6 +166,7 @@ public class ConnectionManager { this.configuration = from.configuration; this.sslContext = from.sslContext; this.http3SslContext = from.http3SslContext; + this.websocketSslContext = from.websocketSslContext; this.clientCustomizer = from.clientCustomizer; this.informationalServiceId = from.informationalServiceId; this.nettyClientSslBuilder = from.nettyClientSslBuilder; @@ -209,6 +211,8 @@ public class ConnectionManager { final void refresh() { SslContext oldSslContext = sslContext; + SslContext oldWebsocketSslContext = websocketSslContext; + websocketSslContext = null; if (configuration.getSslConfiguration().isEnabled()) { sslContext = nettyClientSslBuilder.build(configuration.getSslConfiguration(), httpVersion); } else { @@ -224,6 +228,7 @@ final void refresh() { pool.forEachConnection(c -> ((Pool.ConnectionHolder) c).windDownConnection()); } ReferenceCountUtil.release(oldSslContext); + ReferenceCountUtil.release(oldWebsocketSslContext); } /** @@ -369,7 +374,9 @@ public final void shutdown() { } } ReferenceCountUtil.release(sslContext); + ReferenceCountUtil.release(websocketSslContext); sslContext = null; + websocketSslContext = null; } /** @@ -432,6 +439,32 @@ public final Mono connect(DefaultHttpClient.RequestKey requestKey, @ return pools.computeIfAbsent(requestKey, Pool::new).acquire(blockHint); } + /** + * Builds an {@link SslContext} for the given WebSocket URI if necessary. + * + * @return The {@link SslContext} instance + */ + @Nullable + private SslContext buildWebsocketSslContext(DefaultHttpClient.RequestKey requestKey) { + SslContext sslCtx = websocketSslContext; + if (requestKey.isSecure()) { + if (configuration.getSslConfiguration().isEnabled()) { + if (sslCtx == null) { + synchronized (this) { + sslCtx = websocketSslContext; + if (sslCtx == null) { + sslCtx = nettyClientSslBuilder.build(configuration.getSslConfiguration(), HttpVersionSelection.forWebsocket()); + websocketSslContext = sslCtx; + } + } + } + } else if (configuration.getProxyAddress().isEmpty()){ + throw decorate(new HttpClientException("Cannot send WSS request. SSL is disabled")); + } + } + return sslCtx; + } + /** * Connect to a remote websocket. The given {@link ChannelHandler} is added to the pipeline * when the handshakes complete. @@ -448,7 +481,7 @@ final Mono connectForWebsocket(DefaultHttpClient.RequestKey requestKey, Chann protected void initChannel(@NonNull Channel ch) { addLogHandler(ch); - SslContext sslContext = buildSslContext(requestKey); + SslContext sslContext = buildWebsocketSslContext(requestKey); if (sslContext != null) { ch.pipeline().addLast(configureSslHandler(sslContext.newHandler(ch.alloc(), requestKey.getHost(), requestKey.getPort()))); } 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 index 71957bfc1cf..e564a426b2e 100644 --- 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 @@ -11,6 +11,7 @@ import io.micronaut.websocket.exceptions.WebSocketClientException import jakarta.inject.Inject import jakarta.inject.Singleton import reactor.core.publisher.Mono +import spock.lang.Issue import spock.lang.Specification import java.util.concurrent.ExecutionException @@ -38,6 +39,47 @@ class ClientWebsocketSpec extends Specification { client.close() } + @Issue("https://github.com/micronaut-projects/micronaut-core/issues/10744") + void 'websocket bean can connect to echo server over SSL with wss scheme'() { + 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, 'wss://websocket-echo.com')) + + when: + mono.toFuture().get() + + then: + registry.clientBeans.size() == 1 + registry.clientBeans[0].opened + !registry.clientBeans[0].autoClosed + !registry.clientBeans[0].onClosed + + cleanup: + client.close() + } + + void 'websocket bean can connect to echo server over SSL with https scheme'() { + given: + def ctx = ApplicationContext.run(['spec.name': 'ClientWebsocketSpec'])//, "micronaut.http.client.alpn-modes":"http/1.1"]) + def client = ctx.getBean(WebSocketClient) + def registry = ctx.getBean(ClientBeanRegistry) + def mono = Mono.from(client.connect(ClientBean.class, 'https://websocket-echo.com')) + + when: + mono.toFuture().get() + + then: + 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 { diff --git a/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/CookeFactorySpec.groovy b/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/CookeFactorySpec.groovy index a585783427f..bdb181b95ac 100644 --- a/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/CookeFactorySpec.groovy +++ b/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/CookeFactorySpec.groovy @@ -1,5 +1,6 @@ package io.micronaut.http.netty.cookies +import io.micronaut.http.cookie.Cookie import io.micronaut.http.cookie.CookieFactory import spock.lang.Specification @@ -9,4 +10,9 @@ class CookieFactorySpec extends Specification { expect: CookieFactory.INSTANCE instanceof NettyCookieFactory } + + void "default cookie is a netty cookie with max age undefined"() { + expect: + Cookie.of("SID", "31d4d96e407aad42").getMaxAge() == Cookie.UNDEFINED_MAX_AGE + } } diff --git a/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/NettyServerCookieEncoderSpec.groovy b/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/NettyServerCookieEncoderSpec.groovy index c9d2e43db22..806f9ffe9e4 100644 --- a/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/NettyServerCookieEncoderSpec.groovy +++ b/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/NettyServerCookieEncoderSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.http.netty.cookies import io.micronaut.http.cookie.Cookie +import io.micronaut.http.cookie.HttpCookieFactory import io.micronaut.http.cookie.SameSite import io.micronaut.http.cookie.ServerCookieEncoder import spock.lang.Specification @@ -46,6 +47,41 @@ class NettyServerCookieEncoderSpec extends Specification { expected == result || expected2 == result || expected3 == result } + void "netty server cookie encoder can correctly encode a cookie from HttpCookieFactory"() { + given: + HttpCookieFactory factory = new HttpCookieFactory(); + ServerCookieEncoder cookieEncoder = new NettyServerCookieEncoder() + + when: + Cookie cookie = factory.create("SID", "31d4d96e407aad42").path("/").domain("example.com") + + then: + "SID=31d4d96e407aad42; Path=/; Domain=example.com" == cookieEncoder.encode(cookie)[0] + + when: + cookie = factory.create("SID", "31d4d96e407aad42").path("/").domain("example.com").sameSite(SameSite.Strict) + + then: + "SID=31d4d96e407aad42; Path=/; Domain=example.com; SameSite=Strict" == cookieEncoder.encode(cookie)[0] + + when: + cookie = factory.create("SID", "31d4d96e407aad42").path("/").secure().httpOnly() + + then: 'Netty uses HTTPOnly instead of HttpOnly' + "SID=31d4d96e407aad42; Path=/; Secure; HTTPOnly" == cookieEncoder.encode(cookie)[0] + + when: + long maxAge = 2592000 + cookie = factory.create("id", "a3fWa").maxAge(maxAge) + String result = cookieEncoder.encode(cookie).get(0) + String expected = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge) + String expected2 = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge + 1) // To prevent flakiness + String expected3 = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge - 1) // To prevent flakiness + + then: + expected == result || expected2 == result || expected3 == result + } + void "ServerCookieEncoder is NettyServerCookieEncoder"() { expect: ServerCookieEncoder.INSTANCE instanceof NettyServerCookieEncoder diff --git a/http/src/main/java/io/micronaut/http/MediaType.java b/http/src/main/java/io/micronaut/http/MediaType.java index f5cb83617cd..c8eded817e6 100644 --- a/http/src/main/java/io/micronaut/http/MediaType.java +++ b/http/src/main/java/io/micronaut/http/MediaType.java @@ -82,6 +82,21 @@ public class MediaType implements CharSequence { */ public static final String EXTENSION_XLS = "xls"; + /** + * File extension for GPS Exchange Format files. + */ + public static final String EXTENSION_GPX = "gpx"; + + /** + * File extension for ZIP archive files. + */ + public static final String EXTENSION_ZIP = "zip"; + + /** + * File extension for GZIP compressed files. + */ + public static final String EXTENSION_GZIP = "gz"; + /** * Default empty media type array. */ @@ -424,6 +439,36 @@ public class MediaType implements CharSequence { */ public static final MediaType IMAGE_WEBP_TYPE = new MediaType(IMAGE_WEBP); + /** + * GPS Exchange Format: application/gpx+xml. + */ + public static final String APPLICATION_GPX_XML = "application/gpx+xml"; + + /** + * GPS Exchange Format: application/gpx+xml. + */ + public static final MediaType GPX_XML_TYPE = new MediaType(APPLICATION_GPX_XML, EXTENSION_GPX); + + /** + * ZIP archive format: application/zip. + */ + public static final String APPLICATION_ZIP = "application/zip"; + + /** + * ZIP archive format: application/zip. + */ + public static final MediaType ZIP_TYPE = new MediaType(APPLICATION_ZIP); + + /** + * GZip compressed data: application/gzip. + */ + public static final String APPLICATION_GZIP = "application/gzip"; + + /** + * GZip compressed data: application/gzip. + */ + public static final MediaType GZIP_TYPE = new MediaType(APPLICATION_GZIP); + /** * Parameter {@code "charset"}. */ @@ -645,6 +690,12 @@ public static MediaType of(String mediaType) { return IMAGE_GIF_TYPE; case IMAGE_WEBP: return IMAGE_WEBP_TYPE; + case APPLICATION_GPX_XML: + return GPX_XML_TYPE; + case APPLICATION_GZIP: + return GZIP_TYPE; + case APPLICATION_ZIP: + return ZIP_TYPE; default: return new MediaType(mediaType); } diff --git a/http/src/main/java/io/micronaut/http/cookie/Cookie.java b/http/src/main/java/io/micronaut/http/cookie/Cookie.java index 507db0a111e..6a44989fd1f 100644 --- a/http/src/main/java/io/micronaut/http/cookie/Cookie.java +++ b/http/src/main/java/io/micronaut/http/cookie/Cookie.java @@ -32,6 +32,11 @@ */ public interface Cookie extends Comparable, Serializable { + /** + * Constant for undefined MaxAge attribute value. + */ + long UNDEFINED_MAX_AGE = Long.MIN_VALUE; + /** * @see The Secure Attribute. */ @@ -66,7 +71,7 @@ public interface Cookie extends Comparable, Serializable { * @see The Max-Age Attribute */ String ATTRIBUTE_MAX_AGE = "Max-Age"; - + /** * @return The name of the cookie */ @@ -110,6 +115,10 @@ public interface Cookie extends Comparable, Serializable { boolean isSecure(); /** + * Gets the maximum age of the cookie in seconds. If the max age has not been explicitly set, + * then the value returned will be {@link #UNDEFINED_MAX_AGE}, indicating that the Max-Age + * Attribute should not be written. + * * @return The maximum age of the cookie in seconds */ long getMaxAge(); @@ -136,7 +145,8 @@ default Optional getSameSite() { } /** - * Sets the max age of the cookie in seconds. + * Sets the max age of the cookie in seconds. When not explicitly set, the max age will default + * to {@link #UNDEFINED_MAX_AGE} and cause the Max-Age Attribute not to be encoded. * * @param maxAge The max age * @return This cookie diff --git a/http/src/main/java/io/micronaut/http/cookie/CookieHttpCookieAdapter.java b/http/src/main/java/io/micronaut/http/cookie/CookieHttpCookieAdapter.java index 9bc966705ff..7c4d5705941 100644 --- a/http/src/main/java/io/micronaut/http/cookie/CookieHttpCookieAdapter.java +++ b/http/src/main/java/io/micronaut/http/cookie/CookieHttpCookieAdapter.java @@ -35,6 +35,9 @@ class CookieHttpCookieAdapter implements Cookie { public CookieHttpCookieAdapter(HttpCookie httpCookie) { this.httpCookie = httpCookie; + if (httpCookie.getMaxAge() == -1) { // HttpCookie.UNDEFINED_MAX_AGE = -1 + this.httpCookie.setMaxAge(Cookie.UNDEFINED_MAX_AGE); + } } @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 0acfd01cc61..64963172967 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java +++ b/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java @@ -17,7 +17,6 @@ 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.ObjectUtils; diff --git a/http/src/main/resources/META-INF/http/mime.types b/http/src/main/resources/META-INF/http/mime.types index d061b262b3c..861b240be0d 100644 --- a/http/src/main/resources/META-INF/http/mime.types +++ b/http/src/main/resources/META-INF/http/mime.types @@ -120,7 +120,7 @@ application/font-tdpfr pfr application/gml+xml gml application/gpx+xml gpx application/gxf gxf -# application/gzip +application/gzip gz # application/h224 # application/held+xml # application/http diff --git a/http/src/main/resources/META-INF/services/io.micronaut.http.cookie.CookieFactory b/http/src/main/resources/META-INF/services/io.micronaut.http.cookie.CookieFactory deleted file mode 100644 index 4d4bbceb02a..00000000000 --- a/http/src/main/resources/META-INF/services/io.micronaut.http.cookie.CookieFactory +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.http.cookie.HttpCookieFactory \ No newline at end of file diff --git a/http/src/test/groovy/io/micronaut/http/cookie/DefaultServerCookieEncoderSpec.groovy b/http/src/test/groovy/io/micronaut/http/cookie/DefaultServerCookieEncoderSpec.groovy new file mode 100644 index 00000000000..d5d4b125d2a --- /dev/null +++ b/http/src/test/groovy/io/micronaut/http/cookie/DefaultServerCookieEncoderSpec.groovy @@ -0,0 +1,54 @@ +package io.micronaut.http.cookie + +import spock.lang.Specification + +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +class DefaultServerCookieEncoderSpec extends Specification { + + void "DefaultServerCookieEncoder can correctly encode a cookie from HttpCookieFactory"() { + given: + HttpCookieFactory factory = new HttpCookieFactory(); + ServerCookieEncoder cookieEncoder = new DefaultServerCookieEncoder() + + when: + Cookie cookie = factory.create("SID", "31d4d96e407aad42").path("/").domain("example.com") + + then: + "SID=31d4d96e407aad42; Path=/; Domain=example.com" == cookieEncoder.encode(cookie)[0] + + when: + cookie = factory.create("SID", "31d4d96e407aad42").path("/").domain("example.com").sameSite(SameSite.Strict) + + then: + "SID=31d4d96e407aad42; Path=/; Domain=example.com; SameSite=Strict" == cookieEncoder.encode(cookie)[0] + + when: + cookie = factory.create("SID", "31d4d96e407aad42").path("/").secure().httpOnly() + + then: 'Netty uses HTTPOnly instead of HttpOnly' + "SID=31d4d96e407aad42; Path=/; Secure; HttpOnly" == cookieEncoder.encode(cookie)[0] + + when: + long maxAge = 2592000 + cookie = factory.create("id", "a3fWa").maxAge(maxAge) + String result = cookieEncoder.encode(cookie).get(0) + String expected = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge) + String expected2 = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge + 1) // To prevent flakiness + String expected3 = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge - 1) // To prevent flakiness + + then: + expected == result || expected2 == result || expected3 == result + } + + private static String expires(Long maxAgeSeconds) { + ZoneId gmtZone = ZoneId.of("GMT") + LocalDateTime localDateTime = LocalDateTime.now(gmtZone).plusSeconds(maxAgeSeconds) + ZonedDateTime gmtDateTime = ZonedDateTime.of(localDateTime, gmtZone) + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'") + gmtDateTime.format(formatter) + } +} diff --git a/http/src/test/java/io/micronaut/http/cookie/CookieHttpCookieAdapterTest.java b/http/src/test/java/io/micronaut/http/cookie/CookieHttpCookieAdapterTest.java index 1de52d10650..916de64fb6f 100644 --- a/http/src/test/java/io/micronaut/http/cookie/CookieHttpCookieAdapterTest.java +++ b/http/src/test/java/io/micronaut/http/cookie/CookieHttpCookieAdapterTest.java @@ -19,6 +19,7 @@ void testAdapter() { assertFalse(cookie.isHttpOnly()); assertFalse(cookie.isSecure()); assertTrue(cookie.getSameSite().isEmpty()); + assertEquals(Cookie.UNDEFINED_MAX_AGE, cookie.getMaxAge()); cookie = cookie.value("bar") .httpOnly() diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/InterceptorQualifierSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/introduction/InterceptorQualifierSpec.groovy new file mode 100644 index 00000000000..dce58c13c50 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/InterceptorQualifierSpec.groovy @@ -0,0 +1,63 @@ +/* + * 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.aop.introduction + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class InterceptorQualifierSpec extends Specification { + + @Shared + @AutoCleanup + ApplicationContext applicationContext = ApplicationContext.run(["spec.name": "InterceptorQualifierSpec"]) + + void "test intercepted qualifier"() { + when: + def fooHelper = applicationContext.getBean(MyDataSourceHelper, Qualifiers.byName("FOO")) + then: + fooHelper.name == "FOO" + fooHelper.injectionPointQualifier == "FOO" + fooHelper.helper2.name == null + fooHelper.helper2.injectionPointQualifier == "FOO" + fooHelper.helper3.name == "FOO" + fooHelper.helper3.injectionPointQualifier == "FOO" + when: + def barHelper = applicationContext.getBean(MyDataSourceHelper, Qualifiers.byName("BAR")) + then: + barHelper.name == "BAR" + barHelper.helper2.name == null + when: + def fooInterceptor = applicationContext.getBean(MyInterceptedInterface, Qualifiers.byName("FOO")) + then: + fooInterceptor.value == "FOO" + when: + def barInterceptor = applicationContext.getBean(MyInterceptedInterface, Qualifiers.byName("BAR")) + then: + barInterceptor.value == "BAR" + when: + def fooInterceptorWrapper = applicationContext.getBean(MyInterceptedInterfaceWrapper, Qualifiers.byName("FOO")) + then: + fooInterceptorWrapper.myInterceptedInterface.value == "FOO" + when: + def barInterceptorWrapper = applicationContext.getBean(MyInterceptedInterfaceWrapper, Qualifiers.byName("BAR")) + then: + barInterceptorWrapper.myInterceptedInterface.value == "BAR" + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSource.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSource.java new file mode 100644 index 00000000000..e49fa7c7026 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSource.java @@ -0,0 +1,7 @@ +package io.micronaut.aop.introduction; + +public interface MyDataSource { + + String getValue(); + +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper.java new file mode 100644 index 00000000000..1a08b97cb0b --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper.java @@ -0,0 +1,44 @@ +package io.micronaut.aop.introduction; + +import io.micronaut.context.Qualifier; +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.context.annotation.Requires; +import io.micronaut.inject.InjectionPoint; +import io.micronaut.inject.qualifiers.Qualifiers; + +@Requires(property = "spec.name", value = "InterceptorQualifierSpec") +@EachBean(MyDataSource.class) +public class MyDataSourceHelper { + + private final InjectionPoint injectionPoint; + private final Qualifier qualifier; + private final MyDataSourceHelper2 helper; + private final MyDataSourceHelper3 helper3; + + public MyDataSourceHelper(InjectionPoint injectionPoint, + Qualifier qualifier, + MyDataSourceHelper2 helper2, + @Parameter MyDataSourceHelper3 helper3) { + this.qualifier = qualifier; + this.helper = helper2; + this.helper3 = helper3; + this.injectionPoint = injectionPoint; + } + + public String getName() { + return Qualifiers.findName(qualifier); + } + + public String getInjectionPointQualifier() { + return Qualifiers.findName(injectionPoint.getDeclaringBeanQualifier()); + } + + public MyDataSourceHelper2 getHelper2() { + return helper; + } + + public MyDataSourceHelper3 getHelper3() { + return helper3; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper2.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper2.java new file mode 100644 index 00000000000..38676db1c7f --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper2.java @@ -0,0 +1,28 @@ +package io.micronaut.aop.introduction; + +import io.micronaut.context.Qualifier; +import io.micronaut.context.annotation.Requires; +import io.micronaut.inject.InjectionPoint; +import io.micronaut.inject.qualifiers.Qualifiers; +import jakarta.inject.Singleton; + +@Requires(property = "spec.name", value = "InterceptorQualifierSpec") +@Singleton +public class MyDataSourceHelper2 { + + private final InjectionPoint injectionPoint; + private final Qualifier qualifier; + + public MyDataSourceHelper2(InjectionPoint injectionPoint, Qualifier qualifier) { + this.injectionPoint = injectionPoint; + this.qualifier = qualifier; + } + + public String getName() { + return Qualifiers.findName(qualifier); + } + + public String getInjectionPointQualifier() { + return Qualifiers.findName(injectionPoint.getDeclaringBeanQualifier()); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper3.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper3.java new file mode 100644 index 00000000000..f1df3144adb --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper3.java @@ -0,0 +1,28 @@ +package io.micronaut.aop.introduction; + +import io.micronaut.context.Qualifier; +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Requires; +import io.micronaut.inject.InjectionPoint; +import io.micronaut.inject.qualifiers.Qualifiers; + +@Requires(property = "spec.name", value = "InterceptorQualifierSpec") +@EachBean(MyDataSource.class) +public class MyDataSourceHelper3 { + + private final InjectionPoint injectionPoint; + private final Qualifier qualifier; + + public MyDataSourceHelper3(InjectionPoint injectionPoint, Qualifier qualifier) { + this.injectionPoint = injectionPoint; + this.qualifier = qualifier; + } + + public String getName() { + return Qualifiers.findName(qualifier); + } + + public String getInjectionPointQualifier() { + return Qualifiers.findName(injectionPoint.getDeclaringBeanQualifier()); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedDataSourceFactory.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedDataSourceFactory.java new file mode 100644 index 00000000000..55f2a030e56 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedDataSourceFactory.java @@ -0,0 +1,34 @@ +package io.micronaut.aop.introduction; + +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Requires(property = "spec.name", value = "InterceptorQualifierSpec") +@Factory +public class MyInterceptedDataSourceFactory { + + @Singleton + @Named("FOO") + MyDataSource foo() { + return new MyDataSource() { + @Override + public String getValue() { + return "FOO"; + } + }; + } + + @Singleton + @Named("BAR") + MyDataSource bar() { + return new MyDataSource() { + @Override + public String getValue() { + return "BAR"; + } + }; + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedInterface.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedInterface.java new file mode 100644 index 00000000000..7b9d8cf3784 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedInterface.java @@ -0,0 +1,13 @@ +package io.micronaut.aop.introduction; + +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Requires; + +@Requires(property = "spec.name", value = "InterceptorQualifierSpec") +@EachBean(MyDataSource.class) +@MyInterceptedPoint +public interface MyInterceptedInterface { + + String getValue(); + +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedInterfaceWrapper.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedInterfaceWrapper.java new file mode 100644 index 00000000000..65ce8bcb612 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedInterfaceWrapper.java @@ -0,0 +1,20 @@ +package io.micronaut.aop.introduction; + +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.context.annotation.Requires; + +@Requires(property = "spec.name", value = "InterceptorQualifierSpec") +@EachBean(MyDataSource.class) +public class MyInterceptedInterfaceWrapper { + + private final MyInterceptedInterface myInterceptedInterface; + + public MyInterceptedInterfaceWrapper(@Parameter MyInterceptedInterface myInterceptedInterface) { + this.myInterceptedInterface = myInterceptedInterface; + } + + public MyInterceptedInterface getMyInterceptedInterface() { + return myInterceptedInterface; + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedIntroducer.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedIntroducer.java new file mode 100644 index 00000000000..3a356786d6c --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedIntroducer.java @@ -0,0 +1,28 @@ +package io.micronaut.aop.introduction; + +import io.micronaut.aop.InterceptorBean; +import io.micronaut.aop.MethodInterceptor; +import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.context.annotation.Prototype; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.InjectionPoint; +import io.micronaut.inject.qualifiers.Qualifiers; + +@Requires(property = "spec.name", value = "InterceptorQualifierSpec") +@Prototype +@InterceptorBean(MyInterceptedPoint.class) +public class MyInterceptedIntroducer implements MethodInterceptor { + + private final InjectionPoint injectionPoint; + + public MyInterceptedIntroducer(InjectionPoint injectionPoint) { + this.injectionPoint = injectionPoint; + } + + @Nullable + @Override + public Object intercept(MethodInvocationContext context) { + return Qualifiers.findName(injectionPoint.getDeclaringBeanQualifier()); + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedPoint.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedPoint.java new file mode 100644 index 00000000000..0040765de6f --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedPoint.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.aop.introduction; + +import io.micronaut.aop.Introduction; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Introduction +@Documented +@Retention(RUNTIME) +public @interface MyInterceptedPoint { +} 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 221178d9efd..fc149db5e07 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 @@ -66,11 +66,15 @@ internal class KotlinGenericPlaceholderElement( GenericPlaceholderElementAnnotationMetadata(this, upper) } private val resolvedAnnotationMetadata: AnnotationMetadata by lazy { - AnnotationMetadataHierarchy( - true, - upper.annotationMetadata, - resolvedGenericTypeAnnotationMetadata - ) + if (presetAnnotationMetadata != null) { + presetAnnotationMetadata + } else { + AnnotationMetadataHierarchy( + true, + upper.annotationMetadata, + resolvedGenericTypeAnnotationMetadata + ) + } } private val resolvedGenericTypeAnnotationMetadata: ElementAnnotationMetadata by lazy { elementAnnotationMetadataFactory.buildGenericTypeAnnotations(this) 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 index 4d1b8b02279..82434003a89 100644 --- 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 @@ -68,11 +68,15 @@ internal class KotlinTypeArgumentElement( } private val resolvedAnnotationMetadata: AnnotationMetadata by lazy { - AnnotationMetadataHierarchy( - true, - elementAnnotationMetadata, - resolvedGenericTypeAnnotationMetadata - ) + if (presetAnnotationMetadata != null) { + presetAnnotationMetadata + } else { + AnnotationMetadataHierarchy( + true, + elementAnnotationMetadata, + resolvedGenericTypeAnnotationMetadata + ) + } } private val resolvedGenericTypeAnnotationMetadata: ElementAnnotationMetadata by lazy { 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 ac814a5303f..06aeaca602b 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 @@ -50,11 +50,15 @@ internal class KotlinWildcardElement( } private val resolvedAnnotationMetadata: AnnotationMetadata by lazy { - AnnotationMetadataHierarchy( - true, - upper.annotationMetadata, - resolvedGenericTypeAnnotationMetadata - ) + if (presetAnnotationMetadata != null) { + presetAnnotationMetadata + } else { + AnnotationMetadataHierarchy( + true, + upper.annotationMetadata, + resolvedGenericTypeAnnotationMetadata + ) + } } private val resolvedGenericTypeAnnotationMetadata: ElementAnnotationMetadata by lazy { 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 92ed8d4a5ed..84f864eac3c 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 @@ -1,6 +1,7 @@ package io.micronaut.kotlin.processing.visitor import com.fasterxml.jackson.annotation.JsonClassDescription +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec import io.micronaut.context.annotation.Executable @@ -2382,4 +2383,46 @@ data class Cart( expect: bean } + + void "test ignored problem"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.ClassificationAndStats', ''' +package test + +import com.fasterxml.jackson.annotation.JsonGetter +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonValue +import io.micronaut.core.annotation.Introspected + +@Introspected +enum class Aggregation(@get:JsonValue val fieldName: String) { + FIELD_1("SomeField1"), + FIELD_2("SomeField2"); +} + +interface StatsEntry { + val shouldNotAppearInJson: MutableMap +} + +@Introspected +data class ClassificationVars( + val regionKode: String +) + +@Introspected +data class ClassificationAndStats( + val klassifisering: ClassificationVars, + /** Ignore field to avoid double wrapping of values in resulting JSON */ + @JsonIgnore + val stats: T +) { + @JsonGetter("stats") + fun getValues(): Map = stats.shouldNotAppearInJson +} + + ''') + def statsProp = introspection.getProperty("stats").get() + expect: + statsProp.hasAnnotation(JsonIgnore) + } } diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java index 8dfbfe9b810..1553a3d479d 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java @@ -368,11 +368,11 @@ public Path pushConstructorResolve(BeanDefinition declaringType, Argument argume @Override public Path pushConstructorResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments) { if (CONSTRUCTOR_METHOD_NAME.equals(methodName)) { - ConstructorSegment constructorSegment = new ConstructorArgumentSegment(declaringType, methodName, argument, arguments); + ConstructorSegment constructorSegment = new ConstructorArgumentSegment(declaringType, (Qualifier) getCurrentQualifier(), methodName, argument, arguments); detectCircularDependency(declaringType, argument, constructorSegment); } else { Segment previous = peek(); - MethodSegment methodSegment = new MethodArgumentSegment(declaringType, methodName, argument, arguments, previous instanceof MethodSegment ms ? ms : null); + MethodSegment methodSegment = new MethodArgumentSegment(declaringType, (Qualifier) getCurrentQualifier(), methodName, argument, arguments, previous instanceof MethodSegment ms ? ms : null); if (contains(methodSegment)) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, argument, CIRCULAR_ERROR_MSG); } else { @@ -390,7 +390,7 @@ 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, + MethodSegment methodSegment = new MethodArgumentSegment(declaringType, (Qualifier) getCurrentQualifier(), methodInjectionPoint.getName(), argument, methodInjectionPoint.getArguments(), previous instanceof MethodSegment ms ? ms : null); if (contains(methodSegment)) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, methodInjectionPoint, argument, CIRCULAR_ERROR_MSG); @@ -404,7 +404,7 @@ public Path pushMethodArgumentResolve(BeanDefinition declaringType, MethodInject @Override 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 ms ? ms : null); + MethodSegment methodSegment = new MethodArgumentSegment(declaringType, (Qualifier) getCurrentQualifier(), methodName, argument, arguments, previous instanceof MethodSegment ms ? ms : null); if (contains(methodSegment)) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, declaringType, methodName, argument, CIRCULAR_ERROR_MSG); } else { @@ -416,7 +416,7 @@ public Path pushMethodArgumentResolve(BeanDefinition declaringType, String metho @Override public Path pushFieldResolve(BeanDefinition declaringType, FieldInjectionPoint fieldInjectionPoint) { - FieldSegment fieldSegment = new FieldSegment<>(declaringType, fieldInjectionPoint.asArgument()); + FieldSegment fieldSegment = new FieldSegment<>(declaringType, getCurrentQualifier(), fieldInjectionPoint.asArgument()); if (contains(fieldSegment)) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, fieldInjectionPoint, CIRCULAR_ERROR_MSG); } else { @@ -427,7 +427,7 @@ public Path pushFieldResolve(BeanDefinition declaringType, FieldInjectionPoint f @Override public Path pushFieldResolve(BeanDefinition declaringType, Argument fieldAsArgument) { - FieldSegment fieldSegment = new FieldSegment<>(declaringType, fieldAsArgument); + FieldSegment fieldSegment = new FieldSegment<>(declaringType, getCurrentQualifier(), fieldAsArgument); if (contains(fieldSegment)) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, declaringType, fieldAsArgument.getName(), CIRCULAR_ERROR_MSG); } else { @@ -438,7 +438,7 @@ public Path pushFieldResolve(BeanDefinition declaringType, Argument fieldAsArgum @Override public Path pushAnnotationResolve(BeanDefinition beanDefinition, Argument annotationMemberBeanAsArgument) { - AnnotationSegment annotationSegment = new AnnotationSegment(beanDefinition, annotationMemberBeanAsArgument); + AnnotationSegment annotationSegment = new AnnotationSegment(beanDefinition, getCurrentQualifier(), annotationMemberBeanAsArgument); if (contains(annotationSegment)) { throw new CircularDependencyException(AbstractBeanResolutionContext.this, beanDefinition, annotationMemberBeanAsArgument.getName(), CIRCULAR_ERROR_MSG); } else { @@ -494,39 +494,45 @@ public void push(Segment segment) { /** * A segment that represents a method argument. */ - public static class ConstructorArgumentSegment extends ConstructorSegment implements ArgumentInjectionPoint { - public ConstructorArgumentSegment(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments) { - super(declaringType, methodName, argument, arguments); + public static final class ConstructorArgumentSegment extends ConstructorSegment implements ArgumentInjectionPoint { + public ConstructorArgumentSegment(BeanDefinition declaringType, Qualifier qualifier, String methodName, Argument argument, Argument[] arguments) { + super(declaringType, qualifier, methodName, argument, arguments); } @Override - public CallableInjectionPoint getOuterInjectionPoint() { + public CallableInjectionPoint getOuterInjectionPoint() { throw new UnsupportedOperationException("Outer injection point inaccessible from here"); } @Override - public BeanDefinition getDeclaringBean() { + public BeanDefinition getDeclaringBean() { return getDeclaringType(); } + @Override + public Qualifier getDeclaringBeanQualifier() { + return getDeclaringTypeQualifier(); + } + } /** * A segment that represents a constructor. */ - public static class ConstructorSegment extends AbstractSegment { + public static class ConstructorSegment extends AbstractSegment implements ArgumentInjectionPoint { private final String methodName; - private final Argument[] arguments; + private final Argument[] arguments; /** - * @param declaringClass The declaring class + * @param declaringBeanDefinition The declaring class + * @param qualifier The qualifier * @param methodName The methodName * @param argument The argument * @param arguments The arguments */ - ConstructorSegment(BeanDefinition declaringClass, String methodName, Argument argument, Argument[] arguments) { - super(declaringClass, declaringClass.getBeanType().getName(), argument); + ConstructorSegment(BeanDefinition declaringBeanDefinition, Qualifier qualifier, String methodName, Argument argument, Argument[] arguments) { + super(declaringBeanDefinition, qualifier, declaringBeanDefinition.getBeanType().getName(), argument); this.methodName = methodName; this.arguments = arguments; } @@ -546,31 +552,29 @@ public String toString() { } @Override - public InjectionPoint getInjectionPoint() { - ConstructorInjectionPoint constructorInjectionPoint = getDeclaringType().getConstructor(); - return new ArgumentInjectionPoint() { - @NonNull - @Override - public CallableInjectionPoint getOuterInjectionPoint() { - return constructorInjectionPoint; - } + public InjectionPoint getInjectionPoint() { + return this; + } - @NonNull - @Override - public Argument getArgument() { - return ConstructorSegment.this.getArgument(); - } + @NonNull + @Override + public CallableInjectionPoint getOuterInjectionPoint() { + return getDeclaringType().getConstructor(); + } - @Override - public BeanDefinition getDeclaringBean() { - return constructorInjectionPoint.getDeclaringBean(); - } + @Override + public BeanDefinition getDeclaringBean() { + return ConstructorSegment.this.getDeclaringType(); + } - @Override - public AnnotationMetadata getAnnotationMetadata() { - return getArgument().getAnnotationMetadata(); - } - }; + @Override + public Qualifier getDeclaringBeanQualifier() { + return ConstructorSegment.this.getDeclaringTypeQualifier(); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return getArgument().getAnnotationMetadata(); } } @@ -578,16 +582,21 @@ public AnnotationMetadata getAnnotationMetadata() { /** * A segment that represents a method argument. */ - public static class MethodArgumentSegment extends MethodSegment implements ArgumentInjectionPoint { - private final MethodSegment outer; - - public MethodArgumentSegment(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments, MethodSegment outer) { - super(declaringType, methodName, argument, arguments); + public static final class MethodArgumentSegment extends MethodSegment implements ArgumentInjectionPoint { + private final MethodSegment outer; + + public MethodArgumentSegment(BeanDefinition declaringType, + Qualifier qualifier, + String methodName, + Argument argument, + Argument [] arguments, + MethodSegment outer) { + super(declaringType, qualifier, methodName, argument, arguments); this.outer = outer; } @Override - public CallableInjectionPoint getOuterInjectionPoint() { + public CallableInjectionPoint getOuterInjectionPoint() { if (outer == null) { throw new IllegalStateException("Outer argument inaccessible"); } @@ -614,16 +623,17 @@ public String toString() { */ public static class MethodSegment extends AbstractSegment implements CallableInjectionPoint { - private final Argument[] arguments; + private final Argument[] arguments; /** * @param declaringType The declaring type + * @param qualifier The qualifier * @param methodName The method name * @param argument The argument * @param arguments The arguments */ - MethodSegment(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments) { - super(declaringType, methodName, argument); + MethodSegment(BeanDefinition declaringType, Qualifier qualifier, String methodName, Argument argument, Argument[] arguments) { + super(declaringType, qualifier, methodName, argument); this.arguments = arguments; } @@ -654,19 +664,25 @@ public Argument[] getArguments() { public AnnotationMetadata getAnnotationMetadata() { return getArgument().getAnnotationMetadata(); } + + @Override + public Qualifier getDeclaringBeanQualifier() { + return getDeclaringTypeQualifier(); + } } /** * A segment that represents a field. */ - public static class FieldSegment extends AbstractSegment implements InjectionPoint, ArgumentCoercible, ArgumentInjectionPoint { + public static final class FieldSegment extends AbstractSegment implements InjectionPoint, ArgumentCoercible, ArgumentInjectionPoint { /** * @param declaringClass The declaring class + * @param qualifier The qualifier * @param argument The argument */ - FieldSegment(BeanDefinition declaringClass, Argument argument) { - super(declaringClass, argument.getName(), argument); + FieldSegment(BeanDefinition declaringClass, Qualifier qualifier, Argument argument) { + super(declaringClass, qualifier, argument.getName(), argument); } @Override @@ -698,6 +714,11 @@ public Argument asArgument() { public AnnotationMetadata getAnnotationMetadata() { return getArgument().getAnnotationMetadata(); } + + @Override + public Qualifier getDeclaringBeanQualifier() { + return getDeclaringTypeQualifier(); + } } /** @@ -706,12 +727,14 @@ public AnnotationMetadata getAnnotationMetadata() { * @since 3.3.0 */ public static final class AnnotationSegment extends AbstractSegment implements InjectionPoint { + /** * @param beanDefinition The bean definition + * @param qualifier The qualifier * @param argument The argument */ - AnnotationSegment(BeanDefinition beanDefinition, Argument argument) { - super(beanDefinition, argument.getName(), argument); + AnnotationSegment(BeanDefinition beanDefinition, Qualifier qualifier, Argument argument) { + super(beanDefinition, qualifier, argument.getName(), argument); } @Override @@ -733,23 +756,32 @@ public BeanDefinition getDeclaringBean() { public AnnotationMetadata getAnnotationMetadata() { return getArgument().getAnnotationMetadata(); } + + @Override + public Qualifier getDeclaringBeanQualifier() { + return getDeclaringTypeQualifier(); + } } /** * Abstract class for a Segment. */ - abstract static class AbstractSegment implements Segment, Named { + protected abstract static class AbstractSegment implements Segment, Named { private final BeanDefinition declaringComponent; + @Nullable + private final Qualifier qualifier; private final String name; private final Argument argument; /** * @param declaringClass The declaring class + * @param qualifier The qualifier * @param name The name * @param argument The argument */ - AbstractSegment(BeanDefinition declaringClass, String name, Argument argument) { + AbstractSegment(BeanDefinition declaringClass, Qualifier qualifier, String name, Argument argument) { this.declaringComponent = declaringClass; + this.qualifier = qualifier; this.name = name; this.argument = argument; } @@ -764,6 +796,11 @@ public BeanDefinition getDeclaringType() { return declaringComponent; } + @Override + public Qualifier getDeclaringTypeQualifier() { + return qualifier == null ? declaringComponent.getDeclaredQualifier() : qualifier; + } + @Override public Argument getArgument() { return argument; diff --git a/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java index 2667a895414..2c48f336086 100644 --- a/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/BeanResolutionContext.java @@ -405,6 +405,12 @@ interface Segment { */ BeanDefinition getDeclaringType(); + /** + * @return The declaring type qualifier + * @since 4.5.0 + */ + Qualifier getDeclaringTypeQualifier(); + /** * @return The inject point */ diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index ab9a344d027..a06b73164bd 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -2887,21 +2887,22 @@ private BeanRegistration provideInjectionPoint(BeanResolutionContext reso boolean throwNoSuchBean) { final BeanResolutionContext.Path path = resolutionContext != null ? resolutionContext.getPath() : null; BeanResolutionContext.Segment injectionPointSegment = null; - if (CollectionUtils.isNotEmpty(path)) { - @SuppressWarnings("java:S2259") // false positive + if (path != null) { final Iterator> i = path.iterator(); - injectionPointSegment = i.next(); - BeanResolutionContext.Segment segment = null; if (i.hasNext()) { - segment = i.next(); - if (segment.getDeclaringType().hasStereotype(INTRODUCTION_TYPE)) { - segment = i.hasNext() ? i.next() : null; + injectionPointSegment = i.next(); + BeanResolutionContext.Segment segment = null; + if (i.hasNext()) { + segment = i.next(); + if (segment.getDeclaringType().hasStereotype(INTRODUCTION_TYPE)) { + segment = i.hasNext() ? i.next() : null; + } } - } - if (segment != null) { - T ip = (T) segment.getInjectionPoint(); - if (ip != null && beanType.isInstance(ip)) { - return new BeanRegistration<>(BeanIdentifier.of(InjectionPoint.class.getName()), null, ip); + if (segment != null) { + T ip = (T) segment.getInjectionPoint(); + if (ip != null && beanType.isInstance(ip)) { + return new BeanRegistration<>(BeanIdentifier.of(InjectionPoint.class.getName()), null, ip); + } } } } diff --git a/inject/src/main/java/io/micronaut/inject/InjectionPoint.java b/inject/src/main/java/io/micronaut/inject/InjectionPoint.java index 9f45e4a7ebf..ec6680c4327 100644 --- a/inject/src/main/java/io/micronaut/inject/InjectionPoint.java +++ b/inject/src/main/java/io/micronaut/inject/InjectionPoint.java @@ -15,9 +15,11 @@ */ package io.micronaut.inject; +import io.micronaut.context.Qualifier; import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; /** * An injection point as a point in a class definition where dependency injection is required. @@ -33,4 +35,13 @@ public interface InjectionPoint extends AnnotationMetadataProvider { */ @NonNull BeanDefinition getDeclaringBean(); + /** + * @return The qualifier of the bean that declares this injection point + * @since 4.5.0 + */ + @Nullable + default Qualifier getDeclaringBeanQualifier() { + return getDeclaringBean().getDeclaredQualifier(); + } + } 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 4ff625836dd..d4db264a30e 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 @@ -19,10 +19,13 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires import io.micronaut.core.convert.ArgumentConversionContext import io.micronaut.core.type.Argument +import io.micronaut.core.util.StringUtils import io.micronaut.health.HealthStatus import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import io.micronaut.http.bind.binders.TypedRequestArgumentBinder +import io.micronaut.http.client.BlockingHttpClient import io.micronaut.http.client.HttpClient import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.management.health.aggregator.DefaultHealthAggregator @@ -103,7 +106,7 @@ class HealthEndpointSpec extends Specification { void "test the beans are not available with all disabled"() { given: - ApplicationContext context = ApplicationContext.run(['endpoints.all.enabled': false]) + ApplicationContext context = ApplicationContext.run(['endpoints.all.enabled': StringUtils.FALSE]) expect: !context.containsBean(HealthEndpoint) @@ -117,8 +120,8 @@ class HealthEndpointSpec extends Specification { void "test the beans are available with all disabled and health enabled"() { given: - ApplicationContext context = ApplicationContext.run(['endpoints.all.enabled': false, 'endpoints.health.enabled': true]) - + ApplicationContext context = ApplicationContext.run(['endpoints.all.enabled': StringUtils.FALSE, + 'endpoints.health.enabled': StringUtils.TRUE]) context.start() expect: @@ -136,18 +139,18 @@ class HealthEndpointSpec extends Specification { EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ 'spec.name': getClass().simpleName, 'micronaut.application.name': 'foo', - 'endpoints.health.sensitive': false, + 'endpoints.health.sensitive': StringUtils.FALSE, 'datasources.one.url': 'jdbc:h2:mem:oneDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE', 'datasources.two.url': 'jdbc:h2:mem:twoDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE' ]) URL server = embeddedServer.getURL() - HttpClient rxClient = embeddedServer.applicationContext.createBean(HttpClient, server) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, server) + BlockingHttpClient client = httpClient.toBlocking() when: - def response = rxClient.exchange("/health", Map).blockFirst() + HttpResponse response = client.exchange("/health", Map) Map result = response.body() - then: response.code() == HttpStatus.OK.code result.status == "UP" @@ -173,18 +176,18 @@ class HealthEndpointSpec extends Specification { given: EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ 'spec.name': getClass().simpleName, - 'endpoints.health.sensitive': false, + 'endpoints.health.sensitive': StringUtils.FALSE, 'endpoints.health.disk-space.threshold': '9999GB']) URL server = embeddedServer.getURL() - HttpClient rxClient = embeddedServer.applicationContext.createBean(HttpClient, server) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, server) + BlockingHttpClient client = httpClient.toBlocking() when: - def response = rxClient.exchange("/health", HealthResult) - .onErrorResume(throwable -> { - def rsp = ((HttpClientResponseException) throwable).response - rsp.getBody(HealthResult) - return Flux.just(rsp) - }).blockFirst() + client.exchange("/health", HealthResult) + + then: + HttpClientResponseException ex = thrown(HttpClientResponseException) + HttpResponse response = ex.response HealthResult result = response.getBody(HealthResult).get() then: @@ -202,15 +205,15 @@ class HealthEndpointSpec extends Specification { given: EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ 'spec.name': getClass().simpleName, - 'endpoints.health.sensitive': false, + 'endpoints.health.sensitive': StringUtils.FALSE, 'endpoints.health.status.http-mapping.DOWN': 200, 'endpoints.health.disk-space.threshold': '9999GB']) URL server = embeddedServer.getURL() - HttpClient rxClient = embeddedServer.applicationContext.createBean(HttpClient, server) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, server) + BlockingHttpClient client = httpClient.toBlocking() when: - def response = rxClient.exchange("/health", HealthResult) - .blockFirst() + HttpResponse response = client.exchange("/health", HealthResult) HealthResult result = response.body() then: @@ -228,19 +231,21 @@ class HealthEndpointSpec extends Specification { given: EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ 'spec.name': getClass().simpleName, - 'endpoints.health.sensitive': false, + 'endpoints.health.sensitive': StringUtils.FALSE, 'datasources.one.url': 'jdbc:h2:mem:oneDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE', 'datasources.two.url': 'jdbc:mysql://localhost:59654/foo' ]) URL server = embeddedServer.getURL() - HttpClient rxClient = embeddedServer.applicationContext.createBean(HttpClient, server) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, server) + BlockingHttpClient client = httpClient.toBlocking() + when: + client.exchange("/health", Map) + + then: + HttpClientResponseException ex = thrown(HttpClientResponseException) when: - def response = rxClient.exchange("/health", Map).onErrorResume(throwable -> { - def rsp = ((HttpClientResponseException) throwable).response - rsp.getBody(Map) - return Flux.just(rsp) - }).blockFirst() + HttpResponse response = ex.response Map result = response.getBody(Map).get() then: @@ -254,21 +259,21 @@ class HealthEndpointSpec extends Specification { cleanup: embeddedServer?.close() - } void "test /health/liveness endpoint"() { given: EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ 'spec.name': getClass().simpleName, - 'endpoints.health.sensitive': false, + 'endpoints.health.sensitive': StringUtils.FALSE, ]) URL server = embeddedServer.getURL() - HttpClient rxClient = embeddedServer.applicationContext.createBean(HttpClient, server) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, server) + BlockingHttpClient client = httpClient.toBlocking() embeddedServer.applicationContext.createBean(TestLivenessHealthIndicator.class) when: - def response = rxClient.exchange("/health/liveness", Map).blockFirst() + HttpResponse response = client.exchange("/health/liveness", Map) Map result = response.body() then: @@ -286,14 +291,15 @@ class HealthEndpointSpec extends Specification { EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ 'micronaut.application.name': 'foo', 'spec.name': getClass().simpleName, - 'endpoints.health.sensitive': false, + 'endpoints.health.sensitive': StringUtils.FALSE, ]) URL server = embeddedServer.getURL() - HttpClient rxClient = embeddedServer.applicationContext.createBean(HttpClient, server) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, server) + BlockingHttpClient client = httpClient.toBlocking() embeddedServer.applicationContext.createBean(TestReadinessHealthIndicator.class) when: - def response = rxClient.exchange("/health/readiness", Map).blockFirst() + HttpResponse response = client.exchange("/health/readiness", Map) Map result = response.body() then: @@ -311,14 +317,15 @@ class HealthEndpointSpec extends Specification { given: EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ 'spec.name': getClass().simpleName, - 'endpoints.health.sensitive': false, + 'endpoints.health.sensitive': StringUtils.FALSE, ]) URL server = embeddedServer.getURL() - HttpClient rxClient = embeddedServer.applicationContext.createBean(HttpClient, server) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, server) + BlockingHttpClient client = httpClient.toBlocking() embeddedServer.applicationContext.createBean(TestReadinessHealthIndicator.class) when: - def response = rxClient.exchange("/health/readiness", Map).blockFirst() + HttpResponse response = client.exchange("/health/readiness", Map) Map result = response.body() then: @@ -337,18 +344,20 @@ class HealthEndpointSpec extends Specification { EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ 'spec.name': getClass().simpleName, 'indicator.name': 'TestLivenessDown', - 'endpoints.health.sensitive': false + 'endpoints.health.sensitive': StringUtils.FALSE ]) URL server = embeddedServer.getURL() - HttpClient rxClient = embeddedServer.applicationContext.createBean(HttpClient, server) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, server) + BlockingHttpClient client = httpClient.toBlocking() when: - def response = rxClient.exchange("/health/liveness", HealthResult) - .onErrorResume(throwable -> { - def rsp = ((HttpClientResponseException) throwable).response - rsp.getBody(HealthResult) - return Flux.just(rsp) - }).blockFirst() + client.exchange("/health/liveness", HealthResult) + + then: + HttpClientResponseException ex = thrown(HttpClientResponseException) + + when: + HttpResponse response = ex.response HealthResult result = response.getBody(HealthResult).get() then: @@ -364,18 +373,20 @@ class HealthEndpointSpec extends Specification { EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ 'spec.name': getClass().simpleName, 'indicator.name': 'TestReadinessDown', - 'endpoints.health.sensitive': false + 'endpoints.health.sensitive': StringUtils.FALSE ]) URL server = embeddedServer.getURL() - HttpClient rxClient = embeddedServer.applicationContext.createBean(HttpClient, server) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, server) + BlockingHttpClient client = httpClient.toBlocking() + + when: + client.exchange("/health/readiness", HealthResult) + + then: + HttpClientResponseException ex = thrown(HttpClientResponseException) when: - def response = rxClient.exchange("/health/readiness", HealthResult) - .onErrorResume(throwable -> { - def rsp = ((HttpClientResponseException) throwable).response - rsp.getBody(HealthResult) - return Flux.just(rsp) - }).blockFirst() + HttpResponse response = ex.response HealthResult result = response.getBody(HealthResult).get() then: @@ -391,15 +402,15 @@ class HealthEndpointSpec extends Specification { EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ 'spec.name': getClass().simpleName, 'indicator.name': 'TestReadinessDown', - 'endpoints.health.sensitive': false, + 'endpoints.health.sensitive': StringUtils.FALSE, 'endpoints.health.status.http-mapping.DOWN': 200 ]) URL server = embeddedServer.getURL() - HttpClient rxClient = embeddedServer.applicationContext.createBean(HttpClient, server) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, server) + BlockingHttpClient client = httpClient.toBlocking() when: - def response = rxClient.exchange("/health/readiness", HealthResult) - .blockFirst() + HttpResponse response = client.exchange("/health/readiness", HealthResult) HealthResult result = response.body() then: @@ -415,15 +426,15 @@ class HealthEndpointSpec extends Specification { EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [ 'spec.name': getClass().simpleName, 'indicator.name': 'TestLivenessDown', - 'endpoints.health.sensitive': false, + 'endpoints.health.sensitive': StringUtils.FALSE, 'endpoints.health.status.http-mapping.DOWN': 200 ]) URL server = embeddedServer.getURL() - HttpClient rxClient = embeddedServer.applicationContext.createBean(HttpClient, server) + HttpClient httpClient = embeddedServer.applicationContext.createBean(HttpClient, server) + BlockingHttpClient client = httpClient.toBlocking() when: - def response = rxClient.exchange("/health/liveness", HealthResult) - .blockFirst() + HttpResponse response = client.exchange("/health/liveness", HealthResult) HealthResult result = response.body() then: diff --git a/src/main/docs/guide/ioc/conditionalBeans.adoc b/src/main/docs/guide/ioc/conditionalBeans.adoc index da5efc92361..0ba1d3d5403 100644 --- a/src/main/docs/guide/ioc/conditionalBeans.adoc +++ b/src/main/docs/guide/ioc/conditionalBeans.adoc @@ -8,8 +8,6 @@ snippet::io.micronaut.docs.requires.JdbcBookService[tags="requires",indent=0, ti The above bean defines two requirements. The first indicates that a `DataSource` bean must be present for the bean to load. The second requirement ensures that the `datasource.url` property is set before loading the `JdbcBookService` bean. -NOTE: Kotlin currently does not support repeatable annotations. Use the `@Requirements` annotation when multiple requires are needed. For example, `@Requirements(Requires(...), Requires(...))`. See https://youtrack.jetbrains.com/issue/KT-12794 to track this feature. - If multiple beans require the same combination of requirements, you can define a meta-annotation with the requirements: snippet::io.micronaut.docs.requires.RequiresJdbc[tags="annotation",indent=0, title="Using a @Requires meta-annotation"]