diff --git a/api/src/main/java/jakarta/enterprise/invoke/Invoker.java b/api/src/main/java/jakarta/enterprise/invoke/Invoker.java index f3fca807..f6d5d2e5 100644 --- a/api/src/main/java/jakarta/enterprise/invoke/Invoker.java +++ b/api/src/main/java/jakarta/enterprise/invoke/Invoker.java @@ -90,6 +90,9 @@ public interface Invoker { * * TODO the previous 2 paragraphs refer to "assignability", which needs to be defined somewhere! * + * TODO when the `InvokerBuilder` applies transformations, some of the requirements above + * are no longer strictly necessary, should reflect that in the text somehow + * * @param instance the target instance on which the method is to be invoked, may only be {@code null} * if the invokable method is {@code static} * @param arguments arguments to be supplied to the method, may only be {@code null} diff --git a/api/src/main/java/jakarta/enterprise/invoke/InvokerBuilder.java b/api/src/main/java/jakarta/enterprise/invoke/InvokerBuilder.java index 8bdca2cb..31f57d78 100644 --- a/api/src/main/java/jakarta/enterprise/invoke/InvokerBuilder.java +++ b/api/src/main/java/jakarta/enterprise/invoke/InvokerBuilder.java @@ -11,16 +11,350 @@ package jakarta.enterprise.invoke; /** - * Builder of {@link Invoker}s. + * Builder of {@link Invoker}s that allows configuring input lookups, input and + * output transformations, and invoker wrapping. If a lookup is configured, + * the corresponding input of the invoker is ignored and an instance is looked up + * from the CDI container before the invokable method is invoked. If a transformation + * is configured, the corresponding input or output of the invoker is modified + * in certain way before or after the invokable method is invoked. If a wrapper + * is configured, the invoker is passed to custom code for getting invoked. + * As a result, the built {@code Invoker} instance may have more complex behavior + * than just directly calling the invokable method. + *

+ * Transformations and wrapping are expressed by ordinary methods that must have + * a pre-defined signature, as described below. Such methods are called + * transformers and wrappers. + *

+ * Invokers may only be built during deployment. It is not possible to build new + * invokers at application runtime. + * + *

Example

+ * + * Before describing in detail how lookups, transformers and wrappers work, + * let's take a look at an example. Say we have the following invokable method: + * + *
+ * class MyService {
+ *     @Invokable
+ *     String hello(String name) {
+ *         return "Hello " + name + "!";
+ *     }
+ * }
+ * 
+ * + * And we want to build an invoker that looks up {@code MyService} from the CDI + * container, always passes the argument to {@code hello()} as all upper-case, + * and repeats the return value twice. To transform the argument, we can use + * the zero-parameter method {@code String.toUpperCase()} (which uses the default + * locale, so probably not the best thing to do in real code, but enough for this + * example), and to transform the return value, we write a transformer as a simple + * {@code static} method: + * + *
+ * class Transformations {
+ *     static String repeatTwice(String str) {
+ *         return str + " " + str;
+ *     }
+ * }
+ * 
+ * + * Then, assuming we have obtained the {@code InvokerBuilder} for {@code MyService.hello()}, + * we can set up the lookup and transformations and build an invoker like so: + * + *
+ * builder.setInstanceLookup()
+ *        .setArgumentTransformer(0, String.class, "toUpperCase")
+ *        .setReturnValueTransformer(Transformations.class, "repeatTwice")
+ *        .build();
+ * 
+ * + * The resulting invoker will be equivalent to the following class: + *
+ * class TheInvoker implements Invoker<MyService, String> {
+ *     String invoke(MyService ignored, Object[] arguments) {
+ *         MyService instance = CDI.current().select(MyService.class).get();
+ *         String argument = (String) arguments[0];
+ *         String transformedArgument = argument.toUpperCase();
+ *         String result = instance.hello(transformedArgument);
+ *         String transformedResult = Transformations.repeatTwice(result);
+ *         return transformedResult;
+ *     }
+ * }
+ * 
+ * + * The caller of this invoker may pass {@code null} as the target instance, because + * the invoker will lookup the target instance on its own. Therefore, calling + * {@code invoker.invoke(null, new Object[] {"world"})} will return + * {@code "Hello WORLD! Hello WORLD!"}. + * + *

General requirements

+ * + * To refer to a transformer or a wrapper, all methods in this builder accept: + * 1. the {@code Class} that that declares the method, and 2. the {@code String} name + * of the method. + *

+ * Transformers may be {@code static}, in which case they must be declared directly + * on the given class, or they may be instance methods, in which case they may be declared + * on the given class or inherited from any of its supertypes. + *

+ * It is possible to register only one transformer of each kind, or for each argument + * position in case of argument transformers. Attempting to register a second transformer + * of the same kind, or for the same argument position, leads to an exception. + *

+ * Wrappers must be {@code static} and must be declared directly on the given class. + * It is possible to register only one wrapper. Attempting to register a second wrapper + * leads to an exception. + *

+ * It is a deployment problem if no method with given name and valid signature is found, + * or if multiple methods with given name and different valid signatures are found. It is + * a deployment problem if a registered transformer or wrapper is not visible in the package + * that contains the class which declares the invokable method. + *

+ * Transformers and wrappers may include the {@code throws} clause. The declared exception + * types are ignored when searching for the method. + * + *

Input lookups

+ * + * For the target instance and for each argument, it is possible to specify that the instance + * passed to {@code Invoker.invoke()} should be ignored and an instance should be looked up + * from the CDI container instead. + *

+ * For the target instance, a CDI lookup is performed with type equal to the bean class + * of the bean to which the invokable method belongs, and qualifiers equal to the set of + * all qualifier annotations present on the bean class of the bean to which the invokable + * method belongs. + *

+ * For an argument, a CDI lookup is performed with type equal to the type of the corresponding + * parameter of the invokable method, and qualifiers equal to the set of all qualifier annotations + * present on the corresponding parameter of the invokable method. + *

+ * Implementations are required to resolve all lookups during deployment. It is a deployment + * problem if the lookup ends up unresolved or ambiguous. + *

+ * If the looked up bean is {@code @Dependent}, it is guaranteed that the instance will be + * destroyed after the invokable method is invoked but before the the invoker returns. + * The order in which the looked up {@code @Dependent} beans are destroyed is not specified. + * + *

Input transformations

+ * + * An invokable method has 2 kinds of inputs: the target instance (unless the invokable + * method is {@code static}, in which case the target instance is ignored and should be + * {@code null} by convention) and arguments. These inputs correspond to the parameters + * of {@link Invoker#invoke(Object, Object[]) Invoker.invoke()}. + *

+ * Each input can be transformed by a transformer that has one of the following signatures, + * where {@code X} and {@code Y} are types: + * + *

+ * + * An input transformer must produce a type that can be consumed by the invokable method. + * Specifically: when {@code X} is any-type, it is not type checked during deployment (where + * any-type is recursively defined as: the {@code java.lang.Object} class type, or a type + * variable that has no bound, or a type variable whose first bound is any-type). Otherwise, + * it is a deployment problem if {@code X} is not assignable to the corresponding type in + * the declaration of the invokable method (that is the bean class in case of target instance + * transformers, or the corresponding parameter type in case of argument transformers). + * {@code Y} is not type checked during deployment, so that input transformers may consume + * arbitrary types. + * TODO this paragraph refers to "assignability", which needs to be defined somewhere! + *

+ * When a transformer is registered for given input, it is called before the invokable + * method is invoked, and the outcome of the transformer is used in the invocation + * instead of the original value passed to the invoker by its caller. + *

+ * If the transformer declares the {@code Consumer} parameter, and the execution + * of the transformer calls {@code Consumer.accept()} with some {@code Runnable}, it is + * guaranteed that the {@code Runnable} will be called after the invokable method is invoked + * but before the invoker returns. These {@code Runnable}s are called cleanup tasks. + * The order of cleanup task execution is not specified. Passing a {@code null} cleanup task + * to the {@code Consumer} is permitted, but has no effect. + *

+ * If an input transformation is configured for an input for which a lookup is also configured, + * the lookup is performed first and the transformation is applied to the looked up value. + * If the looked up bean for some input is {@code @Dependent}, it is guaranteed that all + * cleanup tasks registered by a transformer for that input are called before that looked up + * {@code @Dependent} bean is destroyed. + * + *

Output transformations

+ * + * An invokable method has 2 kinds of outputs: the return value and the thrown exception. + * These outputs correspond to the return value of {@link Invoker#invoke(Object, Object[]) Invoker.invoke()} + * or its thrown exception, respectively. + *

+ * Each output can be transformed by a transformer that has one of the following signatures, + * where {@code X} and {@code Y} are types: + * + *

+ * + * An output transformer must consume a type that can be produced by the invokable method. + * Specifically: when {@code Y} is any-type, it is not type checked during deployment (where + * any-type is recursively defined as: the {@code java.lang.Object} class type, or a type + * variable that has no bound, or a type variable whose first bound is any-type). Otherwise, + * it is a deployment problem if {@code Y} is not assignable from the return type of the invokable + * method in case of return value transformers, or from {@code java.lang.Throwable} in case + * of exception transformers. {@code X} is not type checked during deployment, so that output + * transformers may produce arbitrary types. + * TODO this paragraph refers to "assignability", which needs to be defined somewhere! + *

+ * When a transformer is registered for given output, it is called after the invokable + * method is invoked, and the outcome of the transformer is passed back to the caller of + * the invoker instead of the original output produced by the invokable method. + *

+ * If the invokable method returns normally, any registered exception transformer is ignored; + * only the return value transformer is called. The return value transformer may throw, in which + * case the invoker will rethrow the exception. If the invoker is supposed to return normally, + * the return value transformer must return normally. + *

+ * Conversely, if the invokable method throws, any registered return value transformer is ignored; + * only the exception transformer is called. The exception transformer may return normally, + * in which case the invoker will return the return value of the exception transformer. + * If the invoker is supposed to throw an exception, the exception transformer must throw. + * TODO this requires that implementations catch java.lang.Throwable, which is perhaps a bit too much? + * maybe stick with java.lang.Exception? + * + *

Invoker wrapping

+ * + * An invoker, possibly utilizing input lookups and input/output transformations, may be wrapped + * by a custom piece of code for maximum flexibility. A wrapper must have the following signature, + * where {@code X}, {@code Y} and {@code Z} are types: + * + * + * + * A wrapper must operate on a matching instance type. Specifically: when {@code X} is any-type, + * it is not type checked during deployment (where any-type is recursively defined as: + * the {@code java.lang.Object} class type, or a type variable that has no bound, or a type + * variable whose first bound is any-type). Otherwise, it is a deployment problem if {@code X} + * is not assignable from the class type of the bean class to which the invokable method + * belongs. {@code Y} and {@code Z} are not type checked during deployment. + *

+ * When a wrapper is registered, 2 invokers for the same invokable method are created. The inner + * invoker applies all lookups and transformations, as described in previous sections, and + * invokes the invokable method. The outer invoker calls the wrapper with the passed instance + * and arguments and an instance of the inner invoker. The wrapper is supposed to call the invoker + * it is passed, but does not necessarily have to. The wrapper may call the invoker multiple times. + *

+ * In other words, the outer invoker is equivalent to the following class: + * + *

+ * class InvokerWrapper implements Invoker<X, Z> {
+ *     Z invoke(X instance, Object[] arguments) {
+ *         // obtain the invoker as if no wrapper existed
+ *         Invoker<X, Y> invoker = obtainInvoker();
+ *         return SomeClass.wrap(instance, arguments, invoker);
+ *     }
+ * }
+ * 
+ * + * If the wrapper returns normally, the outer invoker returns the return value, unless the wrapper + * is declared {@code void}, in which case the invoker returns {@code null}. If the wrapper throws + * an exception, the outer invoker rethrows it directly. + *

+ * This builder returns the outer invoker as described above. The inner invoker is an internal + * construct and the wrapper should not store it anywhere. + * + *

Type checking

+ * + * An invoker created by this builder has relaxed type checking rules, when compared to + * the description in {@link Invoker#invoke(Object, Object[]) Invoker.invoke()}, depending + * on applied lookups and transformers and possibly a wrapper. Some types may be checked + * during deployment, as described in previous sections. Other types are checked during + * invocation, at the very least due to the type checks performed implicitly by the JVM. + * The lookups, transformers and possibly the wrapper must arrange the inputs and outputs + * so that when the invokable method is eventually invoked, the rules described in + * {@link Invoker#invoke(Object, Object[]) Invoker.invoke()} all hold. * * @param type of outcome of this builder; always represents an {@code Invoker}, * but does not necessarily have to be an {@code Invoker} instance directly * @since 4.1 */ +// TODO more kinds of transformations could be defined, expecially for argument handling +// TODO it would be possible to specify a sequence of transformations for each input/output, instead of just one +// TODO we should probably build a prototype for this based on method handles public interface InvokerBuilder { + /** + * Enables lookup of the target instance. + * + * @return this builder + */ + InvokerBuilder setInstanceLookup(); + + /** + * Enables lookup of the argument on given {@code position}. + * + * @param position zero-based argument position for which lookup is enabled + * @return this builder + * @throws IllegalArgumentException if {@code position} is greather than or equal to + * the number of parameters declared by the invokable method + */ + InvokerBuilder setArgumentLookup(int position); + + /** + * Configures an input transformer for the target instance. + * + * @param clazz class that declares the transformer + * @param methodName transformer method name + * @return this builder + * @throws IllegalStateException if this method is called more than once + */ + InvokerBuilder setInstanceTransformer(Class clazz, String methodName); + + /** + * Configures an input transformer for the argument on given {@code position}. + * + * @param position zero-based argument position for which the input transformer is configured + * @param clazz class that declares the transformer + * @param methodName transformer method name + * @return this builder + * @throws IllegalArgumentException if {@code position} is greather than or equal to + * the number of parameters declared by the invokable method + * @throws IllegalStateException if this method is called more than once with the same {@code position} + */ + InvokerBuilder setArgumentTransformer(int position, Class clazz, String methodName); + + /** + * Configures an output transformer for the return value. + * + * @param clazz class that declares the transformer + * @param methodName transformer method name + * @return this builder + * @throws IllegalStateException if this method is called more than once + */ + InvokerBuilder setReturnValueTransformer(Class clazz, String methodName); + + /** + * Configures an output transformer for the thrown exception. + * + * @param clazz class that declares the transformer + * @param methodName transformer method name + * @return this builder + * @throws IllegalStateException if this method is called more than once + */ + InvokerBuilder setExceptionTransformer(Class clazz, String methodName); + + /** + * Configures an invoker wrapper. + * + * @param clazz class that declares the invoker wrapper + * @param methodName invoker wrapper method name + * @return this builder + * @throws IllegalStateException if this method is called more than once + */ + InvokerBuilder setInvocationWrapper(Class clazz, String methodName); + /** * Returns the built {@link Invoker} or some represention of it. Implementations are allowed - * but not required to reuse already built invokers. + * but not required to reuse already built invokers with the same configuration. * * @return the built invoker */