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}