001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2025 microBean™.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
006 * the License. You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
011 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
012 * specific language governing permissions and limitations under the License.
013 */
014package org.microbean.proxy;
015
016import java.lang.reflect.Constructor;
017import java.lang.reflect.Executable;
018import java.lang.reflect.GenericArrayType;
019import java.lang.reflect.GenericDeclaration;
020import java.lang.reflect.Method;
021import java.lang.reflect.ParameterizedType;
022import java.lang.reflect.Type;
023import java.lang.reflect.TypeVariable;
024import java.lang.reflect.WildcardType;
025
026import java.util.List;
027
028import java.util.function.Supplier;
029
030import javax.lang.model.element.ExecutableElement;
031import javax.lang.model.element.Parameterizable;
032import javax.lang.model.element.TypeElement;
033
034import javax.lang.model.type.DeclaredType;
035import javax.lang.model.type.TypeKind;
036import javax.lang.model.type.TypeMirror;
037
038import java.util.function.Supplier;
039
040import org.microbean.construct.Domain;
041
042/**
043 * An {@link AbstractProxier} that helps subclasses create {@link Proxy proxies} using the {@link
044 * java.lang.reflect.Proxy java.lang.reflect.Proxy} machinery present in the Java Development Kit.
045 *
046 * <p>This class also contains various {@code protected} utility methods that help with converting reflective {@link
047 * Type}s and {@link Executable}s to their {@link TypeMirror} and {@link ExecutableElement} counterparts.</p>
048 *
049 * @param <PS> the {@link ProxySpecification} type
050 *
051 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
052 *
053 * @see #proxy(ProxySpecification, Supplier)
054 *
055 * @see #type(Type)
056 *
057 * @see #executableElement(Executable)
058 */
059public abstract non-sealed class AbstractReflectiveProxier<PS extends ProxySpecification> extends AbstractProxier<PS> {
060
061  private static final TypeMirror[] EMPTY_TYPE_MIRROR_ARRAY = new TypeMirror[0];
062
063  /**
064   * Creates a new {@link AbstractReflectiveProxier} implementation.
065   *
066   * @param domain a {@link Domain}; must not be {@code null}
067   *
068   * @exception NullPointerException if {@code domain} is {@code null}
069   */
070  protected AbstractReflectiveProxier(final Domain domain) {
071    super(domain);
072  }
073
074  /**
075   * Returns {@code true} if the supplied {@link Method} is the {@link Object#equals(Object)
076   * java.lang.Object#equals(java.lang.Object)} method.
077   *
078   * @param m a {@link Method}; must not be {@code null}
079   *
080   * @return {@code true} if the supplied {@link Method} is the {@link Object#equals(Object)
081   * java.lang.Object#equals(java.lang.Object)} method
082   *
083   * @exception NullPointerException if {@code m} is {@code null}
084   */
085  protected final boolean equalsMethod(final Method m) {
086    return
087      m.getDeclaringClass() == Object.class &&
088      m.getReturnType() == boolean.class &&
089      m.getParameterCount() == 1 &&
090      m.getParameterTypes()[0] == Object.class &&
091      m.getName().equals("equals");
092  }
093
094  /**
095   * Returns an {@link ExecutableElement} corresponding to the supplied {@link Executable}.
096   *
097   * @param e an {@link Executable}; must not be {@code null}
098   *
099   * @return an {@link ExecutableElement} corresponding to the supplied {@link Executable}; never {@code null}
100   *
101   * @exception NullPointerException if {@code e} is {@code null}
102   *
103   * @exception IllegalArgumentException if somehow {@code e} is neither a {@link Constructor} nor a {@link Method}
104   */
105  protected final ExecutableElement executableElement(final Executable e) {
106    final Domain domain = domain();
107    return switch (e) {
108    case null -> throw new NullPointerException("e");
109    case Constructor<?> c ->
110      domain.executableElement(domain.typeElement(c.getDeclaringClass().getCanonicalName()),
111                               domain.noType(TypeKind.VOID),
112                               "<init>",
113                               this.types(c.getParameterTypes()));
114    case Method m ->
115      domain.executableElement(domain.typeElement(m.getDeclaringClass().getCanonicalName()),
116                               this.type(m.getReturnType()),
117                               m.getName(),
118                               this.types(m.getParameterTypes()));
119    default -> throw new IllegalArgumentException("e: " + e);
120    };
121  }
122
123  /**
124   * Returns {@code true} if the supplied {@link Method} is the {@link Object#hashCode() java.lang.Object#hashCode()}
125   * method.
126   *
127   * @param m a {@link Method}; must not be {@code null}
128   *
129   * @return {@code true} if the supplied {@link Method} is the {@link Object#hashCode() java.lang.Object#hashCode()}
130   * method
131   *
132   * @exception NullPointerException if {@code m} is {@code null}
133   */
134  protected final boolean hashCodeMethod(final Method m) {
135    return
136      m.getDeclaringClass() == Object.class &&
137      m.getReturnType() == int.class &&
138      m.getParameterCount() == 0 &&
139      m.getName().equals("hashCode");
140  }
141
142  /**
143   * Returns a {@link Parameterizable} corresponding to the supplied {@link GenericDeclaration}.
144   *
145   * @param gd a {@link GenericDeclaration}; must not be {@code null}
146   *
147   * @return a {@link Parameterizable} corresponding to the supplied {@link GenericDeclaration}; never {@code null}
148   *
149   * @exception NullPointerException if {@code gd} is {@code null}
150   *
151   * @exception IllegalArgumentException if {@code gd} is neither a {@link Class} nor an {@link Executable}
152   */
153  protected final Parameterizable parameterizable(final GenericDeclaration gd) {
154    final Domain domain = this.domain();
155    return switch (gd) {
156    case null -> throw new NullPointerException("gd");
157    case Class<?> c -> domain.typeElement(c.getCanonicalName());
158    case Executable e -> this.executableElement(e);
159    default -> throw new IllegalArgumentException("gd: " + gd);
160    };
161  }
162
163  /**
164   * Returns a {@link Proxy} appropriate for the supplied specification and {@link Supplier} of instances.
165   *
166   * @param ps an appropriate proxy specification; must not be {@code null}
167   *
168   * @param instanceSupplier a {@link Supplier} of contextual instances; must not be {@code null}; may or may not create
169   * a new contextual instance each time it is invoked; may or may not be invoked multiple times depending on the
170   * subclass implementation
171   *
172   * @return a non-{@code null} {@link Proxy}
173   *
174   * @exception NullPointerException if any argument is {@code null}
175   *
176   * @exception IllegalArgumentException if the {@linkplain ProxySpecification#superclass() superclass} is not {@link
177   * Object java.lang.Object} (only interfaces may be proxied reflectively)
178   *
179   * @see #proxy(ProxySpecification, Class[], Supplier)
180   *
181   * @see #classLoader()
182   */
183  @Override // AbstractProxier<PS>
184  public final <R> Proxy<R> proxy(final PS ps, final Supplier<? extends R> instanceSupplier) {
185    final Domain domain = this.domain();
186    if (!domain.javaLangObject(ps.superclass())) {
187      throw new IllegalArgumentException("ps: " + ps);
188    }
189    final List<? extends TypeMirror> interfaceTypeMirrors = ps.interfaces();
190    final int size = interfaceTypeMirrors.size();
191    final Class<?>[] interfaces = new Class<?>[size];
192    final ClassLoader classLoader = this.classLoader();
193    try {
194      for (int i = 0; i < size; i++) {
195        final TypeElement e = (TypeElement)((DeclaredType)interfaceTypeMirrors.get(i)).asElement();
196        final String binaryName = domain.toString(domain.binaryName(e));
197        interfaces[i] = Class.forName(binaryName, false, classLoader);
198      }
199    } catch (final ClassNotFoundException cnfe) {
200      throw new IllegalArgumentException("ps: " + ps, cnfe);
201    }
202    return this.proxy(ps, interfaces, instanceSupplier);
203  }
204
205  /**
206   * Returns a {@link Proxy} appropriate for the supplied specification and {@link Supplier} of contextual instances.
207   *
208   * @param <R> the contextual instance type
209   *
210   * @param ps an appropriate proxy specification; must not be {@code null}
211   *
212   * @param interfaces the interfaces to implement; every element is guaranteed to {@linkplain Class#isInterface() be an interface}
213   *
214   * @param instanceSupplier a {@link Supplier} of contextual instances; must not be {@code null}; may or may not create
215   * a new contextual instance each time it is invoked; may or may not be invoked multiple times depending on the
216   * subclass implementation
217   *
218   * @return a non-{@code null} {@link Proxy}
219   *
220   * @exception NullPointerException if any argument is {@code null}
221   */
222  protected abstract <R> Proxy<R> proxy(final PS ps,
223                                        final Class<?>[] interfaces,
224                                        final Supplier<? extends R> instanceSupplier);
225
226  /**
227   * Returns the {@link TypeMirror} corresponding to the supplied {@link Type}.
228   *
229   * @param t a {@link Type}; must not be {@code null}
230   *
231   * @return the {@link TypeMirror} corresponding to the supplied {@link Type}; never {@code null}
232   *
233   * @exception NullPointerException if {@code t} is {@code null}
234   *
235   * @exception IllegalArgumentException if {@code t} is not a {@link Class}, {@link GenericArrayType}, {@link
236   * ParameterizedType}, {@link TypeVariable} or {@link WildcardType}
237   */
238  protected final TypeMirror type(final Type t) {
239    // TODO: anywhere there is domain.declaredType(), consider passing
240    // domain.moduleElement(this.getClass().getModule().getName()) as the first argument. Not sure how this works
241    // exactly but I think it might be necessary.
242    final Domain domain = this.domain();
243    return switch (t) {
244    case null -> throw new NullPointerException("t");
245    case Class<?> c when t == boolean.class -> domain.primitiveType(TypeKind.BOOLEAN);
246    case Class<?> c when t == byte.class -> domain.primitiveType(TypeKind.BYTE);
247    case Class<?> c when t == char.class -> domain.primitiveType(TypeKind.CHAR);
248    case Class<?> c when t == double.class -> domain.primitiveType(TypeKind.DOUBLE);
249    case Class<?> c when t == float.class -> domain.primitiveType(TypeKind.FLOAT);
250    case Class<?> c when t == int.class -> domain.primitiveType(TypeKind.INT);
251    case Class<?> c when t == long.class -> domain.primitiveType(TypeKind.LONG);
252    case Class<?> c when t == short.class -> domain.primitiveType(TypeKind.SHORT);
253    case Class<?> c when t == void.class -> domain.noType(TypeKind.VOID);
254    case Class<?> c when t == Object.class -> domain.javaLangObject().asType(); // cheap and easy optimization
255    case Class<?> c when c.isArray() -> domain.arrayTypeOf(this.type(c.getComponentType()));
256    case Class<?> c -> domain.declaredType(c.getCanonicalName());
257    case GenericArrayType g -> domain.arrayTypeOf(this.type(g.getGenericComponentType()));
258    case ParameterizedType pt when pt.getOwnerType() == null ->
259      domain.declaredType(domain.typeElement(((Class<?>)pt.getRawType()).getCanonicalName()),
260                          this.types(pt.getActualTypeArguments()));
261    case ParameterizedType pt ->
262      domain.declaredType((DeclaredType)this.type(pt.getOwnerType()),
263                          domain.typeElement(((Class<?>)pt.getRawType()).getCanonicalName()),
264                          this.types(pt.getActualTypeArguments()));
265    case TypeVariable<?> tv -> domain.typeVariable(this.parameterizable(tv.getGenericDeclaration()), tv.getName());
266    case WildcardType w when w.getLowerBounds().length <= 0 -> domain.wildcardType(this.type(w.getUpperBounds()[0]), null);
267    case WildcardType w -> domain.wildcardType(null, this.type(w.getLowerBounds()[0]));
268    default -> throw new IllegalArgumentException("t: " + t);
269    };
270  }
271
272  /**
273   * Returns an array of {@link TypeMirror}s whose elements correspond to the elements in the supplied {@link Type} array.
274   *
275   * @param ts an array of {@link Type}s; must not be {@code null}
276   *
277   * @return an array of {@link TypeMirror}s whose elements correspond to the elements in the supplied {@link Type}
278   * array; never {@code null}
279   *
280   * @exception NullPointerException if {@code ts} is {@code null} or contains {@code null} elements
281   *
282   * @exception IllegalArgumentException if any element of {@code ts} is deemed illegal by the {@link #type(Type)}
283   * method
284   *
285   * @see #type(Type)
286   */
287  protected final TypeMirror[] types(final Type[] ts) {
288    if (ts.length <= 0) {
289      return EMPTY_TYPE_MIRROR_ARRAY;
290    } else if (ts.length == 1) {
291      // cheap and easy optimization
292      return new TypeMirror[] { type(ts[0]) };
293    }
294    final TypeMirror[] rv = new TypeMirror[ts.length];
295    for (int i = 0; i < ts.length; i++) {
296      rv[i] = type(ts[i]);
297    }
298    return rv;
299  }
300
301}