diff --git a/README.md b/README.md index da4258f..7fcb0e0 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Maven dependency: org.microbean microbean-interceptor - 0.3.0 + 0.4.0 ``` diff --git a/pom.xml b/pom.xml index a509ed1..4dbb7b0 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.microbean microbean-interceptor - 0.3.1-SNAPSHOT + 0.4.0-SNAPSHOT org.microbean @@ -60,7 +60,7 @@ jakarta.interceptor jakarta.interceptor-api - 2.1.0 + 2.2.0 @@ -281,7 +281,7 @@ false - https://docs.oracle.com/en/java/javase/11/docs/api,https://jakarta.ee/specifications/interceptors/2.1/apidocs/ + https://docs.oracle.com/en/java/javase/11/docs/api,https://jakarta.ee/specifications/interceptors/2.2/apidocs/ deploy,post-site,scm-publish:publish-scm diff --git a/src/main/java/org/microbean/interceptor/Chain.java b/src/main/java/org/microbean/interceptor/Chain.java index c729eb0..6fc206c 100644 --- a/src/main/java/org/microbean/interceptor/Chain.java +++ b/src/main/java/org/microbean/interceptor/Chain.java @@ -48,7 +48,10 @@ * @author Laird Nelson * * @see #proceed() + * + * @deprecated See {@link Interceptions}. */ +@Deprecated(forRemoval = true) public class Chain implements Callable, InvocationContext { diff --git a/src/main/java/org/microbean/interceptor/Interception.java b/src/main/java/org/microbean/interceptor/Interception.java deleted file mode 100644 index 11c7ad8..0000000 --- a/src/main/java/org/microbean/interceptor/Interception.java +++ /dev/null @@ -1,998 +0,0 @@ -/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- - * - * Copyright © 2024 microBean™. - * - * 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 org.microbean.interceptor; - -import java.lang.System.Logger; - -import java.lang.annotation.Annotation; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles.Lookup; -import java.lang.invoke.MethodType; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.Method; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import jakarta.interceptor.InvocationContext; - -import static java.lang.System.getLogger; -import static java.lang.System.Logger.Level.DEBUG; - -import static java.lang.invoke.MethodHandles.lookup; -import static java.lang.invoke.MethodHandles.privateLookupIn; - -import static org.microbean.interceptor.LowLevelOperation.invokeUnchecked; - -/** - * An interception of a construction event, another lifecycle event, or a general purpose method or function. - * - * @author Laird Nelson - * - * @see #call() - * - * @see InvocationContext - */ -public final class Interception implements Callable { - - - /* - * Static fields. - */ - - - private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; - - private static final Logger LOGGER = getLogger(Interception.class.getName()); - - private static final Optional OPTIONAL_EMPTY_OBJECT_ARRAY = Optional.of(EMPTY_OBJECT_ARRAY); - - private static final Lookup lookup = lookup(); - - - /* - * Instance fields. - */ - - - private final Supplier> constructorBootstrap; - - private final ConcurrentMap data; - - private final Supplier> interceptorBindingsBootstrap; - - private final Supplier methodBootstrap; - - private final Callable proceeder; - - private final Supplier timerBootstrap; - - - /* - * Constructors. - */ - - /* - * Lifecycle method/callback. - */ - - /** - * Creates a new {@link Interception} that performs lifecycle event interceptions. - * - *

If this constructor is used, an invocation of the {@link #call()} method will return the value of an invocation - * of the supplied {@code targetBootstrap}'s {@link Supplier#get() get()} method, or {@code null} if the supplied - * {@code targetBootstrap} is, or returns, {@code null}.

- * - * @param interceptorMethods a {@link Collection} of {@link InterceptorMethod}s; may, rather uselessly, be {@code - * null} or empty - * - * @param targetBootstrap a {@link Supplier} of the target instance for which a lifecycle event has occurred; may, - * rather uselessly, be {@code null}, and may, rather uselessly, return {@code null} - * - * @see #call() - */ - public Interception(final Collection interceptorMethods, - final Supplier targetBootstrap) { - this(interceptorMethods, - null, // terminalFunction - false, // setTarget - null, // constructorBootstrap - null, // methodBootstrap - targetBootstrap, - null, // argumentsBootstrap, - null, // argumentsValidator, - null, // timerBootstrap - null); // interceptorBindingsBootstrap - } - - /* - * Around-construct. - */ - - /** - * Creates a new {@link Interception} that performs around-construct interceptions. - * - *

If this constructor is used, an invocation of the {@link #call()} method will return the value of an invocation - * of the supplied {@link Constructor}'s {@link Constructor#newInstance(Object...) newInstance(Object...)} method.

- * - * @param interceptorMethods a {@link Collection} of {@link InterceptorMethod}s; may, rather uselessly, be {@code - * null} - * - * @param constructor the {@link Constructor} being intercepted; must not be {@code null} - * - * @exception NullPointerException if {@code constructor} is {@code null} - * - * @exception IllegalAccessException if {@linkplain Lookup#unreflectConstructor(Constructor) unreflecting} fails - * - * @see #call() - * - * @see #terminalFunctionOf(Constructor) - */ - public Interception(final Collection interceptorMethods, - final Constructor constructor) - throws IllegalAccessException { - this(interceptorMethods, - terminalFunctionOf(constructor), - true, // setTarget - constructorBootstrap(constructor), - null, // methodBootstrap - null, // targetBootstrap - null, // argumentsBootstrap, - argumentsValidator(constructor), - null, // timerBootstrap - null); // interceptorBindingsBootstrap - } - - /** - * Creates a new {@link Interception} that performs around-construct interceptions. - * - *

If this constructor is used, an invocation of the {@link #call()} method will return the value of an invocation - * of the supplied {@link Constructor}'s {@link Constructor#newInstance(Object...) newInstance(Object...)} method.

- * - * @param interceptorMethods a {@link Collection} of {@link InterceptorMethod}s; may, rather uselessly, be {@code - * null} - * - * @param constructor the {@link Constructor} being intercepted; must not be {@code null} - * - * @param argumentsBootstrap a {@link Supplier} of initial arguments that will be supplied to the interception; may be - * {@code null} - * - * @exception NullPointerException if {@code constructor} is {@code null} - * - * @exception IllegalAccessException if {@linkplain Lookup#unreflectConstructor(Constructor) unreflecting} fails - * - * @see #call() - * - * @see #terminalFunctionOf(Constructor) - */ - public Interception(final Collection interceptorMethods, - final Constructor constructor, - final Supplier argumentsBootstrap) - throws IllegalAccessException { - this(interceptorMethods, - terminalFunctionOf(constructor), - true, // setTarget - constructorBootstrap(constructor), - null, // methodBootstrap - null, // targetBootstrap - argumentsBootstrap, - argumentsValidator(constructor), - null, // timerBootstrap - null); // interceptorBindingsBootstrap - } - - /** - * Creates a new {@link Interception} that performs around-construct interceptions. - * - *

If this constructor is used, an invocation of the {@link #call()} method will return the value of an invocation - * of the supplied {@code terminalFunction}'s {@link Function#apply(Object) apply(Object[])} method, or {@code null} - * if the supplied {@code terminalFunction} is {@code null}

- * - * @param interceptorMethods a {@link Collection} of {@link InterceptorMethod}s; may, rather uselessly, be {@code - * null} - * - * @param terminalFunction the terminal {@link Function} being intercepted; may, rather uselessly, be {@code null} - * - * @param argumentsBootstrap a {@link Supplier} of initial arguments that will be supplied to the interception; may be - * {@code null} - * - * @see #call() - */ - public Interception(final Collection interceptorMethods, - final Function terminalFunction, - final Supplier argumentsBootstrap) { - this(interceptorMethods, - terminalFunction, - true, // setTarget - null, // constructorBootstrap - null, // methodBootstrap, - null, // targetBootstrap - argumentsBootstrap, - null, // argumentsValidator, - null, // timerBootstrap - null); // interceptorBindingsBootstrap - } - - /* - * Around-invoke or lifecycle. - */ - - /** - * Creates a new {@link Interception} that performs around-invoke or lifecycle event interceptions. - * - * @param interceptorMethods a {@link Collection} of {@link InterceptorMethod}s; may be {@code null} if this - * interception is a lifecycle event interception - * - * @param method the {@link Method} to intercept; may be {@code null} if this interception is a lifecycle event - * interception - * - * @param targetBootstrap a {@link Supplier} of the target instance for the around-invoke invocation or for which a - * lifecycle event has occurred; may, rather uselessly, be {@code null}, and may, rather uselessly, return {@code - * null}; it is strongly recommended that this {@link Supplier} return a constant value - * - * @exception IllegalAccessException if {@linkplain Lookup#unreflect(Method) unreflecting} fails - * - * @see #call() - */ - public Interception(final Collection interceptorMethods, - final Method method, - final Supplier targetBootstrap) - throws IllegalAccessException { - this(interceptorMethods, - method == null ? null : terminalFunctionOf(method, targetBootstrap(targetBootstrap)), - false, // setTarget - null, // constructorBootstrap - methodBootstrap(method), - targetBootstrap(targetBootstrap), - null, // argumentsBootstrap - argumentsValidator(method), - null, // timerBootstrap - null); // interceptorBindingsBootstrap - } - - /** - * Creates a new {@link Interception} that performs around-invoke or lifecycle event interceptions. - * - * @param interceptorMethods a {@link Collection} of {@link InterceptorMethod}s; may be {@code null} - * - * @param method the {@link Method} to intercept; may be {@code null} if this interception is a lifecycle event - * interception - * - * @param targetBootstrap a {@link Supplier} of the target instance for the around-invoke invocation or for which a - * lifecycle event has occurred; may, rather uselessly, be {@code null}, and may, rather uselessly, return {@code - * null}; it is strongly recommended that this {@link Supplier} return a constant value - * - * @param argumentsBootstrap a {@link Supplier} of initial arguments that will be supplied to the interception; may be - * {@code null} - * - * @exception IllegalAccessException if {@linkplain Lookup#unreflect(Method) unreflecting} fails - * - * @see #call() - */ - public Interception(final Collection interceptorMethods, - final Method method, - final Supplier targetBootstrap, - final Supplier argumentsBootstrap) - throws IllegalAccessException { - this(interceptorMethods, - terminalFunctionOf(method, targetBootstrap(targetBootstrap)), - false, // setTarget - null, // constructorBootstrap - methodBootstrap(method), - targetBootstrap(targetBootstrap), - argumentsBootstrap, - argumentsValidator(method), - null, // timerBootstrap - null); // interceptorBindingsBootstrap - } - - /* - * Around-construct, around-invoke or lifecycle. - */ - - /** - * Creates a new {@link Interception} that performs around-construct, around-invoke or lifecycle event interceptions. - * - * @param interceptorMethods a {@link Collection} of {@link InterceptorMethod}s; may be {@code null} - * - * @param terminalFunction the terminal {@link Function} being intercepted; may be {@code null} if this is a lifecycle - * event interception - * - * @param aroundConstruct whether this is an around-construct interception - * - * @param targetBootstrap a {@link Supplier} of the target instance for an around-invoke invocation or for which a - * lifecycle event has occurred; may, rather uselessly, be {@code null}, and may, rather uselessly, return {@code - * null}; ignored if {@code aroundConstruct} is {@code true} - * - * @param argumentsBootstrap a {@link Supplier} of initial arguments that will be supplied to the interception; may be - * {@code null} - * - * @see #call() - */ - public Interception(final Collection interceptorMethods, - final Function terminalFunction, - final boolean aroundConstruct, - final Supplier targetBootstrap, - final Supplier argumentsBootstrap) { - this(interceptorMethods, - terminalFunction, - aroundConstruct, - null, // constructorBootstrap - null, // methodBootstrap - targetBootstrap(targetBootstrap), - argumentsBootstrap, - null, // argumentsValidator - null, // timerBootstrap - null); // interceptorBindingsBootstrap - } - - /* - * Kitchen sink. Everything is nullable with null meaning, in general, "use a sensible default". - */ - - private Interception(final Collection interceptorMethods, - final Function terminalFunction, // null means lifecycle event - final boolean setTarget, // ignored if interceptorMethods is null or empty - final Supplier> constructorBootstrap, // better be memoized - final Supplier methodBootstrap, // better be memoized - final Supplier targetBootstrap, // ignored if terminalFunction is null or setTarget is true - final Supplier argumentsBootstrap, // ignored if terminalFunction is null - final Consumer argumentsValidator, // ignored if terminalFunction is null - final Supplier timerBootstrap, // better be memoized - final Supplier> interceptorBindingsBootstrap) { // better be memoized - super(); - this.data = new ConcurrentHashMap<>(); - this.interceptorBindingsBootstrap = interceptorBindingsBootstrap == null ? Set::of : interceptorBindingsBootstrap; - if (terminalFunction == null) { - // Lifecycle event interception. Intercepted by lifecycle callback interceptor methods. - // - // Examples: @PostConstruct, @PreDestroy - if (setTarget) { - throw new IllegalArgumentException("terminalFunction: null; setTarget: true"); - } else if (interceptorMethods == null || interceptorMethods.isEmpty()) { - // Pathological (no interception; no terminal function). - this.proceeder = Interception::returnNull; - } else { - final Supplier tb = targetBootstrap == null ? Interception::returnNull : targetBootstrap; - final List ims = List.copyOf(interceptorMethods); - this.proceeder = () -> - new State(tb) - .new Context(ims.iterator()) - .proceed(); - } - this.constructorBootstrap = Interception::returnNull; - this.methodBootstrap = Interception::returnNull; - this.timerBootstrap = Interception::returnNull; - } else if (setTarget) { - // Around-construct interception. Intercepted by lifecycle callback interceptor methods. - // - // Example: @AroundConstruct - if (interceptorMethods == null || interceptorMethods.isEmpty()) { - // Pathological (no interception; only the terminal constructor function). - final Supplier ab = - argumentsBootstrap == null ? Interception::emptyObjectArray : argumentsBootstrap; - this.proceeder = () -> terminalFunction.apply(ab.get()); - } else if (targetBootstrap == null) { - final List ims = List.copyOf(interceptorMethods); - this.proceeder = () -> { - final State s = new State(argumentsBootstrap, argumentsValidator); - final State.Context c = s.new Context(ims.iterator(), args -> s.target(terminalFunction.apply(args))); - // This return-value-ignoring ic.proceed() call will look odd to future maintainers (future me). - // - // An around-construct interceptor method can have one of two signatures: - // - // void (InvocationContext) - // Object (InvocationContext) // this one is odd - // - // The one that returns Object is mainly so you can use the same method to intercept constructors and business - // methods. Its return value in a constructor interception scenario is basically undefined. In - // around-construct situations I'm not sure why you would rely on this value. We log it here just to make sure - // it doesn't get lost. - final Object v = c.proceed(); - final Object t = c.getTarget(); - if (v != t && LOGGER.isLoggable(DEBUG)) { - LOGGER.log(DEBUG, "around-construct proceed() return value: " + v + "; returning getTarget() return value: " + t); - } - return t; - }; - } else { - throw new IllegalArgumentException("setTarget: true; targetBootstrap: " + targetBootstrap); - } - this.constructorBootstrap = constructorBootstrap == null ? Interception::returnNull : constructorBootstrap; - // little extension here for, say, factory methods - this.methodBootstrap = methodBootstrap == null ? Interception::returnNull : methodBootstrap; - this.timerBootstrap = Interception::returnNull; - } else { - // Around-invoke or around-timeout interception. Intercepted by business method interceptor methods and timeout - // method interceptor methods respectively. You need a target, so you need a targetBootstrap. - // - // Examples: @AroundInvoke, @AroundTimeout - if (interceptorMethods == null || interceptorMethods.isEmpty()) { - // Pathological (no interception; only the terminal function). - final Supplier ab = - argumentsBootstrap == null ? Interception::emptyObjectArray : argumentsBootstrap; - this.proceeder = () -> terminalFunction.apply(ab.get()); - } else { - final Supplier tb = targetBootstrap == null ? Interception::returnNull : targetBootstrap; - final List ims = List.copyOf(interceptorMethods); - this.proceeder = () -> - new State(tb, argumentsBootstrap, argumentsValidator, true) - .new Context(ims.iterator(), terminalFunction) - .proceed(); - } - this.constructorBootstrap = Interception::returnNull; - this.methodBootstrap = methodBootstrap == null ? Interception::returnNull : methodBootstrap; - this.timerBootstrap = timerBootstrap == null ? Interception::returnNull : timerBootstrap; - } - } - - - /* - * Instance methods. - */ - - - /** - * Invokes the interception chain this {@link Interception} represents and returns the result, which may be {@code - * null}. - * - * @return the result of interception - * - * @exception Exception if an error occurs - */ - @Override // Callable - public final Object call() throws Exception { - return this.proceeder.call(); - } - - - /* - * Static methods. - */ - - - /** - * Creates and returns a {@link Function} encapsulating the supplied {@link Constructor}. - * - * @param c a {@link Constructor}; must not be {@code null} - * - * @return a {@link Function} encapsulating the supplied {@link Constructor}; never {@code null} - * - * @exception NullPointerException if {@code c} is {@code null} - * - * @exception IllegalAccessException if {@linkplain Lookup#unreflectConstructor(Constructor) unreflecting} fails - */ - public static final Function terminalFunctionOf(final Constructor c) throws IllegalAccessException { - return terminalFunctionOf(privateLookupIn(c.getDeclaringClass(), lookup).unreflectConstructor(c), null); - } - - /** - * Creates and returns a {@link Function} encapsulating the supplied {@code static} {@link Method}. - * - * @param staticMethod a {@code static} {@link Method}; must not be {@code null} - * - * @return a {@link Function} encapsulating the supplied {@code static} {@link Method}; never {@code null} - * - * @exception NullPointerException if {@code staticMethod} is {@code null} - * - * @exception IllegalAccessException if {@linkplain Lookup#unreflect(Method) unreflecting} fails - */ - public static final Function terminalFunctionOf(final Method staticMethod) throws IllegalAccessException { - return terminalFunctionOf(staticMethod, null); - } - - /** - * Creates and returns a {@link Function} encapsulating the supplied {@link Method}. - * - * @param m a {@link Method}; must not be {@code null}. If {@code m} is a virtual method, then the supplied {@code - * receiverSupplier} will be used to supply its receiver - * - * @param receiverSupplier a {@link Supplier} of the receiver for the supplied {@link Method}; may be {@code null} in - * which case the supplied {@link Method} must be {@code static} - * - * @return a {@link Function} encapsulating the supplied {@link Method}; never {@code null} - * - * @exception NullPointerException if {@code m} is {@code null} - * - * @exception IllegalAccessException if {@linkplain Lookup#unreflect(Method) unreflecting} fails - */ - public static final Function terminalFunctionOf(final Method m, final Supplier receiverSupplier) - throws IllegalAccessException { - return terminalFunctionOf(privateLookupIn(m.getDeclaringClass(), lookup).unreflect(m), receiverSupplier); - } - - /** - * Creates and returns a {@link Function} encapsulating the supplied {@link MethodHandle}. - * - * @param receiverlessMethodHandle a {@link MethodHandle}; must not be {@code null}; must be receiverless or - * {@linkplain MethodHandle#bindTo(Object) bound} to a receiver already. - * - * @return a {@link Function} encapsulating the supplied {@link MethodHandle}; never {@code null} - * - * @exception NullPointerException if {@code receiverlessMethodHandle} is {@code null} - */ - public static final Function terminalFunctionOf(final MethodHandle receiverlessMethodHandle) { - return terminalFunctionOf(receiverlessMethodHandle, null); - } - - /** - * Creates and returns a {@link Function} encapsulating the supplied {@link MethodHandle}. - * - * @param mh a {@link MethodHandle}; must not be {@code null}. If {@code receiverSupplier} is non-{@code null}, then - * {@code mh} must accept at least one leading argument (a receiver supplied by the supplied {@code - * receiverSupplier}). - * - * @param receiverSupplier a {@link Supplier} of the receiver for the supplied {@link MethodHandle}; may be {@code - * null} in which case the supplied {@link MethodHandle} must be receiverless - * - * @return a {@link Function} encapsulating the supplied {@link MethodHandle}; never {@code null} - * - * @exception NullPointerException if {@code mh} is {@code null} - */ - public static final Function terminalFunctionOf(MethodHandle mh, final Supplier receiverSupplier) { - mh = mh.asType(mh.type().changeReturnType(Object.class)); - MethodType mt = mh.type(); - - final int pc = mt.parameterCount(); - - final MethodHandle terminalFunction; - - if (receiverSupplier == null) { - // Static - switch (pc) { - case 0: - terminalFunction = mh; - return ps -> invokeUnchecked(terminalFunction::invokeExact); - default: - terminalFunction = mh.asSpreader(Object[].class, pc); - return ps -> invokeUnchecked(() -> terminalFunction.invokeExact(ps)); - } - } - - // Virtual - mh = mh.asType(mt.changeParameterType(0, Object.class)); - mt = mh.type(); - - switch (pc) { - case 0: - throw new AssertionError(); // changeParameterType() would have blown up - case 1: - terminalFunction = mh; - return ps -> invokeUnchecked(() -> terminalFunction.invokeExact(receiverSupplier.get())); - default: - terminalFunction = mh.asSpreader(Object[].class, pc - 1); - return ps -> invokeUnchecked(() -> terminalFunction.invokeExact(receiverSupplier.get(), ps)); - } - } - - /** - * A convenience method that ensures that every element of the supplied {@code arguments} array can be assigned to a - * reference bearing the corresponding {@link Class} drawn from the supplied {@code parameterTypes} array. - * - *

Boxing, unboxing and widening conversions are taken into consideration.

- * - *

This method implements the logic implied, but nowhere actually specified, by the contract of {@link - * InvocationContext#setParameters(Object[])}.

- * - * @param parameterTypes an array of {@link Class} instances; may be {@code null}; must not contain {@code null} - * elements or {@code void.class}; must have a length equal to that of the supplied {@code arguments} array - * - * @param arguments an array of {@link Object}s; may be {@code null}; must have a length equal to that of the supplied - * {@code parameterTypes} array - * - * @exception IllegalArgumentException if validation fails, i.e. if the length of {@code parameterTypes} is not equal - * to the length of {@code arguments}, or if an element of {@code parameterTypes} is {@code null} or {@code - * void.class}, or if not every element of the supplied {@code arguments} array can be assigned to a reference bearing - * the corresponding {@link Class} drawn from the supplied {@code parameterTypes} array - */ - public static final void validate(final Class[] parameterTypes, final Object[] arguments) { - final int parameterTypesLength = parameterTypes == null ? 0 : parameterTypes.length; - final int argumentsLength = arguments == null ? 0 : arguments.length; - if (argumentsLength != parameterTypesLength) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments)); - } - for (int i = 0; i < argumentsLength; i++) { - final Class parameterType = parameterTypes[i]; - if (parameterType == null || parameterType == void.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: " + parameterType); - } - final Object argument = arguments[i]; - if (argument == null) { - if (parameterType.isPrimitive() || parameterType != Void.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: " + parameterType.getName() + - "; argument: null"); - } - } else { - final Class argumentType = argument.getClass(); - if (parameterType != argumentType) { - if (parameterType == boolean.class) { - if (argumentType != Boolean.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: boolean" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == Boolean.class) { - if (argumentType != boolean.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: java.lang.Boolean" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == byte.class) { - if (argumentType != Byte.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: byte" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == Byte.class) { - if (argumentType != byte.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: java.lang.Byte" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == char.class) { - if (argumentType != Character.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: char" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == Character.class) { - if (argumentType != char.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: java.lang.Character" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == double.class) { - if (argumentType != byte.class && - argumentType != char.class && - argumentType != Double.class && - argumentType != float.class && - argumentType != int.class && - argumentType != long.class && - argumentType != short.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: double" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == Double.class) { - if (argumentType != double.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: java.lang.Double" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == float.class) { - if (argumentType != byte.class && - argumentType != char.class && - argumentType != Float.class && - argumentType != int.class && - argumentType != long.class && - argumentType != short.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: float" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == Float.class) { - if (argumentType != float.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: java.lang.Float" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == int.class) { - if (argumentType != byte.class && - argumentType != char.class && - argumentType != Integer.class && - argumentType != short.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: int; argument type: " + argumentType.getName()); - } - } else if (parameterType == Integer.class) { - if (argumentType != int.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: java.lang.Integer" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == long.class) { - if (argumentType != byte.class && - argumentType != char.class && - argumentType != int.class && - argumentType != Long.class && - argumentType != short.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: long" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == Long.class) { - if (argumentType != long.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: java.lang.Long" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == short.class) { - if (argumentType != byte.class && - argumentType != Short.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: byte" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == Short.class) { - if (argumentType != short.class) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: java.lang.Short" + - "; argument type: " + argumentType.getName()); - } - } else if (parameterType == Void.class || !parameterType.isAssignableFrom(argumentType)) { - throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + - "; arguments: " + Arrays.toString(arguments) + - "; parameter type: " + parameterType.getName() + - "; argument type: " + argumentType.getName()); - } - } - } - } - } - - private static final Object[] emptyObjectArray() { - return EMPTY_OBJECT_ARRAY; - } - - private static final T returnNull() { - return null; - } - - private static final void sink(final T ignored) { - - } - - private static final Consumer argumentsValidator(final Executable e) { - return e == null ? Interception::sink : args -> validate(e.getParameterTypes(), args); - } - - private static final Supplier> constructorBootstrap(final Constructor c) { - return c == null ? Interception::returnNull : () -> c; - } - - private static final Supplier methodBootstrap(final Method m) { - return m == null ? Interception::returnNull : () -> m; - } - - private static final Supplier targetBootstrap(final Supplier s) { - return s == null ? Interception::returnNull : s; - } - - private static final X throwIllegalStateException() { - throw new IllegalStateException(); - } - - private static final X throwIllegalStateException(final X ignored) { - throw new IllegalStateException(); - } - - - /* - * Inner and nested classes. - */ - - - private final class State { - - private volatile Optional arguments; - - private final Supplier argumentsReader; - - private final Consumer argumentsInstaller; - - private volatile Optional target; - - private final Supplier targetReader; - - // "update and get" semantics - private final Function targetInstaller; - - private State(final Supplier targetBootstrap) { - this(targetBootstrap, null, null, false); - } - - private State(final Supplier argumentsBootstrap, - final Consumer argumentsValidator) { - this(null, argumentsBootstrap, argumentsValidator, true); - } - - private State(final Supplier targetBootstrap, // nullable - final Supplier argumentsBootstrap, // ignored if mutableArguments is false - final Consumer argumentsValidator, // ignored if mutable arguments is false - final boolean mutableArguments) { - super(); - if (targetBootstrap == null) { - this.target = Optional.empty(); - this.targetInstaller = target -> { - this.target = Optional.ofNullable(target); // volatile write - return target; - }; - this.targetReader = () -> this.target.orElse(null); // volatile read - } else { - this.targetInstaller = Interception::throwIllegalStateException; - this.targetReader = () -> { - Optional target = this.target; // volatile read - if (target == null) { - target = Optional.ofNullable(targetBootstrap.get()); - this.target = target; // volatile write - } - return target.orElse(null); - }; - } - if (mutableArguments) { - if (argumentsBootstrap == null) { - this.arguments = OPTIONAL_EMPTY_OBJECT_ARRAY; - this.argumentsReader = this.arguments::orElseThrow; // volatile read - } else { - this.argumentsReader = () -> { - Optional arguments = this.arguments; // volatile read - if (arguments == null) { - final Object[] a = argumentsBootstrap.get(); - arguments = a == null || a.length == 0 ? OPTIONAL_EMPTY_OBJECT_ARRAY : Optional.of(a); - this.arguments = arguments; // volatile write - } - return arguments.orElse(null); - }; - } - final Consumer av = argumentsValidator == null ? Interception::sink : argumentsValidator; - this.argumentsInstaller = args -> { - if (args == null || args.length == 0) { - av.accept(EMPTY_OBJECT_ARRAY); - this.arguments = OPTIONAL_EMPTY_OBJECT_ARRAY; // volatile write - } else { - av.accept(args); - this.arguments = Optional.of(args.clone()); // volatile write - } - }; - } else { - this.argumentsReader = Interception::throwIllegalStateException; - this.argumentsInstaller = Interception::throwIllegalStateException; - } - } - - private final Object[] arguments() { - return this.argumentsReader.get(); - } - - private final void arguments(final Object[] arguments) { - this.argumentsInstaller.accept(arguments); - } - - private final Object target() { - return this.targetReader.get(); - } - - // "update and get" semantics - private final Object target(final Object target) { - return this.targetInstaller.apply(target); - } - - - /* - * Inner and nested classes. - */ - - - private final class Context implements InvocationContext { - - private final Callable proceeder; - - private Context(final Iterator iterator) { - this(iterator, Interception::returnNull); - } - - private Context(final Iterator iterator, final Function tf) { - this(iterator, () -> tf.apply(arguments())); - } - - private Context(final Iterator iterator, final Callable tc) { - super(); - if (iterator == null || !iterator.hasNext()) { - this.proceeder = tc == null ? Interception::returnNull : tc; - } else { - final InterceptorMethod im = Objects.requireNonNull(iterator.next()); - final Context c = new Context(iterator, tc); - this.proceeder = () -> im.intercept(c); - } - } - - @Override // InvocationContext - public final Constructor getConstructor() { - return constructorBootstrap.get(); - } - - @Override // InvocationContext - public final Map getContextData() { - return data; - } - - // So deeply unfortunate this is going to be part of Interceptors 2.2 - // @Override - public final Set getInterceptorBindings() { - return interceptorBindingsBootstrap.get(); - } - - @Override // InvocationContext - public final Method getMethod() { - return methodBootstrap.get(); - } - - @Override // InvocationContext - public final Object[] getParameters() { - return arguments(); - } - - @Override // InvocationContext - public final Object getTarget() { - return target(); - } - - @Override // InvocationContext - public final Object getTimer() { - return timerBootstrap.get(); - } - - @Override // InvocationContext - public final Object proceed() throws Exception { - return this.proceeder.call(); - } - - @Override // InvocationContext - public final void setParameters(final Object[] arguments) { - arguments(arguments); - } - - } - - } - -} diff --git a/src/main/java/org/microbean/interceptor/InterceptionFunction.java b/src/main/java/org/microbean/interceptor/InterceptionFunction.java new file mode 100644 index 0000000..d817bbc --- /dev/null +++ b/src/main/java/org/microbean/interceptor/InterceptionFunction.java @@ -0,0 +1,40 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2024 microBean™. + * + * 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 org.microbean.interceptor; + +/** + * A {@linkplain FunctionalInterface functional interface} whose implementations represent an interception of some kind. + * + * @author Laird Nelson + * + * @see #apply(Object...) + */ +@FunctionalInterface +public interface InterceptionFunction { + + /** + * Applies the interception represented by this {@link InterceptionFunction}, with the supplied arguments, and returns + * the result. + * + * @param arguments arguments to the interception; must not be a {@code null} array + * + * @return the result of the interception, which may be {@code null} + * + * @exception NullPointerException if {@code arguments} is a {@code null} array + * + * @see Interceptions + */ + public Object apply(final Object... arguments); + +} diff --git a/src/main/java/org/microbean/interceptor/Interceptions.java b/src/main/java/org/microbean/interceptor/Interceptions.java new file mode 100644 index 0000000..875a4bc --- /dev/null +++ b/src/main/java/org/microbean/interceptor/Interceptions.java @@ -0,0 +1,1104 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2024 microBean™. + * + * 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 org.microbean.interceptor; + +import java.lang.System.Logger; + +import java.lang.annotation.Annotation; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.lang.invoke.VarHandle; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import jakarta.interceptor.InvocationContext; + +import static java.lang.System.getLogger; +import static java.lang.System.Logger.Level.DEBUG; + +import static java.lang.invoke.MethodHandles.lookup; +import static java.lang.invoke.MethodHandles.privateLookupIn; + +/** + * A utility class that makes {@link InterceptionFunction}s and {@link Runnable}s that intercept lifecycle events, + * constructions, and invocations of methods in accordance with the Jakarta Interceptors specification. + * + * @author Laird Nelson + */ +public final class Interceptions { + + + /* + * Static fields. + */ + + + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + private static final Property EMPTY_OBJECT_ARRAY_PROPERTY = new Property<>(Interceptions::emptyObjectArray, false); + + private static final Logger LOGGER = getLogger(Interceptions.class.getName()); + + private static final Optional OPTIONAL_EMPTY_OBJECT_ARRAY = Optional.of(EMPTY_OBJECT_ARRAY); + + private static final Lookup lookup = lookup(); + + + /* + * Instance fields. + */ + + + private final ConcurrentMap data; + + private final Supplier> constructorBootstrap; + + private final Supplier methodBootstrap; + + private final Supplier timerBootstrap; + + private final Supplier> interceptorBindingsBootstrap; + + + /* + * Constructors. + */ + + + private Interceptions(final Supplier> constructorBootstrap, + final Supplier methodBootstrap, + final Supplier timerBootstrap, + final Supplier> interceptorBindingsBootstrap) { + super(); + this.data = new ConcurrentHashMap<>(); + this.constructorBootstrap = constructorBootstrap == null ? Interceptions::returnNull : constructorBootstrap; + this.methodBootstrap = methodBootstrap == null ? Interceptions::returnNull : methodBootstrap; + this.timerBootstrap = timerBootstrap == null ? Interceptions::returnNull : timerBootstrap; + this.interceptorBindingsBootstrap = interceptorBindingsBootstrap == null ? Set::of : interceptorBindingsBootstrap; + } + + + /* + * Static methods. + */ + + + /** + * Returns a {@link Runnable} whose {@link Runnable#run() run()} method will invoke all supplied {@link + * InterceptorMethod}s in encounter order. + * + * @param interceptorMethods the {@link InterceptorMethod}s to invoke; may be {@code null} in which case the returned + * {@link Runnable}'s {@link Runnable#run() run()} method will do nothing + * + * @param targetBootstrap a {@link Supplier} that will be called for the initial value to be returned by the first + * invocation of {@link InvocationContext#getTarget()}; may be {@code null} in which case the value too will be {@code + * null} + * + * @return a {@link Runnable}; never {@code null} + */ + // Methodless lifecycle event interception. For example, post-construct interceptions by external interceptors only. + public static final Runnable ofLifecycleEvent(final Collection interceptorMethods, + final Supplier targetBootstrap) { + return ofLifecycleEvent(interceptorMethods, targetBootstrap, Set::of); + } + + /** + * Returns a {@link Runnable} whose {@link Runnable#run() run()} method will invoke all supplied {@link + * InterceptorMethod}s in encounter order. + * + * @param interceptorMethods the {@link InterceptorMethod}s to invoke; may be {@code null} in which case the returned + * {@link Runnable}'s {@link Runnable#run() run()} method will do nothing + * + * @param targetBootstrap a {@link Supplier} that will be called for the initial value to be returned by the first + * invocation of {@link InvocationContext#getTarget()}; may be {@code null} in which case the value too will be {@code + * null} + * + * @param interceptorBindingsBootstrap a {@link Supplier} of a {@link Set} of {@link Annotation}s that will be called + * for the value to be returned by the first invocation of {@link InvocationContext#getInterceptorBindings()}; may be + * {@code null} in which case the value will be an {@linkplain Set#of() empty, immutable Set} + * + * @return a {@link Runnable}; never {@code null} + */ + // Methodless lifecycle event interception. For example, post-construct interceptions by external interceptors only. + public static final Runnable ofLifecycleEvent(final Collection interceptorMethods, + final Supplier targetBootstrap, + final Supplier> interceptorBindingsBootstrap) { + return () -> { + ff(interceptorMethods, + null, + targetBootstrap, + null, // argumentsValidator + false, // setTarget + null, // cb + null, // mb // TODO: may have to be the last im in the chain if it's defined on the target class (!); weird requirement + null, // tb + interceptorBindingsBootstrap) + .apply(EMPTY_OBJECT_ARRAY); + }; + } + + /** + * Returns an {@link InterceptionFunction} whose {@link InterceptionFunction#apply(Object...) apply(Object...)} method + * will invoke all supplied {@link InterceptorMethod}s in encounter order before invoking the supplied {@link + * Constructor}'s {@link Constructor#newInstance(Object...) newInstance(Object...)} method. + * + * @param interceptorMethods the {@link InterceptorMethod}s to invoke; may be {@code null} (rather uselessly) + * + * @param constructor the {@link Constructor} to invoke; may be {@code null} (rather uselessly) + * + * @return an {@link InterceptionFunction}; never {@code null} + * + * @exception IllegalAccessException if {@linkplain Lookup#unreflectConstructor(Constructor) unreflecting} fails + */ + // Around-construct + public static final InterceptionFunction ofConstruction(final Collection interceptorMethods, + final Constructor constructor) + throws IllegalAccessException { + return ofConstruction(interceptorMethods, constructor, Set::of); + } + + /** + * Returns an {@link InterceptionFunction} whose {@link InterceptionFunction#apply(Object...) apply(Object...)} method + * will invoke all supplied {@link InterceptorMethod}s in encounter order before invoking the supplied {@link + * Constructor}'s {@link Constructor#newInstance(Object...) newInstance(Object...)} method. + * + * @param interceptorMethods the {@link InterceptorMethod}s to invoke; may be {@code null} (rather uselessly) + * + * @param constructor the {@link Constructor} to invoke; may be {@code null} (rather uselessly) + * + * @param interceptorBindingsBootstrap a {@link Supplier} of a {@link Set} of {@link Annotation}s that will be called + * for the value to be returned by the first invocation of {@link InvocationContext#getInterceptorBindings()}; may be + * {@code null} in which case the value will be an {@linkplain Set#of() empty, immutable Set} + * + * @return an {@link InterceptionFunction}; never {@code null} + * + * @exception IllegalAccessException if {@linkplain Lookup#unreflectConstructor(Constructor) unreflecting} fails + */ + // Around-construct + public static final InterceptionFunction ofConstruction(final Collection interceptorMethods, + final Constructor constructor, + final Supplier> interceptorBindingsBootstrap) + throws IllegalAccessException { + return + ff(interceptorMethods, + terminalBiFunctionOf(constructor), + null, // targetBootstrap + a -> validate(constructor.getParameterTypes(), a), + true, // setTarget + () -> constructor, + null, // mb + null, // tb + interceptorBindingsBootstrap); + } + + /** + * Returns an {@link InterceptionFunction} whose {@link InterceptionFunction#apply(Object...) apply(Object...)} method + * will invoke all supplied {@link InterceptorMethod}s in encounter order before invoking the supplied {@link + * BiFunction}'s {@link BiFunction#apply(Object, Object) apply(Object, Object[])} method with {@code null} (the return + * value of {@link InvocationContext#getTarget()}, which will always be {@code null} in this scenario) and the return + * value of an invocation of {@link InvocationContext#getParameters()}. + * + * @param interceptorMethods the {@link InterceptorMethod}s to invoke; may be {@code null} (rather uselessly) + * + * @param terminalBiFunction a {@link BiFunction} serving as a notional constructor that takes {@code null}, always, + * as its first argument, and the return value of an invocation of {@link InvocationContext#getParameters()} as its + * second argument, and returns a new instance; may be {@code null} (rather uselessly) + * + * @return an {@link InterceptionFunction}; never {@code null} + */ + // Around-construct + public static final InterceptionFunction ofConstruction(final Collection interceptorMethods, + final BiFunction terminalBiFunction) { + return ofConstruction(interceptorMethods, terminalBiFunction, Set::of); + } + + /** + * Returns an {@link InterceptionFunction} whose {@link InterceptionFunction#apply(Object...) apply(Object...)} method + * will invoke all supplied {@link InterceptorMethod}s in encounter order before invoking the supplied {@link + * BiFunction}'s {@link BiFunction#apply(Object, Object) apply(Object Object[])} method with {@code null} (the return + * value of {@link InvocationContext#getTarget()}, which will always be {@code null} in this scenario) and the return + * value of an invocation of {@link InvocationContext#getParameters()}. + * + * @param interceptorMethods the {@link InterceptorMethod}s to invoke; may be {@code null} (rather uselessly) + * + * @param terminalBiFunction a {@link BiFunction} serving as a notional constructor that takes {@code null}, always, + * as its first argument, and the return value of an invocation of {@link InvocationContext#getParameters()} as its + * second argument, and returns a new instance; may be {@code null} (rather uselessly) + * + * @param interceptorBindingsBootstrap a {@link Supplier} of a {@link Set} of {@link Annotation}s that will be called + * for the value to be returned by the first invocation of {@link InvocationContext#getInterceptorBindings()}; may be + * {@code null} in which case the value will be an {@linkplain Set#of() empty, immutable Set} + * + * @return an {@link InterceptionFunction}; never {@code null} + */ + // Around-construct + public static final InterceptionFunction ofConstruction(final Collection interceptorMethods, + final BiFunction terminalBiFunction, + final Supplier> interceptorBindingsBootstrap) { + return + ff(interceptorMethods, + terminalBiFunction, + null, // targetBootstrap + null, // argumentsValidator + true, // setTarget + null, // cb + null, // mb + null, // tb + interceptorBindingsBootstrap); + } + + /** + * Returns an {@link InterceptionFunction} whose {@link InterceptionFunction#apply(Object...) apply(Object...)} method + * will invoke all supplied {@link InterceptorMethod}s in encounter order before invoking the supplied {@link + * Method}'s {@link Method#invoke(Object, Object...) invoke(Object, Object...)} method with the return value of {@link + * InvocationContext#getTarget()}, and with the return value of {@link InvocationContext#getParameters()}. + * + * @param interceptorMethods the {@link InterceptorMethod}s to invoke; may be {@code null} (rather uselessly) + * + * @param method a {@link Method} encapsulating the invocation to be intercepted whose {@link Method#invoke(Object, + * Object...) invoke(Object, Object...)} method takes the return value of {@link InvocationContext#getTarget()} as + * its first argument, and the return value of {@link InvocationContext#getParameters()} spread out appropriately as + * its trailing arguments; may be {@code null} (rather uselessly) + * + * @param targetBootstrap a {@link Supplier} that will be called for the initial value to be returned by the first + * invocation of {@link InvocationContext#getTarget()}; may be {@code null} in which case the value too will be {@code + * null} + * + * @return an {@link InterceptionFunction}; never {@code null} + * + * @exception IllegalAccessException if {@linkplain Lookup#unreflect(Method) unreflecting} fails + */ + // Around-invoke or similar. + public static final InterceptionFunction ofInvocation(final Collection interceptorMethods, + final Method method, // not nullable + final Supplier targetBootstrap) + throws IllegalAccessException { + return ofInvocation(interceptorMethods, method, targetBootstrap, Set::of); + } + + /** + * Returns an {@link InterceptionFunction} whose {@link InterceptionFunction#apply(Object...) apply(Object...)} method + * will invoke all supplied {@link InterceptorMethod}s in encounter order before invoking the supplied {@link + * Method}'s {@link Method#invoke(Object, Object...) invoke(Object, Object...)} method with the return value of {@link + * InvocationContext#getTarget()}, and with the return value of {@link InvocationContext#getParameters()}. + * + * @param interceptorMethods the {@link InterceptorMethod}s to invoke; may be {@code null} (rather uselessly) + * + * @param method a {@link Method} encapsulating the invocation to be intercepted whose {@link Method#invoke(Object, + * Object...) invoke(Object, Object...)} method takes the return value of {@link InvocationContext#getTarget()} as + * its first argument, and the return value of {@link InvocationContext#getParameters()} spread out appropriately as + * its trailing arguments; may be {@code null} (rather uselessly) + * + * @param targetBootstrap a {@link Supplier} that will be called for the initial value to be returned by the first + * invocation of {@link InvocationContext#getTarget()}; may be {@code null} in which case the value too will be {@code + * null} + * + * @param interceptorBindingsBootstrap a {@link Supplier} of a {@link Set} of {@link Annotation}s that will be called + * for the value to be returned by the first invocation of {@link InvocationContext#getInterceptorBindings()}; may be + * {@code null} in which case the value will be an {@linkplain Set#of() empty, immutable Set} + * + * @return an {@link InterceptionFunction}; never {@code null} + * + * @exception IllegalAccessException if {@linkplain Lookup#unreflect(Method) unreflecting} fails + */ + // Around-invoke or similar. + public static final InterceptionFunction ofInvocation(final Collection interceptorMethods, + final Method method, // not nullable + final Supplier targetBootstrap, + final Supplier> interceptorBindingsBootstrap) + throws IllegalAccessException { + return + ff(interceptorMethods, + method == null ? null : terminalBiFunctionOf(method), + targetBootstrap, + method == null ? null : a -> validate(method.getParameterTypes(), a), + false, // setTarget + null, // cb, + method == null ? null : () -> method, + null, // tb + interceptorBindingsBootstrap); + } + + /** + * Returns an {@link InterceptionFunction} whose {@link InterceptionFunction#apply(Object...) apply(Object...)} method + * will invoke all supplied {@link InterceptorMethod}s in encounter order before invoking the supplied {@link + * BiFunction}'s {@link BiFunction#apply(Object, Object) apply(Object Object[])} method with the return value of + * {@link InvocationContext#getTarget()}, and with the return value of {@link InvocationContext#getParameters()}. + * + * @param interceptorMethods the {@link InterceptorMethod}s to invoke; may be {@code null} (rather uselessly) + * + * @param terminalBiFunction a {@link BiFunction} encapsulating the invocation to be intercepted that takes the return + * value of {@link InvocationContext#getTarget()} as its first argument, and the return value of {@link + * InvocationContext#getParameters()} as its second argument; may be {@code null} (rather uselessly) + * + * @param targetBootstrap a {@link Supplier} that will be called for the initial value to be returned by the first + * invocation of {@link InvocationContext#getTarget()}; may be {@code null} in which case the value too will be {@code + * null} + * + * @return an {@link InterceptionFunction}; never {@code null} + */ + // Around-invoke or similar. + public static final InterceptionFunction ofInvocation(final Collection interceptorMethods, + final BiFunction terminalBiFunction, + final Supplier targetBootstrap) { + return ofInvocation(interceptorMethods, terminalBiFunction, targetBootstrap, Set::of); + } + + /** + * Returns an {@link InterceptionFunction} whose {@link InterceptionFunction#apply(Object...) apply(Object...)} method + * will invoke all supplied {@link InterceptorMethod}s in encounter order before invoking the supplied {@link + * BiFunction}'s {@link BiFunction#apply(Object, Object) apply(Object Object[])} method with the return value of + * {@link InvocationContext#getTarget()}, and with the return value of {@link InvocationContext#getParameters()}. + * + * @param interceptorMethods the {@link InterceptorMethod}s to invoke; may be {@code null} (rather uselessly) + * + * @param terminalBiFunction a {@link BiFunction} encapsulating the invocation to be intercepted that takes the return + * value of {@link InvocationContext#getTarget()} as its first argument, and the return value of {@link + * InvocationContext#getParameters()} as its second argument; may be {@code null} (rather uselessly) + * + * @param targetBootstrap a {@link Supplier} that will be called for the initial value to be returned by the first + * invocation of {@link InvocationContext#getTarget()}; may be {@code null} in which case the value too will be {@code + * null} + * + * @param interceptorBindingsBootstrap a {@link Supplier} of a {@link Set} of {@link Annotation}s that will be called + * for the value to be returned by the first invocation of {@link InvocationContext#getInterceptorBindings()}; may be + * {@code null} in which case the value will be an {@linkplain Set#of() empty, immutable Set} + * + * @return an {@link InterceptionFunction}; never {@code null} + */ + // Around-invoke or similar. + public static final InterceptionFunction ofInvocation(final Collection interceptorMethods, + final BiFunction terminalBiFunction, + final Supplier targetBootstrap, + final Supplier> interceptorBindingsBootstrap) { + return + ff(interceptorMethods, + terminalBiFunction, + targetBootstrap, + null, // argumentsValidator + false, // setTarget + null, // cb, + null, // mb, + null, // tb, + interceptorBindingsBootstrap); + } + + // ff for function factory :-) + private static InterceptionFunction ff(final Collection interceptorMethods, + final BiFunction tbf, + final Supplier targetBootstrap, + final Consumer argumentsValidator, + final boolean setTarget, + final Supplier> cb, + final Supplier mb, + final Supplier tb, + final Supplier> interceptorBindingsBootstrap) { + final Interceptions i; + final List ims; + if (tbf == null) { + if (interceptorMethods == null || interceptorMethods.isEmpty()) { + return Interceptions::returnNull; + } + ims = List.copyOf(interceptorMethods); + // We can get away with hoisting this Property out of the function scope because we know it will never change. + final Property target = new Property(targetBootstrap, false); + i = new Interceptions(cb, mb, tb, interceptorBindingsBootstrap); + return a -> + i.new State(target) + .new Context(ims.iterator()) + .proceed(); + } else if (interceptorMethods == null || interceptorMethods.isEmpty()) { + final Property target = new Property(targetBootstrap, false); + return argumentsValidator == null ? Interceptions::returnNull : a -> { + argumentsValidator.accept(a); + return tbf.apply(target.get(), a); + }; + } else { + ims = List.copyOf(interceptorMethods); + i = new Interceptions(cb, mb, tb, interceptorBindingsBootstrap); + if (setTarget) { + return a -> { + final State s = i.new State(new Property<>(targetBootstrap, true), + new Property<>(a == null ? Interceptions::emptyObjectArray : () -> a, + argumentsValidator, + true)); + final State.Context c = s + .new Context(ims.iterator(), + (t, a2) -> { + s.target(tbf.apply(t, a2)); + return s.target(); + }); + final Object v = c.proceed(); + final Object t = c.getTarget(); + if (v != t && LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "around-construct proceed() return value: " + v + "; returning getTarget() return value: " + t); + } + return t; + }; + } + // We can get away with hoisting this Property out of the function scope because we know it will never change. + final Property target = new Property(targetBootstrap, false); + return a -> { + return i.new State(target, new Property(a == null ? Interceptions::emptyObjectArray : () -> a, + argumentsValidator, + true)) + .new Context(ims.iterator(), tbf) + .proceed(); + }; + } + } + + /** + * Creates and returns a {@link BiFunction} encapsulating the supplied {@link Constructor}. + * + * @param c a {@link Constructor}; must not be {@code null} + * + * @return a {@link BiFunction} encapsulating the supplied {@link Constructor}; never {@code null} + * + * @exception NullPointerException if {@code c} is {@code null} + * + * @exception IllegalAccessException if {@linkplain Lookup#unreflectConstructor(Constructor) unreflecting} fails + */ + public static final BiFunction terminalBiFunctionOf(final Constructor c) throws IllegalAccessException { + return terminalBiFunctionOf(privateLookupIn(c.getDeclaringClass(), lookup).unreflectConstructor(c)); + } + + /** + * Creates and returns a {@link BiFunction} encapsulating the supplied {@link Method}. + * + * @param m a {@link Method}; must not be {@code null} + * + * @return a {@link BiFunction} encapsulating the supplied {@link Method}; never {@code null} + * + * @exception NullPointerException if {@code m} is {@code null} + * + * @exception IllegalAccessException if {@linkplain Lookup#unreflect(Method) unreflecting} fails + */ + public static final BiFunction terminalBiFunctionOf(final Method m) throws IllegalAccessException { + return terminalBiFunctionOf(privateLookupIn(m.getDeclaringClass(), lookup).unreflect(m)); + } + + /** + * Creates and returns a {@link BiFunction} encapsulating the supplied {@link MethodHandle}. + * + * @param mh a {@link MethodHandle}; must not be {@code null} + * + * @return a {@link BiFunction} encapsulating the supplied {@link MethodHandle}; never {@code null} + * + * @exception NullPointerException if {@code mh} is {@code null} + */ + public static final BiFunction terminalBiFunctionOf(MethodHandle mh) { + mh = mh.asType(mh.type().changeReturnType(Object.class)); + final MethodType mt = mh.type(); + final int pc = mt.parameterCount(); + final MethodHandle terminalBiFunction; + if (pc == 0) { + terminalBiFunction = mh; + return (t, a) -> { + try { + return terminalBiFunction.invokeExact(); + } catch (final RuntimeException | Error e) { + throw e; + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e.getMessage(), e); + } catch (final Throwable e) { + throw new IllegalStateException(e.getMessage(), e); + } + }; + } else if (pc == 1) { + if (mt.parameterType(0) == Object[].class) { + terminalBiFunction = mh; // no need to spread + return (t, a) -> { + try { + return terminalBiFunction.invokeExact(a); + } catch (final RuntimeException | Error e) { + throw e; + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e.getMessage(), e); + } catch (final Throwable e) { + throw new IllegalStateException(e.getMessage(), e); + } + }; + } + terminalBiFunction = mh.asType(mt.changeParameterType(0, Object.class)); + return (t, a) -> { + try { + return terminalBiFunction.invokeExact(t); + } catch (final RuntimeException | Error e) { + throw e; + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e.getMessage(), e); + } catch (final Throwable e) { + throw new IllegalStateException(e.getMessage(), e); + } + }; + } + terminalBiFunction = mh.asType((mt.changeParameterType(0, Object.class))).asSpreader(Object[].class, pc - 1); + return (t, a) -> { + try { + return terminalBiFunction.invokeExact(t, a); + } catch (final RuntimeException | Error e) { + throw e; + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e.getMessage(), e); + } catch (final Throwable e) { + throw new IllegalStateException(e.getMessage(), e); + } + }; + } + + /** + * A convenience method that ensures that every element of the supplied {@code arguments} array can be assigned to a + * reference bearing the corresponding {@link Class} drawn from the supplied {@code parameterTypes} array. + * + *

Boxing, unboxing and widening conversions are taken into consideration.

+ * + *

This method implements the logic implied, but nowhere actually specified, by the contract of {@link + * InvocationContext#setParameters(Object[])}.

+ * + * @param parameterTypes an array of {@link Class} instances; may be {@code null}; must not contain {@code null} + * elements or {@code void.class}; must have a length equal to that of the supplied {@code arguments} array + * + * @param arguments an array of {@link Object}s; may be {@code null}; must have a length equal to that of the supplied + * {@code parameterTypes} array + * + * @exception IllegalArgumentException if validation fails, i.e. if the length of {@code parameterTypes} is not equal + * to the length of {@code arguments}, or if an element of {@code parameterTypes} is {@code null} or {@code + * void.class}, or if not every element of the supplied {@code arguments} array can be assigned to a reference bearing + * the corresponding {@link Class} drawn from the supplied {@code parameterTypes} array + */ + public static final void validate(final Class[] parameterTypes, final Object[] arguments) { + final int parameterTypesLength = parameterTypes == null ? 0 : parameterTypes.length; + final int argumentsLength = arguments == null ? 0 : arguments.length; + if (argumentsLength != parameterTypesLength) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments)); + } + for (int i = 0; i < argumentsLength; i++) { + final Class parameterType = parameterTypes[i]; + if (parameterType == null || parameterType == void.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: " + parameterType); + } + final Object argument = arguments[i]; + if (argument == null) { + if (parameterType.isPrimitive() || parameterType != Void.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: " + parameterType.getName() + + "; argument: null"); + } + } else { + final Class argumentType = argument.getClass(); + if (parameterType != argumentType) { + if (parameterType == boolean.class) { + if (argumentType != Boolean.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: boolean" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == Boolean.class) { + if (argumentType != boolean.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: java.lang.Boolean" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == byte.class) { + if (argumentType != Byte.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: byte" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == Byte.class) { + if (argumentType != byte.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: java.lang.Byte" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == char.class) { + if (argumentType != Character.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: char" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == Character.class) { + if (argumentType != char.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: java.lang.Character" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == double.class) { + if (argumentType != byte.class && + argumentType != char.class && + argumentType != Double.class && + argumentType != float.class && + argumentType != int.class && + argumentType != long.class && + argumentType != short.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: double" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == Double.class) { + if (argumentType != double.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: java.lang.Double" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == float.class) { + if (argumentType != byte.class && + argumentType != char.class && + argumentType != Float.class && + argumentType != int.class && + argumentType != long.class && + argumentType != short.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: float" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == Float.class) { + if (argumentType != float.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: java.lang.Float" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == int.class) { + if (argumentType != byte.class && + argumentType != char.class && + argumentType != Integer.class && + argumentType != short.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: int; argument type: " + argumentType.getName()); + } + } else if (parameterType == Integer.class) { + if (argumentType != int.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: java.lang.Integer" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == long.class) { + if (argumentType != byte.class && + argumentType != char.class && + argumentType != int.class && + argumentType != Long.class && + argumentType != short.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: long" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == Long.class) { + if (argumentType != long.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: java.lang.Long" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == short.class) { + if (argumentType != byte.class && + argumentType != Short.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: byte" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == Short.class) { + if (argumentType != short.class) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: java.lang.Short" + + "; argument type: " + argumentType.getName()); + } + } else if (parameterType == Void.class || !parameterType.isAssignableFrom(argumentType)) { + throw new IllegalArgumentException("parameter types: " + Arrays.toString(parameterTypes) + + "; arguments: " + Arrays.toString(arguments) + + "; parameter type: " + parameterType.getName() + + "; argument type: " + argumentType.getName()); + } + } + } + } + } + + private static final Object[] emptyObjectArray() { + return EMPTY_OBJECT_ARRAY; + } + + private static final T returnNull() { + return null; + } + + private static final T returnNull(final U ignored) { + return null; + } + + private static final void throwIllegalStateException(final T ignored) { + throw new IllegalStateException(); + + } + + + /* + * Inner and nested classes. + */ + + + private final class State { + + + /* + * Instance fields. + */ + + + private final Property target; + + private final Property arguments; + + + /* + * Constructors. + */ + + + private State(final Property target) { + this(target, null); + } + + private State(final Property target, + final Property arguments) { + super(); + this.target = Objects.requireNonNull(target, "target"); + this.arguments = arguments == null ? EMPTY_OBJECT_ARRAY_PROPERTY : arguments; + } + + + /* + * Instance methods. + */ + + + private final Object target() { + return this.target.get(); + } + + private final void target(final Object target) { + this.target.accept(target); + } + + private final Object[] arguments() { + final Object[] a = this.arguments.get(); + return a == null ? EMPTY_OBJECT_ARRAY : a.clone(); + } + + private final void arguments(final Object[] a) { + this.arguments.accept(a == null ? EMPTY_OBJECT_ARRAY : a.clone()); + } + + + /* + * Inner and nested classes. + */ + + + private final class Context implements InvocationContext { + + + /* + * Instance fields. + */ + + + private final Supplier proceeder; + + + /* + * Constructors. + */ + + + private Context(final Iterator iterator) { + this(iterator, (Supplier)Interceptions::returnNull); + } + + private Context(final Iterator iterator, + final BiFunction tbf) { + this(iterator, () -> tbf.apply(target(), arguments())); + } + + private Context(final Iterator iterator, + final Supplier f) { + super(); + if (iterator == null || !iterator.hasNext()) { + this.proceeder = f == null ? Interceptions::returnNull : f; + } else { + final InterceptorMethod im = Objects.requireNonNull(iterator.next()); + final Context c = new Context(iterator, f); + this.proceeder = () -> { + try { + return im.intercept(c); + } catch (final RuntimeException e) { + throw e; + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new InterceptorException(e.getMessage(), e); + } catch (final Exception e) { + throw new InterceptorException(e.getMessage(), e); + } + }; + } + } + + + /* + * Instance methods. + */ + + + @Override // InvocationContext + public final Constructor getConstructor() { + return constructorBootstrap.get(); + } + + @Override // InvocationContext + public final Map getContextData() { + return data; + } + + @Override // InvocationContext + public final Set getInterceptorBindings() { + return interceptorBindingsBootstrap.get(); + } + + @Override // InvocationContext + public final Method getMethod() { + return methodBootstrap.get(); + } + + @Override // InvocationContext + public final Object[] getParameters() { + return arguments(); + } + + @Override // InvocationContext + public final Object getTarget() { + return target(); + } + + @Override // InvocationContext + public final Object getTimer() { + return timerBootstrap.get(); + } + + @Override // InvocationContext + public final Object proceed() { + return this.proceeder.get(); + } + + @Override // InvocationContext + public final void setParameters(final Object[] arguments) { + arguments(arguments); + } + + } + + } + + private static final class Property implements Consumer, Supplier { + + + /* + * Static fields. + */ + + + private static final VarHandle VALUE; + + static { + try { + // Bizarrely, we cannot use the lookup static field belonging to the Interceptions class, at least in JDK + // 11 (NullPointerException (!)). Compiler bug? + VALUE = lookup().findVarHandle(Property.class, "value", Optional.class); + } catch (final IllegalAccessException | NoSuchFieldException e) { + throw (ExceptionInInitializerError)new ExceptionInInitializerError(e.getMessage()).initCause(e); + } + } + + + /* + * Instance fields. + */ + + + // set only through/using VALUE + private volatile Optional value; + + private final Supplier reader; + + private final Consumer writer; + + + /* + * Constructors. + */ + + + private Property(final Supplier bootstrap, final boolean mutable) { + this(bootstrap, null, mutable); + } + + private Property(final Supplier bootstrap, final Consumer validator, final boolean mutable) { + super(); + if (bootstrap == null) { + if (mutable) { + VALUE.set(this, Optional.empty()); // no need for volatile semantics here + this.reader = () -> ((Optional)VALUE.getVolatile(this)).orElse(null); + if (validator == null) { + this.writer = v -> { + VALUE.setVolatile(this, Optional.ofNullable(v)); + }; + } else { + validator.accept(null); // make sure the Optional.empty() assignment above was OK + this.writer = v -> { + validator.accept(v); + VALUE.setVolatile(this, Optional.ofNullable(v)); + }; + } + } else { + this.reader = Interceptions::returnNull; + this.writer = Interceptions::throwIllegalStateException; + } + } else if (mutable) { + if (validator == null) { + this.reader = () -> { + Optional o = (Optional)VALUE.getVolatile(this); + if (o == null) { + o = Optional.ofNullable(bootstrap.get()); + if (!VALUE.compareAndSet(this, null, o)) { + o = (Optional)VALUE.getVolatile(this); + } + } + return o.orElse(null); + }; + this.writer = v -> { + VALUE.setVolatile(this, Optional.ofNullable(v)); + }; + } else { + this.reader = () -> { + Optional o = (Optional)VALUE.getVolatile(this); + if (o == null) { + final T v = bootstrap.get(); + validator.accept(v); + o = Optional.ofNullable(v); + if (!VALUE.compareAndSet(this, null, o)) { + o = (Optional)VALUE.getVolatile(this); + } + } + return o.orElse(null); + }; + this.writer = v -> { + validator.accept(v); + VALUE.setVolatile(this, Optional.ofNullable(v)); + }; + } + } else { // !mutable + this.writer = Interceptions::throwIllegalStateException; + if (validator == null) { + this.reader = () -> { + Optional o = (Optional)VALUE.getVolatile(this); + if (o == null) { + o = Optional.ofNullable(bootstrap.get()); + if (!VALUE.compareAndSet(this, null, o)) { + o = (Optional)VALUE.getVolatile(this); + } + } + return o.orElse(null); + }; + } else { + this.reader = () -> { + Optional o = (Optional)VALUE.getVolatile(this); + if (o == null) { + final T v = bootstrap.get(); + validator.accept(v); + o = Optional.ofNullable(v); + if (!VALUE.compareAndSet(this, null, o)) { + o = (Optional)VALUE.getVolatile(this); + } + } + return o.orElse(null); + }; + } + } + } + + + /* + * Instance methods. + */ + + + @Override // Supplier + public final T get() { + return this.reader.get(); + } + + @Override // Consumer + public final void accept(final T value) { + this.writer.accept(value); + } + + @Override + public final String toString() { + return String.valueOf(this.get()); + } + + } + +} diff --git a/src/test/java/org/microbean/interceptor/TestChain.java b/src/test/java/org/microbean/interceptor/TestChain.java index e660506..6eabc70 100644 --- a/src/test/java/org/microbean/interceptor/TestChain.java +++ b/src/test/java/org/microbean/interceptor/TestChain.java @@ -39,6 +39,7 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +@Deprecated(forRemoval = true) final class TestChain { private static final Lookup lookup = lookup(); diff --git a/src/test/java/org/microbean/interceptor/TestInterception.java b/src/test/java/org/microbean/interceptor/TestInterceptions.java similarity index 82% rename from src/test/java/org/microbean/interceptor/TestInterception.java rename to src/test/java/org/microbean/interceptor/TestInterceptions.java index 0498a5d..c42f082 100644 --- a/src/test/java/org/microbean/interceptor/TestInterception.java +++ b/src/test/java/org/microbean/interceptor/TestInterceptions.java @@ -22,9 +22,12 @@ import java.lang.reflect.Method; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + import jakarta.interceptor.InvocationContext; import org.junit.jupiter.api.BeforeEach; @@ -40,7 +43,10 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; -final class TestInterception { +import static org.microbean.interceptor.Interceptions.ofConstruction; +import static org.microbean.interceptor.Interceptions.ofInvocation; + +final class TestInterceptions { private static final Lookup lookup = lookup(); @@ -52,7 +58,7 @@ final class TestInterception { private static boolean invoke; - private TestInterception() { + private TestInterceptions() { super(); construct = true; } @@ -106,21 +112,21 @@ final void testVoidAroundConstruct() throws Exception { final List ims = List.of(InterceptorMethod.of(this.getClass().getDeclaredMethod("voidAroundConstruct", InvocationContext.class), this::returnThis)); - final Interception interception = new Interception(ims, this.getClass().getDeclaredConstructor()); - final TestInterception t = (TestInterception)interception.call(); + final InterceptionFunction f = ofConstruction(ims, this.getClass().getDeclaredConstructor(), Set::of); + final TestInterceptions t = (TestInterceptions)f.apply((Object[])null); // null arguments assertNotNull(t); assertNotSame(this, t); - final TestInterception newT = (TestInterception)interception.call(); + final TestInterceptions newT = (TestInterceptions)f.apply((Object[])null); // null arguments assertNotNull(newT); assertNotSame(t, newT); assertNotSame(this, newT); - + assertTrue(aroundConstruct); assertTrue(construct); assertFalse(aroundInvoke); assertFalse(invoke); } - + @Test final void testMethodHandleStuff() throws Throwable { final Method m = Frobnicator.class.getDeclaredMethod("frobnicate"); @@ -136,8 +142,8 @@ final void testAroundInvokeOnFrobnicate() throws Exception { final List ims = List.of(InterceptorMethod.of(this.getClass().getDeclaredMethod("aroundInvoke", InvocationContext.class), this::returnThis)); - final Interception interception = new Interception(ims, Frobnicator.class.getDeclaredMethod("frobnicate"), Frobnicator::new); - assertNull(interception.call()); + final InterceptionFunction f = ofInvocation(ims, Frobnicator.class.getDeclaredMethod("frobnicate"), Frobnicator::new, Set::of); + assertNull(f.apply((Object[])null)); assertTrue(aroundInvoke); assertTrue(invoke); } @@ -145,8 +151,8 @@ final void testAroundInvokeOnFrobnicate() throws Exception { @Test final void testUninterceptedAdd() throws Exception { final Method add = Frobnicator.class.getDeclaredMethod("add", int.class, int.class); - final Interception interception = new Interception(List.of(), add, Frobnicator::new, () -> new Object[] { 1, 2 }); - assertEquals(Integer.valueOf(3), interception.call()); + final InterceptionFunction f = ofInvocation(List.of(), add, Frobnicator::new, Set::of); + assertEquals(Integer.valueOf(3), f.apply(new Object[] { 1, 2 })); assertFalse(construct); assertFalse(aroundConstruct); assertTrue(invoke); @@ -159,8 +165,8 @@ final void testAroundInvokeOnAdd() throws Exception { final List ims = List.of(InterceptorMethod.of(this.getClass().getDeclaredMethod("aroundInvoke", InvocationContext.class), this::returnThis)); - final Interception interception = new Interception(ims, add, Frobnicator::new, () -> new Object[] { 1, 2 }); - assertEquals(Integer.valueOf(3), interception.call()); + final InterceptionFunction f = ofInvocation(ims, add, Frobnicator::new, Set::of); + assertEquals(Integer.valueOf(3), f.apply(new Object[] { 1, 2 })); assertTrue(aroundInvoke); assertTrue(invoke); } @@ -171,8 +177,8 @@ final void testAroundInvokeOnRuminate() throws Exception { final List ims = List.of(InterceptorMethod.of(this.getClass().getDeclaredMethod("aroundInvoke", InvocationContext.class), this::returnThis)); - final Interception interception = new Interception(ims, ruminate, Frobnicator::new, () -> new Object[] { 1, 2 }); - assertNull(interception.call()); + final InterceptionFunction interception = ofInvocation(ims, ruminate, Frobnicator::new, Set::of); + assertNull(interception.apply(new Object[] { 1, 2 })); assertTrue(aroundInvoke); assertTrue(invoke); } @@ -183,8 +189,8 @@ final void testAroundInvokeOnVoidCaturgiate() throws Exception { final List ims = List.of(InterceptorMethod.of(this.getClass().getDeclaredMethod("aroundInvoke", InvocationContext.class), this::returnThis)); - final Interception interception = new Interception(ims, voidCaturgiate, Frobnicator::new, () -> new Object[] { new Object[] { 1, 2 } }); - assertNull(interception.call()); + final InterceptionFunction interception = ofInvocation(ims, voidCaturgiate, Frobnicator::new, Set::of); + assertNull(interception.apply(new Object[] { new Object[] { 1, 2 } })); assertTrue(aroundInvoke); assertTrue(invoke); } @@ -196,13 +202,13 @@ final void testAroundInvokeOnCaturgiate() throws Exception { final List ims = List.of(InterceptorMethod.of(this.getClass().getDeclaredMethod("aroundInvoke", InvocationContext.class), this::returnThis)); - final Interception interception = new Interception(ims, caturgiate, Frobnicator::new, () -> new Object[] { new Object[] { 1, 2 } }); - assertEquals(Integer.valueOf(3), interception.call()); + final InterceptionFunction interception = ofInvocation(ims, caturgiate, Frobnicator::new, Set::of); + assertEquals(Integer.valueOf(3), interception.apply(new Object[] { new Object[] { 1, 2 } })); assertTrue(aroundInvoke); assertTrue(invoke); } - private final TestInterception returnThis() { + private final TestInterceptions returnThis() { return this; } diff --git a/src/test/java/org/microbean/interceptor/TestWithLifecycles.java b/src/test/java/org/microbean/interceptor/TestWithLifecycles.java deleted file mode 100644 index d797c94..0000000 --- a/src/test/java/org/microbean/interceptor/TestWithLifecycles.java +++ /dev/null @@ -1,63 +0,0 @@ -/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- - * - * Copyright © 2022–2023 microBean™. - * - * 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 org.microbean.interceptor; - -import java.util.List; - -import java.util.concurrent.atomic.AtomicReference; - -import java.util.function.Supplier; - -import jakarta.interceptor.InvocationContext; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -final class TestWithLifecycles { - - private TestWithLifecycles() { - super(); - } - - @Test - final void testTargetClassInterceptorMethod() throws Exception { - final Target target = new Target(); - final Supplier targetSupplier = () -> target; - final InterceptorMethod im = InterceptorMethod.of(Target.class.getMethod("aroundInvokeMethod", InvocationContext.class), targetSupplier); - final Chain chain = new Chain(List.of(im), targetSupplier, Target.class.getMethod("businessMethod"), null /* no parameters */); - assertTrue(chain.getTarget() instanceof Target); - assertNull(chain.call()); // null because businessMethod() returns void - } - - private static final class Target { - - private Target() { - super(); - } - - public Object aroundInvokeMethod(final InvocationContext ic) throws Exception { - System.out.println("Around invoke method"); - return ic.proceed(); - } - - public void businessMethod() { - System.out.println("Business method"); - } - - } - -}