1 /*
2  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */

15
16 package software.amazon.awssdk.utils;
17
18 import java.lang.reflect.InvocationTargetException;
19 import java.lang.reflect.Method;
20 import software.amazon.awssdk.annotations.SdkProtectedApi;
21
22 /**
23  * This class acts as a proxy to invoke a specific method on objects of a specific class. It will use the JDK's
24  * reflection library to find and invoke the method.
25  * <p>
26  * The relatively expensive call to find the correct method on the class is lazy and will not be performed until the
27  * first invocation or until the invoker is explicitly initialized. Once found, the method is cached so repeated
28  * calls to initialize() or invoke() will not incur the reflection cost of searching for the method on the class.
29  * <p>
30  * Example:
31  * {@code
32  * ReflectionMethodInvoker<String, Integer> invoker =
33  *       new ReflectionMethodInvoker<String, Integer>(String.class, Integer.class"indexOf", String.classint.class);
34  * invoker.invoke("ababab""ab", 1);     // This is equivalent to calling "ababab".indexOf("ab", 1);
35  * }
36  * @param <T> The class type that has the method to be invoked.
37  * @param <R> The expected return type of the method invocation.
38  */

39 @SdkProtectedApi
40 public class ReflectionMethodInvoker<T, R> {
41     private final Class<T> clazz;
42     private final String methodName;
43     private final Class<R> returnType;
44     private final Class<?>[] parameterTypes;
45
46     private Method targetMethod;
47
48     /**
49      * Construct an instance of {@code ReflectionMethodInvoker}.
50      * <p>
51      * This constructor will not make any reflection calls as part of initialization; i.e. no validation of the
52      * existence of the given method signature will occur.
53      * @param clazz The class that has the method to be invoked.
54      * @param returnType The expected return class of the method invocation. The object returned by the invocation
55      *                   will be cast to this class.
56      * @param methodName The name of the method to invoke.
57      * @param parameterTypes The classes of the parameters of the method to invoke.
58      */

59     public ReflectionMethodInvoker(Class<T> clazz,
60                                    Class<R> returnType,
61                                    String methodName,
62                                    Class<?>... parameterTypes) {
63         this.clazz = clazz;
64         this.methodName = methodName;
65         this.returnType = returnType;
66         this.parameterTypes = parameterTypes;
67     }
68
69     /**
70      * Attempt to invoke the method this proxy targets for on the given object with the given arguments. If the
71      * invoker has not yet been initialized, an attempt to initialize the invoker will be made first. If the call
72      * succeeds the invoker will be in an initialized state after this call and future calls to the same method will
73      * not incur an initialization cost. If the call fails because the target method could not be found, the invoker
74      * will remain in an uninitialized state after the exception is thrown.
75      * @param obj The object to invoke the method on.
76      * @param args The arguments to pass to the method. These arguments must match the signature of the method.
77      * @return The returned value of the method cast to the 'returnType' class that this proxy was initialized with.
78      * @throws NoSuchMethodException if the JVM could not find a method matching the signature specified in the
79      * initialization of this proxy.
80      * @throws RuntimeException if any other exception is thrown when attempting to invoke the method or by the
81      * method itself. The cause of this exception will be the exception that was actually thrown.
82      */

83     public R invoke(T obj, Object... args) throws NoSuchMethodException {
84         Method targetMethod = getTargetMethod();
85
86         try {
87             Object rawResult = targetMethod.invoke(obj, args);
88             return returnType.cast(rawResult);
89         } catch (IllegalAccessException | InvocationTargetException e) {
90             throw new RuntimeException(createInvocationErrorMessage(), e);
91         }
92     }
93
94     /**
95      * Initializes the method invoker by finding and caching the target method from the target class that will be used
96      * for subsequent invoke operations. If the invoker is already initialized this call will do nothing.
97      * @throws NoSuchMethodException if the JVM could not find a method matching the signature specified in the
98      * initialization of this proxy.
99      */

100     public void initialize() throws NoSuchMethodException {
101         getTargetMethod();
102     }
103
104     /**
105      * Gets the initialization state of the invoker.
106      * @return true if the invoker has been initialized and the method has been cached for invocation; otherwise false.
107      */

108     public boolean isInitialized() {
109         return targetMethod != null;
110     }
111
112     private Method getTargetMethod() throws NoSuchMethodException {
113         if (targetMethod != null) {
114             return targetMethod;
115         }
116
117         try {
118             targetMethod = clazz.getMethod(methodName, parameterTypes);
119             return targetMethod;
120         } catch (NullPointerException e) {
121             throw new RuntimeException(createInvocationErrorMessage(), e);
122         }
123     }
124
125     private String createInvocationErrorMessage() {
126         return String.format("Failed to reflectively invoke method %s on %s", methodName, clazz.getName());
127     }
128 }
129