001package ball.lang.reflect;
002/*-
003 * ##########################################################################
004 * Utilities
005 * %%
006 * Copyright (C) 2008 - 2022 Allen D. Ball
007 * %%
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *      http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 * ##########################################################################
020 */
021import java.lang.reflect.Array;
022import java.lang.reflect.InvocationHandler;
023import java.lang.reflect.Method;
024import java.lang.reflect.Proxy;
025import java.util.IdentityHashMap;
026import java.util.stream.Collectors;
027import lombok.NoArgsConstructor;
028import lombok.ToString;
029
030import static lombok.AccessLevel.PROTECTED;
031
032/**
033 * {@link InvocationHandler} abstract base class to "extend" concrete
034 * implementation {@link Class}es by adding "facade" interfaces.  See
035 * {@link #getProxyClassFor(Object)}.
036 *
037 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
038 */
039@NoArgsConstructor(access = PROTECTED) @ToString
040public abstract class FacadeProxyInvocationHandler extends DefaultInvocationHandler {
041    private final ProxyMap map = new ProxyMap();
042
043    /**
044     * Method to return an extended {@link Proxy} implementing "facade"
045     * interfaces for {@code in} if {@link #getProxyClassFor(Object)}
046     * returns non-{@code null}.
047     *
048     * @param   in              The {@link Object} to extend.
049     *
050     * @return  A {@link Proxy} if {@link #getProxyClassFor(Object)} returns
051     *          non-{@code null}; {@code in} otherwise.
052     */
053    public Object enhance(Object in) {
054        Object out = null;
055
056        if (! hasFacade(in)) {
057            Class<?> type = getProxyClassFor(in);
058
059            if (type != null) {
060                out = map.computeIfAbsent(in, k -> compute(type));
061            }
062        }
063
064        return (out != null) ? out : in;
065    }
066
067    private <T> T compute(Class<T> type) {
068        T proxy = null;
069
070        try {
071            proxy = type.getConstructor(InvocationHandler.class).newInstance(this);
072        } catch (RuntimeException exception) {
073            throw exception;
074        } catch (Exception exception) {
075            throw new IllegalStateException(exception);
076        }
077
078        return proxy;
079    }
080
081    /**
082     * Method provided by subclasses to provide the {@link Proxy}
083     * {@link Class} if the input {@link Object} should be extended.
084     *
085     * @param   object          The {@link Object} that may or may not be
086     *                          extended.
087     *
088     * @return  A {@link Proxy} {@link Class} if the {@link Object} should
089     *          be extended; {@code null} otherwise.
090     */
091    protected abstract Class<?> getProxyClassFor(Object object);
092
093    private boolean hasFacade(Object object) {
094        return reverseOf(object) != null;
095    }
096
097    private Object reverseOf(Object in) {
098        Object out = null;
099
100        if (in instanceof Proxy && Proxy.isProxyClass(in.getClass())) {
101            InvocationHandler handler = Proxy.getInvocationHandler(in);
102
103            if (handler instanceof FacadeProxyInvocationHandler) {
104                out = ((FacadeProxyInvocationHandler) handler).map.reverse().get(in);
105            }
106        }
107
108        return out;
109    }
110
111    private Object reverseFor(Class<?> type, Object in) {
112        Object out = null;
113
114        if (out == null) {
115            Object object = reverseOf(in);
116
117            if (object != null && type.isAssignableFrom(object.getClass())) {
118                out = object;
119            }
120        }
121
122        if (out == null) {
123            if (in instanceof Object[]) {
124                if (type.isArray()) {
125                    int length = ((Object[]) in).length;
126
127                    out = Array.newInstance(type.getComponentType(), length);
128
129                    for (int i = 0; i < length; i += 1) {
130                        ((Object[]) out)[i] = reverseFor(type.getComponentType(), ((Object[]) in)[i]);
131                    }
132                }
133            }
134        }
135
136        return (out != null) ? out : in;
137    }
138
139    private Object[] reverseFor(Class<?>[] types, Object[] in) {
140        Object[] out = null;
141
142        if (in != null) {
143            out = new Object[in.length];
144
145            for (int i = 0; i < out.length; i += 1) {
146                out[i] = reverseFor(types[i], in[i]);
147            }
148        }
149
150        return (out != null) ? out : in;
151    }
152
153    @Override
154    public Object invoke(Object proxy, Method method, Object[] argv) throws Throwable {
155        Object result = null;
156        Class<?> declarer = method.getDeclaringClass();
157        Object that = map.reverse.get(proxy);
158
159        if (declarer.isAssignableFrom(Object.class)) {
160            result = method.invoke(that, argv);
161        } else {
162            argv = reverseFor(method.getParameterTypes(), argv);
163
164            if (declarer.isAssignableFrom(that.getClass())) {
165                result = method.invoke(that, argv);
166            } else {
167                result = super.invoke(proxy, method, argv);
168            }
169        }
170
171        return enhance(result);
172    }
173
174    @NoArgsConstructor
175    private class ProxyMap extends IdentityHashMap<Object,Object> {
176        private static final long serialVersionUID = 6708505296087349421L;
177
178        private final IdentityHashMap<Object,Object> reverse = new IdentityHashMap<>();
179
180        public IdentityHashMap<Object,Object> reverse() { return reverse; }
181
182        @Override
183        public Object put(Object key, Object value) {
184            reverse().put(value, key);
185
186            return super.put(key, value);
187        }
188    }
189}