From 7223658c359dbb79453e9aa336e42ca982336c00 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 22 Apr 2024 18:04:51 +0200 Subject: [PATCH 01/20] KSP: Correct replacing annotations (#10750) --- .../KotlinGenericPlaceholderElement.kt | 14 +++--- .../visitor/KotlinTypeArgumentElement.kt | 14 +++--- .../visitor/KotlinWildcardElement.kt | 14 +++--- .../visitor/BeanIntrospectionSpec.groovy | 43 +++++++++++++++++++ 4 files changed, 70 insertions(+), 15 deletions(-) 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) + } } From 208df2d2c5d72f0a68aa2d234bb3551782ad011c Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 22 Apr 2024 17:57:39 +0000 Subject: [PATCH 02/20] [skip ci] Release v4.4.6 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c1660da3a1d..e161014840b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.4.6-SNAPSHOT +projectVersion=4.4.6 projectGroupId=io.micronaut projectDesc=Core components supporting the Micronaut Framework title=Micronaut Core From 6a78f0c8365a7a10825a3f530de5669d6f62cc69 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 22 Apr 2024 18:16:23 +0000 Subject: [PATCH 03/20] chore: Bump version to 4.4.7-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e161014840b..290accfc1d3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.4.6 +projectVersion=4.4.7-SNAPSHOT projectGroupId=io.micronaut projectDesc=Core components supporting the Micronaut Framework title=Micronaut Core From f7dbe03442da5905cf4c2d93717e9ae4d3e043dd Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 23 Apr 2024 15:03:02 +0200 Subject: [PATCH 04/20] Support accessing the injection point qualifier of each-beans (#10753) --- .../InterceptorQualifierSpec.groovy | 63 ++++++++ .../aop/introduction/MyDataSource.java | 7 + .../aop/introduction/MyDataSourceHelper.java | 44 ++++++ .../aop/introduction/MyDataSourceHelper2.java | 28 ++++ .../aop/introduction/MyDataSourceHelper3.java | 28 ++++ .../MyInterceptedDataSourceFactory.java | 34 ++++ .../introduction/MyInterceptedInterface.java | 13 ++ .../MyInterceptedInterfaceWrapper.java | 20 +++ .../introduction/MyInterceptedIntroducer.java | 28 ++++ .../aop/introduction/MyInterceptedPoint.java | 29 ++++ .../AbstractBeanResolutionContext.java | 147 +++++++++++------- .../context/BeanResolutionContext.java | 6 + .../micronaut/context/DefaultBeanContext.java | 25 +-- .../io/micronaut/inject/InjectionPoint.java | 11 ++ 14 files changed, 416 insertions(+), 67 deletions(-) create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/introduction/InterceptorQualifierSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSource.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper2.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/introduction/MyDataSourceHelper3.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedDataSourceFactory.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedInterface.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedInterfaceWrapper.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedIntroducer.java create mode 100644 inject-java/src/test/groovy/io/micronaut/aop/introduction/MyInterceptedPoint.java 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/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(); + } + } From 336907a447d95bc745898ff6493494ccf1214d6b Mon Sep 17 00:00:00 2001 From: Peter Fokkinga Date: Wed, 24 Apr 2024 15:50:32 +0200 Subject: [PATCH 05/20] feat: adding zip gzip and gpx to MediaType (#10747) Close: #10673 --- .../java/io/micronaut/http/MediaType.java | 51 +++++++++++++++++++ .../micronaut/http/uri/UriMatchTemplate.java | 1 - .../main/resources/META-INF/http/mime.types | 2 +- 3 files changed, 52 insertions(+), 2 deletions(-) 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/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 From dc3e41fcddb4bb311f00c7637cfb7c8ed98012e6 Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Thu, 25 Apr 2024 03:16:07 -0400 Subject: [PATCH 06/20] Force secure WebSocket connections to use http/1.1 (#10754) * Force secure WebSocket connections to use http/1.1 Connection manager is updated to use a separate SSLContext for WebSocket connections that will only advertise http/1.1 in the list of supported protocols in the ALPN section of the TLS handshake. WebSocket is not currently supported over HTTP 2 connections, thus if an HTTP 2 connection is established through ALPN, the subsequent upgrade to the WebSocket protocol would fail. This resolves #10744 * Enable ALPN with Websocket and ensure SSLContext released. * Lazily create websocket SSL context per connection * Use lazily intialized field for WebSocket SSL context. * Actually initialize in the initializer. --- .../http/client/HttpVersionSelection.java | 17 ++++++++ .../http/client/netty/ConnectionManager.java | 35 +++++++++++++++- .../websocket/ClientWebsocketSpec.groovy | 42 +++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) 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 { From 533a099abb19c41adf3518ad781cc041aca00f7d Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 25 Apr 2024 15:06:38 +0200 Subject: [PATCH 07/20] doc: Kotlin supports repeatable annotations since 1.6 (#10755) --- src/main/docs/guide/ioc/conditionalBeans.adoc | 2 -- 1 file changed, 2 deletions(-) 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"] From ac4176d62324d7899b27cfd74864dbadab8682a2 Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Mon, 29 Apr 2024 06:01:19 -0400 Subject: [PATCH 08/20] fix: Define default value for Cookie Max-Age (#10775) The Cookie interface is updated with a constant value to represent an undefined Max-Age, and its JavaDocs are updated to explicitly state that the undefined value should not be encoded. The CookieHttpCookieAdapter implementation is updated to explicitly set the required undefined max age value in its constructor. The new constant is consistent with the behavior of the Netty Cookie implementation, and is meant to enforce consistency between it and the HttpCookie based implementation, if only by explicitly stating the intended contract. The CookieFactory service definition is removed from the http module as HttpCookieFactory already gets loaded by default (if no other service definitions are loaded) via CookieFactory.INSTANCE. This allows other explicit services definitions such as that in http-netty to reliably override the default implementation. Tests are added to verify the undefined max age value in both implementations, and to ensure that NettyServerCookieEncoder can correctly encode a Cookie created by the default HttpCookieFactory. * test: DefaultServerCookieEncoder encoding test * Only set if -1 --- .../netty/cookies/CookeFactorySpec.groovy | 6 +++ .../NettyServerCookieEncoderSpec.groovy | 36 +++++++++++++ .../java/io/micronaut/http/cookie/Cookie.java | 14 ++++- .../http/cookie/CookieHttpCookieAdapter.java | 3 ++ .../io.micronaut.http.cookie.CookieFactory | 1 - .../DefaultServerCookieEncoderSpec.groovy | 54 +++++++++++++++++++ .../cookie/CookieHttpCookieAdapterTest.java | 1 + 7 files changed, 112 insertions(+), 3 deletions(-) delete mode 100644 http/src/main/resources/META-INF/services/io.micronaut.http.cookie.CookieFactory create mode 100644 http/src/test/groovy/io/micronaut/http/cookie/DefaultServerCookieEncoderSpec.groovy 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/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/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() From 3c50750af94fee6f1d6574f67d1de9e13e3de2ba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:01:35 +0200 Subject: [PATCH 09/20] fix(deps): update dependency io.micronaut.session:micronaut-session to v4.3.0 (#10774) 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 3ffd3dcf3e3..c10f14d1d7c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ 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" From 1d3fe30bb040a214fa091cbed2e5e7a1456d9e3d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:01:50 +0200 Subject: [PATCH 10/20] chore(deps): update dependency io.micronaut.build.internal:micronaut-gradle-plugins to v6.7.1 (#10764) 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 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" From ef64d6408426ef0b0b7df379ea642470a0fbe8c5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:02:31 +0200 Subject: [PATCH 11/20] fix(deps): update dependency io.micronaut.rxjava3:micronaut-rxjava3-bom to v3.3.0 (#10751) 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 c10f14d1d7c..514be66a3d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,7 +41,7 @@ micronaut-sql = "5.3.0" micronaut-test = "4.1.1" micronaut-validation = "4.4.4" 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" From 585c99b68f08906b3057f13009bd87a4436e8ba0 Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Mon, 29 Apr 2024 06:01:19 -0400 Subject: [PATCH 12/20] fix: Define default value for Cookie Max-Age (#10775) The Cookie interface is updated with a constant value to represent an undefined Max-Age, and its JavaDocs are updated to explicitly state that the undefined value should not be encoded. The CookieHttpCookieAdapter implementation is updated to explicitly set the required undefined max age value in its constructor. The new constant is consistent with the behavior of the Netty Cookie implementation, and is meant to enforce consistency between it and the HttpCookie based implementation, if only by explicitly stating the intended contract. The CookieFactory service definition is removed from the http module as HttpCookieFactory already gets loaded by default (if no other service definitions are loaded) via CookieFactory.INSTANCE. This allows other explicit services definitions such as that in http-netty to reliably override the default implementation. Tests are added to verify the undefined max age value in both implementations, and to ensure that NettyServerCookieEncoder can correctly encode a Cookie created by the default HttpCookieFactory. * test: DefaultServerCookieEncoder encoding test * Only set if -1 --- .../netty/cookies/CookeFactorySpec.groovy | 6 +++ .../NettyServerCookieEncoderSpec.groovy | 36 +++++++++++++ .../java/io/micronaut/http/cookie/Cookie.java | 14 ++++- .../http/cookie/CookieHttpCookieAdapter.java | 3 ++ .../io.micronaut.http.cookie.CookieFactory | 1 - .../DefaultServerCookieEncoderSpec.groovy | 54 +++++++++++++++++++ .../cookie/CookieHttpCookieAdapterTest.java | 1 + 7 files changed, 112 insertions(+), 3 deletions(-) delete mode 100644 http/src/main/resources/META-INF/services/io.micronaut.http.cookie.CookieFactory create mode 100644 http/src/test/groovy/io/micronaut/http/cookie/DefaultServerCookieEncoderSpec.groovy 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/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/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() From e20e3bfc17caacb44294d0aed67d606d582074f3 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 25 Apr 2024 15:06:38 +0200 Subject: [PATCH 13/20] doc: Kotlin supports repeatable annotations since 1.6 (#10755) --- src/main/docs/guide/ioc/conditionalBeans.adoc | 2 -- 1 file changed, 2 deletions(-) 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"] From 19a5c0e8cf9e263821b26cd7dfc0449c9d7cd287 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 29 Apr 2024 11:19:44 +0000 Subject: [PATCH 14/20] [skip ci] Release v4.3.17 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0c011bfd41b..7d1b363938d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.3.17-SNAPSHOT +projectVersion=4.3.17 projectGroupId=io.micronaut projectDesc=Core components supporting the Micronaut Framework title=Micronaut Core From 8fe5365eb22bcea6ed5a75e2094e3f27b56030eb Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 29 Apr 2024 11:34:42 +0000 Subject: [PATCH 15/20] chore: Bump version to 4.3.18-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7d1b363938d..85116a0d018 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.3.17 +projectVersion=4.3.18-SNAPSHOT projectGroupId=io.micronaut projectDesc=Core components supporting the Micronaut Framework title=Micronaut Core From c29a7cedfcc90975067321feea50ac3b03a2c0be Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 29 Apr 2024 13:27:55 +0000 Subject: [PATCH 16/20] [skip ci] Release v4.4.7 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 290accfc1d3..2564da212a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.4.7-SNAPSHOT +projectVersion=4.4.7 projectGroupId=io.micronaut projectDesc=Core components supporting the Micronaut Framework title=Micronaut Core From 4d23cd0db2a80e5be8bd40c043d83569da7f3e20 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 29 Apr 2024 13:41:24 +0000 Subject: [PATCH 17/20] chore: Bump version to 4.4.8-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2564da212a9..dd74e3cfd8d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.4.7 +projectVersion=4.4.8-SNAPSHOT projectGroupId=io.micronaut projectDesc=Core components supporting the Micronaut Framework title=Micronaut Core From aaec6ebd6487b2bd9fc866c14f59cea2cdf52b00 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 30 Apr 2024 11:35:53 +0200 Subject: [PATCH 18/20] refactor: use Blocking HTTP Client in HealthEndpointSpec (#10780) - Use StringUtils.FALSE or StringUtils.FALSE - Uses BlockingHttpClient intead of Reactive HTTP Client - Remove usage of def --- .../endpoint/health/HealthEndpointSpec.groovy | 131 ++++++++++-------- 1 file changed, 71 insertions(+), 60 deletions(-) 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: From 2c699f71cd6a6a9b72ea1335af029e28b3172d47 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:36:34 +0200 Subject: [PATCH 19/20] fix(deps): update dependency io.micronaut.validation:micronaut-validation-bom to v4.5.0 (#10778) 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 514be66a3d4..e79e7c0176c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,7 +39,7 @@ micronaut-groovy = "4.3.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.3.0" micronaut-reactor = "3.3.0" From 243ef5a707cf07e566aa7900aae897eeac556aeb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:36:56 +0200 Subject: [PATCH 20/20] fix(deps): update dependency io.smallrye:smallrye-fault-tolerance to v6.3.0 (#10779) 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 e79e7c0176c..13159175e3d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ 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"