From 49d1785b6d7677938cfbedcaf92cfb2c29b79ecc Mon Sep 17 00:00:00 2001 From: Mitchell Herrijgers Date: Tue, 22 Oct 2024 15:53:13 +0100 Subject: [PATCH] Support decorators with null fields of type A customer experienced the following error: ``` Failed to connect to AxonIQ Console. Error: null cannot be cast to non-null type T of io.axoniq.console.framework.UtilsKt.unwrapPossiblyDecoratedClass$lambda-0. Will keep trying to connect ``` This is because a field of an expected decorated type is null. The code in this PR adjusts the method to find the first suitable non-null field. If none is found, it will return the decorator itself. --- .../java/io/axoniq/console/framework/utils.kt | 17 +++-- .../axoniq/console/framework/UtilsKtTest.kt | 73 +++++++++++++++++++ 2 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 console-framework-client/src/test/java/io/axoniq/console/framework/UtilsKtTest.kt diff --git a/console-framework-client/src/main/java/io/axoniq/console/framework/utils.kt b/console-framework-client/src/main/java/io/axoniq/console/framework/utils.kt index 86b08a0..5125578 100644 --- a/console-framework-client/src/main/java/io/axoniq/console/framework/utils.kt +++ b/console-framework-client/src/main/java/io/axoniq/console/framework/utils.kt @@ -26,18 +26,19 @@ import java.time.Duration * Useful for when users wrap components as decorators, like Axon FireStarter does. */ fun T.unwrapPossiblyDecoratedClass(clazz: Class): T { - return fieldOfMatchingType(clazz) - ?.let { ReflectionUtils.getFieldValue(it, this) as T } - ?.unwrapPossiblyDecoratedClass(clazz) - // No field of provided type - reached end of decorator chain - ?: this + return fieldsOfMatchingType(clazz) + .mapNotNull { ReflectionUtils.getFieldValue(it, this) as T? } + .map { it.unwrapPossiblyDecoratedClass(clazz) } + .firstOrNull() + // No field of provided type - reached end of decorator chain + ?: this } -private fun T.fieldOfMatchingType(clazz: Class): Field? { +private fun T.fieldsOfMatchingType(clazz: Class): List { // When we reach our own AS-classes, stop unwrapping - if (this::class.java.name.startsWith("org.axonframework") && this::class.java.simpleName.startsWith("AxonServer")) return null + if (this::class.java.name.startsWith("org.axonframework") && this::class.java.simpleName.startsWith("AxonServer")) return listOf() return ReflectionUtils.fieldsOf(this::class.java) - .firstOrNull { f -> clazz.isAssignableFrom(f.type) } + .filter { f -> clazz.isAssignableFrom(f.type) } } fun MutableMap.computeIfAbsentWithRetry(key: K, retries: Int = 0, defaultValue: (K) -> V): V { diff --git a/console-framework-client/src/test/java/io/axoniq/console/framework/UtilsKtTest.kt b/console-framework-client/src/test/java/io/axoniq/console/framework/UtilsKtTest.kt new file mode 100644 index 0000000..d523157 --- /dev/null +++ b/console-framework-client/src/test/java/io/axoniq/console/framework/UtilsKtTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024. AxonIQ B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.axoniq.console.framework + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class UtilsKtTest { + @Test + fun `Unwraps decorator with single field`() { + val core = MyCoreDecoratableService() + val decorated = MySingleValueDecoratableService(core) + val unwrapped = decorated.unwrapPossiblyDecoratedClass(MyDecoratableService::class.java) + assertEquals(core, unwrapped) + } + + @Test + fun `Unwraps decorator that has a nullable field out of two`() { + val core = MyCoreDecoratableService() + val decorated = MyDoubleNullableValueDecoratableService(core, null) + val unwrapped = decorated.unwrapPossiblyDecoratedClass(MyDecoratableService::class.java) + val decorated2 = MyDoubleNullableValueDecoratableService(null, core) + val unwrapped2 = decorated2.unwrapPossiblyDecoratedClass(MyDecoratableService::class.java) + assertEquals(core, unwrapped) + assertEquals(core, unwrapped2) + } + + @Test + fun `Does not crash on null fields of single-field decorator and returns decorator itself`() { + val decorated = MySingleValueDecoratableService(null) + val unwrapped = decorated.unwrapPossiblyDecoratedClass(MyDecoratableService::class.java) + assertEquals(decorated, unwrapped) + } + + @Test + fun `Does not crash on both null fields of double-field decorator and returns decorator itself`() { + val decorated = MyDoubleNullableValueDecoratableService(null, null) + val unwrapped = decorated.unwrapPossiblyDecoratedClass(MyDecoratableService::class.java) + assertEquals(decorated, unwrapped) + } + + interface MyDecoratableService { + + } + + class MyCoreDecoratableService : MyDecoratableService { + + } + + class MySingleValueDecoratableService( + val delegate: MyDecoratableService? + ) : MyDecoratableService + + + class MyDoubleNullableValueDecoratableService( + val delegate: MyDecoratableService?, + val delegate2: MyDecoratableService?, + ) : MyDecoratableService +} \ No newline at end of file