001package com.identityworksllc.iiq.common; 002 003import sailpoint.tools.GeneralException; 004 005import java.lang.invoke.MethodHandle; 006import java.lang.invoke.MethodHandles; 007import java.lang.invoke.MethodType; 008import java.lang.reflect.InvocationHandler; 009import java.lang.reflect.Method; 010import java.lang.reflect.Proxy; 011import java.util.HashMap; 012import java.util.Map; 013import java.util.function.BiConsumer; 014import java.util.function.Consumer; 015 016/** 017 * Proxy wrapper for duck typing in Java. Duck typing is named for the old saying 018 * that 'if it quacks like a duck, it must be a duck'. In dynamically typed languages 019 * like JavaScript, method handles are resolved at runtime so any object implementing 020 * the appropriate methods can be used. 021 * 022 * This class creates a {@link Proxy} that forwards interface methods to the given 023 * wrapped object. This allows you to 'fake' an interface implementation where you 024 * don't have control over the class's source code. 025 * 026 * If the object being wrapped doesn't have a method matching something in your 027 * interface, it will just be ignored. Invoking the method will always return 028 * null with any arguments. 029 * 030 * Some examples: 031 * 032 * 1) Many SailPointObjects have a getAttributes(), but those classes don't implement 033 * a common interface. As a developer, you don't have access to modify SailPoint's 034 * API classes. To simplify your own code, you could create an AttributesContainer 035 * interface and use this class to coerce the SailPointObject to that interface. 036 * 037 * 2) IIQ has two different nearly-identical versions of WebServicesClient. One 038 * is inaccessible except through the connector classloader. You could construct 039 * an instance of the inaccessible client class, exposing any relevant methods 040 * to your application via your own interface. 041 */ 042public class DuckWrapper implements InvocationHandler { 043 /** 044 * Wraps the given object so that it appears to implement the given interface. 045 * Any calls to the interface methods will forward to the most appropriate method 046 * on the object. 047 * 048 * @param intf The interface the resulting proxy needs to implement 049 * @param input The target object to be wrapped within the proxy 050 * @param <T> The resulting type 051 * @return A proxy to the underlying object that appears to implement the given interface 052 * @throws GeneralException if any failures occur establishing the proxy 053 */ 054 @SuppressWarnings("unchecked") 055 public static <T> T wrap(Class<? super T> intf, Object input) throws GeneralException { 056 return wrap(intf, input, null); 057 } 058 059 /** 060 * Wraps the given object in the given interface. Any calls to the 061 * interface methods will forward to the object. 062 * 063 * @param intf The interface the resulting proxy needs to implement 064 * @param input The target object to be wrapped within the proxy 065 * @param callback An optional callback that will be invoked for every method called (for testing) 066 * @param <T> The resulting type 067 * @return A proxy to the underlying object that appears to implement the given interface 068 * @throws GeneralException if any failures occur establishing the proxy 069 */ 070 @SuppressWarnings("unchecked") 071 public static <T> T wrap(Class<? super T> intf, Object input, BiConsumer<Method, MethodHandle> callback) throws GeneralException { 072 try { 073 return (T) Proxy.newProxyInstance(intf.getClassLoader(), new Class[]{intf}, new DuckWrapper(intf, input, callback)); 074 } catch(Exception e) { 075 throw new GeneralException(e); 076 } 077 } 078 079 private final BiConsumer<Method, MethodHandle> callback; 080 081 /** 082 * The method handles (null means no match) 083 */ 084 private final Map<String, MethodHandle> methodHandleMap; 085 086 /** 087 * The wrapped object 088 */ 089 private final Object wrapped; 090 091 /** 092 * Constructs a new instance of DuckWrapper, with the given expected interface, 093 * wrapped object, and optional callback. 094 * 095 * @param intf The interface to analyze 096 * @param wrapped The wrapped object to analyze 097 * @param callback The testing callback method 098 * @throws Exception if any failures occur analyzing methods 099 */ 100 private DuckWrapper(Class<?> intf, Object wrapped, BiConsumer<Method, MethodHandle> callback) throws Exception { 101 this.wrapped = wrapped; 102 this.methodHandleMap = new HashMap<>(); 103 this.callback = callback; 104 105 MethodHandles.Lookup publicLookup = MethodHandles.publicLookup(); 106 107 for(Method m : intf.getMethods()) { 108 MethodType type = MethodType.methodType(m.getReturnType(), m.getParameterTypes()); 109 try { 110 MethodHandle mh = publicLookup.findVirtual(wrapped.getClass(), m.getName(), type); 111 methodHandleMap.put(m.toString(), mh); 112 } catch(NoSuchMethodException e) { 113 // This is fine. invoke() will just return null 114 methodHandleMap.put(m.toString(), null); 115 } 116 } 117 } 118 119 /** 120 * Invokes the optional callback, and then the method on the wrapped object, in 121 * that order. If the method handle didn't match (i.e., your interface has a 122 * method not matched in the wrapped object), this will return null without 123 * failing. 124 * 125 * @see InvocationHandler#invoke(Object, Method, Object[]) 126 */ 127 @Override 128 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 129 MethodHandle handle = methodHandleMap.get(method.toString()); 130 if (callback != null) { 131 callback.accept(method, handle); 132 } 133 if (handle == null) { 134 return null; 135 } 136 int length = 0; 137 if (args != null) { 138 length = args.length; 139 } 140 Object[] arguments = new Object[length + 1]; 141 arguments[0] = wrapped; 142 if (args != null) { 143 System.arraycopy(args, 0, arguments, 1, args.length); 144 } 145 return handle.invokeWithArguments(arguments); 146 } 147}