From 67c362a23e50ba1c2caf16cb1dff5cee000dbc34 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 30 Mar 2024 23:19:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20`WrapperAdapterUtils`=20?= =?UTF-8?q?=F0=9F=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inspectablewrappers/WrapperAdapter.java | 5 + .../utils/WrapperAdapterUtils.java | 119 ++++++++++++++++++ .../inspectablewrappers/WrapperAdapterTest.kt | 50 ++++++++ 3 files changed, 174 insertions(+) create mode 100644 src/main/java/io/foldright/inspectablewrappers/utils/WrapperAdapterUtils.java diff --git a/src/main/java/io/foldright/inspectablewrappers/WrapperAdapter.java b/src/main/java/io/foldright/inspectablewrappers/WrapperAdapter.java index 674efd6..1b9e6f0 100644 --- a/src/main/java/io/foldright/inspectablewrappers/WrapperAdapter.java +++ b/src/main/java/io/foldright/inspectablewrappers/WrapperAdapter.java @@ -14,11 +14,16 @@ *

* Wrapper Chain contains WrapperAdapter + *

+ * Provided {@link io.foldright.inspectablewrappers.utils.WrapperAdapterUtils WrapperAdapterUtils} + * to creates {@link WrapperAdapter} instances of the given biz interface type + * by the adaptee and underlying instance without writing boilerplate code of creating a new adapter class. * * @param the type of instances that be wrapped * @author Jerry Lee (oldratlee at gmail dot com) * @author Zava Xu (zava dot kid at gmail dot com) * @see Wrapper + * @see io.foldright.inspectablewrappers.utils.WrapperAdapterUtils */ public interface WrapperAdapter extends Wrapper { /** diff --git a/src/main/java/io/foldright/inspectablewrappers/utils/WrapperAdapterUtils.java b/src/main/java/io/foldright/inspectablewrappers/utils/WrapperAdapterUtils.java new file mode 100644 index 0000000..8f46616 --- /dev/null +++ b/src/main/java/io/foldright/inspectablewrappers/utils/WrapperAdapterUtils.java @@ -0,0 +1,119 @@ +package io.foldright.inspectablewrappers.utils; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import io.foldright.inspectablewrappers.Attachable; +import io.foldright.inspectablewrappers.Wrapper; +import io.foldright.inspectablewrappers.WrapperAdapter; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Objects; + + +/** + * Utility class for creating {@link WrapperAdapter} instances. + * + * @author Jerry Lee (oldratlee at gmail dot com) + */ +public final class WrapperAdapterUtils { + /** + * Creates a {@link WrapperAdapter} instance of the given biz interface type + * by the adapted/existed wrapper instance and the underlying instance that be wrapped. + * + * @param bizInterface the class of instances that be wrapped + * @param adaptee the adapted/existed wrapper instance, more info see {@link WrapperAdapter#adaptee()} + * @param underlying the underlying instance that be wrapped, more info see {@link Wrapper#unwrap()} + * @param the type of instances that be wrapped + * @return the {@link WrapperAdapter} instance + * @see Wrapper#unwrap() + * @see WrapperAdapter#adaptee() + */ + @NonNull + public static T createWrapperAdapter(Class bizInterface, T adaptee, T underlying) { + return createWrapperAdapter0( + Objects.requireNonNull(bizInterface, "bizInterface is null"), + Objects.requireNonNull(adaptee, "adaptee is null"), + Objects.requireNonNull(underlying, "underlying is null"), + null); + } + + /** + * Creates a {@link WrapperAdapter} instance of the given biz interface type with attachable interface + * by the adapted/existed wrapper instance, the underlying instance that be wrapped and an attachable instance. + * + * @param bizInterface the class of instances that be wrapped + * @param adaptee the adapted/existed wrapper instance, more info see {@link WrapperAdapter#adaptee()} + * @param underlying the underlying instance that be wrapped, more info see {@link Wrapper#unwrap()} + * @param attachable the attachable instance, more info see {@link Attachable} + * @param the type of instances that be wrapped + * @return the {@link WrapperAdapter} instance with attachable ability + * @see Wrapper#unwrap() + * @see WrapperAdapter#adaptee() + * @see Attachable#getAttachment(Object) + * @see Attachable#setAttachment(Object, Object) + */ + @NonNull + public static T createWrapperAdapter(Class bizInterface, T adaptee, T underlying, Attachable attachable) { + return createWrapperAdapter0( + Objects.requireNonNull(bizInterface, "bizInterface is null"), + Objects.requireNonNull(adaptee, "adaptee is null"), + Objects.requireNonNull(underlying, "underlying is null"), + Objects.requireNonNull(attachable, "attachable is null")); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static T createWrapperAdapter0(Class bizInterface, T adaptee, T underlying, @Nullable Attachable attachable) { + final InvocationHandler handler = (proxy, method, args) -> { + if (isMethodAdaptee(method)) return adaptee; + if (isMethodUnwrap(method)) return underlying; + + if (attachable != null && isMethodGetAttachment(method)) + return ((Attachable) attachable).getAttachment(args[0]); + if (attachable != null && isMethodSetAttachment(method)) { + ((Attachable) attachable).setAttachment(args[0], args[1]); + return null; + } + + return method.invoke(adaptee, args); + }; + + return (T) Proxy.newProxyInstance( + adaptee.getClass().getClassLoader(), + attachable == null + ? new Class[]{bizInterface, WrapperAdapter.class} + : new Class[]{bizInterface, WrapperAdapter.class, Attachable.class}, + handler + ); + } + + private static boolean isMethodAdaptee(Method method) { + return checkMethod(method, "adaptee"); + } + + private static boolean isMethodUnwrap(Method method) { + return checkMethod(method, "unwrap"); + } + + private static boolean isMethodGetAttachment(Method method) { + return checkMethod(method, "getAttachment", Object.class); + } + + private static boolean isMethodSetAttachment(Method method) { + return checkMethod(method, "setAttachment", Object.class, Object.class); + } + + private static boolean checkMethod(Method method, String methodName, Class... parameterTypes) { + return method.getName().equals(methodName) + && method.getParameterCount() == parameterTypes.length + && Arrays.equals(method.getParameterTypes(), parameterTypes); + } + + /** + * NO need to create instance at all + */ + private WrapperAdapterUtils() { + } +} diff --git a/src/test/java/io/foldright/inspectablewrappers/WrapperAdapterTest.kt b/src/test/java/io/foldright/inspectablewrappers/WrapperAdapterTest.kt index 9cd2c9b..02545c8 100644 --- a/src/test/java/io/foldright/inspectablewrappers/WrapperAdapterTest.kt +++ b/src/test/java/io/foldright/inspectablewrappers/WrapperAdapterTest.kt @@ -3,6 +3,7 @@ package io.foldright.inspectablewrappers import io.foldright.inspectablewrappers.Inspector.containsInstanceOnWrapperChain import io.foldright.inspectablewrappers.Inspector.getAttachmentFromWrapperChain import io.foldright.inspectablewrappers.utils.AttachableDelegate +import io.foldright.inspectablewrappers.utils.WrapperAdapterUtils.createWrapperAdapter import io.kotest.assertions.fail import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec @@ -36,6 +37,55 @@ class WrapperAdapterTest : FunSpec({ getAttachmentFromWrapperChain(executorChain, "not existed").shouldBeNull() } + @Suppress("UNCHECKED_CAST") + test("WrapperAdapter - createWrapperAdapter with Attachable") { + val chain: Executor = Executor { runnable -> runnable.run() } + .let { + val existed = ExistedExecutorWrapper(it) + val adapter = createWrapperAdapter( + Executor::class.java, + existed, + it, + AttachableDelegate() + ) + val attachable = adapter as Attachable + attachable.setAttachment(ADAPTED_MSG_KEY, ADAPTED_MSG_VALUE) + attachable.getAttachment(ADAPTED_MSG_KEY) shouldBe ADAPTED_MSG_VALUE + adapter + } + .let(::ChattyExecutorWrapper) + + containsInstanceOnWrapperChain(chain, ExistedExecutorWrapper::class.java).shouldBeTrue() + containsInstanceOnWrapperChain(chain, ChattyExecutorWrapper::class.java).shouldBeTrue() + containsInstanceOnWrapperChain(chain, ExecutorService::class.java).shouldBeFalse() + + val value: String? = getAttachmentFromWrapperChain(chain, ADAPTED_MSG_KEY) + value shouldBe ADAPTED_MSG_VALUE + + getAttachmentFromWrapperChain(chain, "not existed").shouldBeNull() + + // testing the proxy invocation + chain.execute { println("I'm working.") } + } + + test("WrapperAdapter - createWrapperAdapter without Attachable") { + val chain: Executor = Executor { runnable -> runnable.run() } + .let { + createWrapperAdapter( + Executor::class.java, + ExistedExecutorWrapper(it), + it + ) + } + .let(::ChattyExecutorWrapper) + + containsInstanceOnWrapperChain(chain, ExistedExecutorWrapper::class.java).shouldBeTrue() + containsInstanceOnWrapperChain(chain, ChattyExecutorWrapper::class.java).shouldBeTrue() + containsInstanceOnWrapperChain(chain, ExecutorService::class.java).shouldBeFalse() + + getAttachmentFromWrapperChain(chain, "not existed").shouldBeNull() + } + test("ClassCastException") { shouldThrow { val value = getAttachmentFromWrapperChain(executorChain, ADAPTED_MSG_KEY)