Browse Source

Create InvocationHandlerBuilder

master
Paweł Płazieński 3 months ago
parent
commit
73ccf13449
3 changed files with 500 additions and 0 deletions
  1. +206
    -0
      src/main/java/org/perfectable/introspection/proxy/InvocationHandlerBuilder.java
  2. +184
    -0
      src/main/java/org/perfectable/introspection/proxy/Signatures.java
  3. +110
    -0
      src/test/java/org/perfectable/introspection/proxy/InvocationHandlerBuilderTest.java

+ 206
- 0
src/main/java/org/perfectable/introspection/proxy/InvocationHandlerBuilder.java View File

@@ -0,0 +1,206 @@
package org.perfectable.introspection.proxy;

import org.perfectable.introspection.FunctionalReference;

import java.lang.reflect.Method;
import java.util.Map;

import com.google.common.collect.ImmutableMap;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
* Builder pattern for {@link InvocationHandler}.
*
* <p>Handler is built by binding methods to other invocation handlers, so that when single method is called, configured
* handler is executed. When no handler was assigned to invoked method, a fallback is called, which by default throws.
*
* @param <T> type of proxies supported
*/
public final class InvocationHandlerBuilder<T> {
private final Map<Method, InvocationHandler<?, ?, MethodInvocation<T>>> mapping;
private final InvocationHandler<?, ?, MethodInvocation<T>> fallback;

/**
* Creates empty invocation handler builder.
*
* <p>This builder will create handlers that handles invocations by throwing {@code UnsupportedOperationException}.
*
* @param <T> Receiver type for intercepted methods
* @return Unconfigured builder
*/
public static <T> InvocationHandlerBuilder<T> create() {
InvocationHandler<?, ?, MethodInvocation<T>> fallback = invocation -> {
throw new UnsupportedOperationException(); // SUPPRESS JavadocMethod
};
return new InvocationHandlerBuilder<T>(ImmutableMap.of(), fallback);
}

private InvocationHandlerBuilder(Map<Method, InvocationHandler<?, ?, MethodInvocation<T>>> mapping,
InvocationHandler<?, ?, MethodInvocation<T>> fallback) {
this.mapping = mapping;
this.fallback = fallback;
}

/**
* Starts configuration for binding of instance method without result and no arguments.
*
* @param reference instance method reference that will be bound
* @param <X> type of exception thrown by method
* @return Binder that will produce partially configured builder with specified method configured.
*/
@SuppressWarnings("FunctionalInterfaceClash")
public <X extends Throwable>
Binder.Replacing<T, Signatures.Procedure1<T, X>> bind(Signatures.Procedure1<T, X> reference) {
return bindReplacement(reference);
}

/**
* Starts configuration for binding of instance method without result and one arguments.
*
* @param reference instance method reference that will be bound
* @param <X> type of exception thrown by method
* @param <P1> type of first method parameter
* @return Binder that will produce partially configured builder with specified method configured.
*/
@SuppressWarnings("FunctionalInterfaceClash")
public <P1, X extends Throwable>
Binder.Replacing<T, Signatures.Procedure2<T, P1, X>> bind(Signatures.Procedure2<T, P1, X> reference) {
return bindReplacement(reference);
}

/**
* Starts configuration for binding of instance method with result and one arguments.
*
* @param reference instance method reference that will be bound
* @param <X> type of exception thrown by method
* @param <R> type of method result
* @return Binder that will produce partially configured builder with specified method configured.
*/
@SuppressWarnings("FunctionalInterfaceClash")
public <R, X extends Throwable>
Binder.Replacing<T, Signatures.Function1<R, T, X>> bind(Signatures.Function1<R, T, X> reference) {
return bindReplacement(reference);
}


/**
* Starts configuration for binding of instance method with result and one arguments.
*
* @param reference instance method reference that will be bound
* @param <X> type of exception thrown by method
* @param <P1> type of first method parameter
* @param <R> type of method result
* @return Binder that will produce partially configured builder with specified method configured.
*/
@SuppressWarnings("FunctionalInterfaceClash")
public <R, P1, X extends Throwable>
Binder.Replacing<T, Signatures.Function2<R, T, P1, X>> bind(Signatures.Function2<R, T, P1, X> reference) {
return bindReplacement(reference);
}

/**
* Starts configuration for binding of specified method.
*
* @param method method to be bound
* @return Binder that will produce partially configured builder with specified method configured.
*/
public Binder<T> bindMethod(Method method) {
return replacement -> delegateTo(method, replacement);
}

/**
* Configures fallback handler that is called when method with no binding was called.
*
* @param newFallback invocation handler to be called as callback
* @return Builder with same configuration, but with specified callback.
*/
public InvocationHandlerBuilder<T> withFallback(InvocationHandler<?, ?, MethodInvocation<T>> newFallback) {
return new InvocationHandlerBuilder<>(mapping, newFallback);
}

/**
* Builds invocation handler as configured by this builder.
*
* @return Invocation handler configured from this builder
*/
public InvocationHandler<?, ?, MethodInvocation<T>> build() {
return new InvocationHandler<@Nullable Object, Throwable, MethodInvocation<T>>() {
@Override
public @Nullable Object handle(MethodInvocation<T> invocation) throws Throwable {
InvocationHandler<?, ?, MethodInvocation<T>> replacement =
invocation.decompose((method, receiver, arguments) -> mapping.getOrDefault(method, fallback));
return replacement.handle(invocation);
}
};
}

/**
* Instantiates proxy with configured invocation handler.
*
* @param proxyClass class of proxy to instantiate
* @return proxy instance backed by built handler
*/
public T instantiate(Class<T> proxyClass) {
InvocationHandler<?, ?, MethodInvocation<T>> handler = build();
return ProxyBuilder.forType(proxyClass).instantiate(handler);
}

@SuppressWarnings("PMD.UnusedPrivateMethod") // false positive
private <R extends Signatures.Curryable<T, ? extends Signatures.InvocationConvertible<?, ?>> & FunctionalReference>
Binder.Replacing<T, R> bindReplacement(R target) {
Method method = target.introspect().referencedMethod();
return replacement -> delegateTo(method, replacement);
}

private InvocationHandlerBuilder<T> delegateTo(Method method,
InvocationHandler<?, ?, MethodInvocation<T>> handler) {
Map<Method, InvocationHandler<?, ?, MethodInvocation<T>>> newMapping =
ImmutableMap.<Method, InvocationHandler<?, ?, MethodInvocation<T>>>builder()
.putAll(mapping).put(method, handler).build();
return new InvocationHandlerBuilder<T>(newMapping, fallback);
}

/**
* Configures binding between already selected method to specified invocation handler.
*
* @param <T> type of proxy
*/
public interface Binder<T> {
/**
* Binds method invocation to be handled by specific handler.
*
* @param handler handler to execute when method is called
* @return Invocation handler builder with configured method
*/
InvocationHandlerBuilder<T> to(InvocationHandler<?, ?, MethodInvocation<T>> handler);

/**
* Subtype of {@link Binder} which also allows binding to handler that is matched with bound method, passed
* as a lambda or method reference.
*
* @param <T> type of proxy
* @param <F> method signature to be accepted
*/
interface Replacing<T, F extends Signatures.Curryable<T, ? extends Signatures.InvocationConvertible<?, ?>>>
extends Binder<T> {

/**
* Configures builder to use specified replacement execution when matching method is called.
*
* @param replacement code to be called when matching method is invoked
* @return Invocation handler builder with configured method
*/
default InvocationHandlerBuilder<T> as(F replacement) {
InvocationHandler<@Nullable Object, Throwable, MethodInvocation<T>> handler = invocation -> {
Invocation<?, ?> decompose = invocation.decompose((method, receiver, arguments) -> {
Signatures.InvocationConvertible<?, ?> curried = replacement.curry(receiver);
return curried.toInvocation(arguments);
});
return decompose.invoke();
};
return to(handler);
}
}
}

}

+ 184
- 0
src/main/java/org/perfectable/introspection/proxy/Signatures.java View File

@@ -0,0 +1,184 @@
package org.perfectable.introspection.proxy;

import org.perfectable.introspection.FunctionalReference;

import org.checkerframework.checker.nullness.qual.Nullable;

/**
* Collects possible method signatures, to use for binding in {@link InvocationHandlerBuilder}.
*
* <p>This class contains declarations of possible instance method signatures with combinations
* of returning/not returning value and argument count and type.
* Conventions are "Procedure" doesn't have a result (is void) and "Function" does have result
* (as in Ada programming language). After name there is a number of arguments.
*
* @see InvocationHandlerBuilder
*/
@SuppressWarnings("FunctionalInterfaceMethodChanged")
public final class Signatures {

/**
* Marks signature that can be partially applied.
*
* <p>Currying is a process of setting one argument as local constant in function to get function with one less
* parameter.
*
* @param <A> Type of argument that can be removed. This is always first argument of a function/procedure.
* @param <R> Result of currying, a function or procedure with one less parameter.
*/
@FunctionalInterface
public interface Curryable<A, R> {
/**
* Applies first argument and returns partially applied function/procedure.
*
* @param argument1 Argument to be fixed in result
* @return Resulting partial signature
*/
R curry(A argument1);
}

/**
* Marks signature as convertible to Invocation.
*
* @param <R> Result of an invocation
* @param <X> Exception thrown by invocation
*/
public interface InvocationConvertible<R, X extends Throwable> {
/**
* Curries required arguments invocation.
*
* @param arguments arguments to be fixed in the call
* @return Invocation calling the function/procedure with provided arguments.
* @throws IllegalArgumentException if there are invalid amount of arguments provided
*/
Invocation<R, X> toInvocation(@Nullable Object... arguments);
}

@SuppressWarnings("javadoc")
@FunctionalInterface
public interface Procedure0<X extends Throwable> extends InvocationConvertible<Void, X>, FunctionalReference {
void call() throws X;

@Override
default Invocation<Void, X> toInvocation(@Nullable Object... arguments) {
checkArguments(arguments, 0);
return () -> {
call();
return null;
};
}
}

@SuppressWarnings("javadoc")
@FunctionalInterface
public interface Procedure1<P1, X extends Throwable>
extends Curryable<P1, Procedure0<X>>, InvocationConvertible<Void, X>, FunctionalReference {

void call(P1 argument1) throws X;

@Override
default Procedure0<X> curry(P1 argument1) {
return () -> call(argument1);
}

@SuppressWarnings({"unchecked", "assignment.type.incompatible"})
@Override
default Invocation<Void, X> toInvocation(@Nullable Object... arguments) {
checkArguments(arguments, 1);
P1 argument1 = (P1) arguments[0];
return () -> {
call(argument1);
return null;
};
}
}

@SuppressWarnings("javadoc")
@FunctionalInterface
public interface Procedure2<P1, P2, X extends Throwable>
extends Curryable<P1, Procedure1<P2, X>>, InvocationConvertible<Void, X>, FunctionalReference {
void call(P1 argument1, P2 argument2) throws X;

@Override
default Procedure1<P2, X> curry(P1 argument1) {
return argument2 -> call(argument1, argument2);
}

@SuppressWarnings({"unchecked", "assignment.type.incompatible"})
@Override
default Invocation<Void, X> toInvocation(@Nullable Object... arguments) {
checkArguments(arguments, 2);
P1 argument1 = (P1) arguments[0];
P2 argument2 = (P2) arguments[1];
return () -> {
call(argument1, argument2);
return null;
};
}
}

@SuppressWarnings("javadoc")
@FunctionalInterface
public interface Function0<R, X extends Throwable>
extends InvocationConvertible<R, X>, FunctionalReference {
R call() throws X;

@Override
default Invocation<R, X> toInvocation(@Nullable Object... arguments) {
checkArguments(arguments, 0);
return () -> call();
}
}

@SuppressWarnings("javadoc")
@FunctionalInterface
public interface Function1<R, P1, X extends Throwable>
extends Curryable<P1, Function0<R, X>>, InvocationConvertible<R, X>, FunctionalReference {

R call(P1 argument1) throws X;

@Override
default Function0<R, X> curry(P1 argument1) {
return () -> call(argument1);
}

@SuppressWarnings({"unchecked", "assignment.type.incompatible"})
@Override
default Invocation<R, X> toInvocation(@Nullable Object... arguments) {
checkArguments(arguments, 1);
P1 argument1 = (P1) arguments[0];
return () -> call(argument1);
}
}

@SuppressWarnings("javadoc")
@FunctionalInterface
public interface Function2<R, P1, P2, X extends Throwable>
extends Curryable<P1, Function1<R, P2, X>>, InvocationConvertible<R, X>, FunctionalReference {
R call(P1 argument1, P2 argument2) throws X;

@Override
default Function1<R, P2, X> curry(P1 argument1) {
return argument2 -> call(argument1, argument2);
}

@SuppressWarnings({"unchecked", "assignment.type.incompatible"})
@Override
default Invocation<R, X> toInvocation(@Nullable Object... arguments) {
checkArguments(arguments, 2);
P1 argument1 = (P1) arguments[0];
P2 argument2 = (P2) arguments[1];
return () -> call(argument1, argument2);
}
}

private static void checkArguments(@Nullable Object[] arguments, int requiredLength) {
if (arguments.length != requiredLength) {
throw new IllegalArgumentException();
}
}

private Signatures() {
// utility class
}
}

+ 110
- 0
src/test/java/org/perfectable/introspection/proxy/InvocationHandlerBuilderTest.java View File

@@ -0,0 +1,110 @@
package org.perfectable.introspection.proxy;

import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

@SuppressWarnings({"MultipleStringLiterals", "argument.type.incompatible"})
public class InvocationHandlerBuilderTest {

@Test
void testProcedure1() {
Marker<Subject, Void> marker = new Marker<>(null);
Subject proxy = InvocationHandlerBuilder.<Subject>create()
.bind(Subject::doStuff)
.as(receiver -> marker.call(receiver))
.instantiate(Subject.class);

proxy.doStuff();
marker.check(proxy);
}

@Test
void testProcedure2() {
Marker<Subject, Void> marker = new Marker<>(null);
Subject proxy = InvocationHandlerBuilder.<Subject>create()
.bind(Subject::setValue)
.as((receiver, argument) -> marker.call(receiver, argument))
.instantiate(Subject.class);

String newValue = "testValue";
proxy.setValue(newValue);
marker.check(proxy, newValue);
}

@SuppressWarnings("UnnecessaryTypeArgument")
@Test
void testFunction1() {
String result = "testValue";
Marker<Subject, @Nullable String> marker = new Marker<>(result);

Subject proxy = InvocationHandlerBuilder.<Subject>create()
.<@Nullable String, RuntimeException>bind(Subject::getValue)
.as(receiver -> marker.call(receiver))
.instantiate(Subject.class);

@Nullable String actual = proxy.getValue();
assertThat(actual).isEqualTo(result);
marker.check(proxy);
}

@Test
void testFunction2() {
String result = "testValue";
Marker<Subject, @Nullable String> marker = new Marker<>(result);

Subject proxy = InvocationHandlerBuilder.<Subject>create()
.<@Nullable String, @Nullable String, RuntimeException>bind(Subject::replaceValue)
.as((receiver, argument) -> marker.call(receiver, argument))
.instantiate(Subject.class);

String newValue = "newValue";
@Nullable String actual = proxy.replaceValue(newValue);
assertThat(actual).isEqualTo(result);
marker.check(proxy, newValue);
}

@SuppressWarnings("initialization.fields.uninitialized")
private static final class Marker<T, V> {
private boolean called;
private T acceptedReceiver;
private Object[] acceptedArguments;
private final V result;

private Marker(V result) {
this.result = result;
}

@SuppressWarnings("AnnotationLocation")
V call(T receiver, Object... arguments) {
if (called) {
throw new AssertionError("Marker was already called");
}
this.acceptedReceiver = receiver;
this.acceptedArguments = arguments;
this.called = true;
return result;
}

void check(T expectedReceiver, Object... expectedArguments) {
if (!called) {
throw new AssertionError("Marker was not called");
}
assertThat(acceptedReceiver).isSameAs(expectedReceiver);
assertThat(acceptedArguments).isEqualTo(expectedArguments);
}
}

@SuppressWarnings("AnnotationLocation")
private interface Subject {
void doStuff();

@Nullable String getValue();

void setValue(@Nullable String value);

@Nullable String replaceValue(@Nullable String value);
}

}

Loading…
Cancel
Save