001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2022 microBean™.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.loader;
018
019import java.lang.annotation.Annotation;
020
021import java.lang.reflect.InvocationHandler;
022import java.lang.reflect.Method;
023import java.lang.reflect.Modifier;
024import java.lang.reflect.Parameter;
025import java.lang.reflect.Proxy;
026import java.lang.reflect.Type;
027
028import java.util.Arrays;
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.List;
033import java.util.Map;
034import java.util.Objects;
035import java.util.TreeMap;
036import java.util.TreeSet;
037
038import java.util.concurrent.ConcurrentHashMap;
039import java.util.concurrent.ConcurrentMap;
040
041import java.util.function.BiFunction;
042import java.util.function.Supplier;
043
044import org.microbean.development.annotation.Convenience;
045
046import org.microbean.invoke.OptionalSupplier;
047import org.microbean.invoke.OptionalSupplier.Determinism;
048
049import org.microbean.loader.api.Loader;
050
051import org.microbean.loader.spi.AbstractProvider;
052import org.microbean.loader.spi.LoaderFacade;
053import org.microbean.loader.spi.Value;
054
055import org.microbean.path.Path;
056import org.microbean.path.Path.Element;
057
058import org.microbean.qualifier.Qualifier;
059import org.microbean.qualifier.Qualifiers;
060
061import org.microbean.type.JavaTypes;
062
063/**
064 * An {@link AbstractProvider} that is capable of {@linkplain Proxy
065 * proxying} {@linkplain #isProxiable(Loader, Path) certain}
066 * interfaces and supplying them as environmental objects.
067 *
068 * @author <a href="https://about.me/lairdnelson"
069 * target="_parent">Laird Nelson</a>
070 *
071 * @see #get(Loader, Path)
072 *
073 * @see #isProxiable(Loader, Path)
074 */
075public class ProxyingProvider extends AbstractProvider {
076
077
078  /*
079   * Instance fields.
080   */
081
082
083  private final ConcurrentMap<Path<? extends Type>, Object> proxies;
084
085
086  /*
087   * Constructors.
088   */
089
090
091  /**
092   * Creates a new {@link ProxyingProvider}.
093   *
094   * @deprecated This constructor should be invoked by subclasses and
095   * {@link java.util.ServiceLoader} instances only.
096   */
097  @Deprecated // intended for use by subclasses and java.util.ServiceLoader only
098  public ProxyingProvider() {
099    super();
100    this.proxies = new ConcurrentHashMap<>();
101  }
102
103
104  /*
105   * Instance methods.
106   */
107
108
109  @Override // Provider
110  protected Supplier<?> find(final Loader<?> requestor, final Path<? extends Type> absolutePath) {
111    assert absolutePath.absolute();
112    assert absolutePath.startsWith(requestor.path());
113    assert !absolutePath.equals(requestor.path());
114    if (this.isProxiable(requestor, absolutePath)) {
115      return
116        OptionalSupplier.of(Determinism.PRESENT,
117                            () -> this.proxies.computeIfAbsent(absolutePath,
118                                                               p -> this.newProxyInstance(requestor, p, JavaTypes.erase(p.qualified()))));
119    }
120    return null;
121  }
122
123
124  /**
125   * Returns {@code true} if the {@linkplain Path#qualified() type
126   * identified by the supplied <code>absolutePath</code>} can be
127   * proxied.
128   *
129   * <p>A type can be proxied by this {@link ProxyingProvider} if its
130   * {@linkplain JavaTypes#erase(Type) type erasure}:</p>
131   *
132   * <ul>
133   *
134   * <li>is an {@linkplain Class#isInterface() interface}</li>
135   *
136   * <li>is {@linkplain Class#isHidden() not hidden}</li>
137   *
138   * <li>is {@linkplain Class#isSealed() not sealed}</li>
139   *
140   * </ul>
141   *
142   * <p>In addition, the default implementation of this method rules
143   * out interfaces that declare or inherit {@code public} instance
144   * methods with either exactly one parameter that does not pass the
145   * test codified by the {@link #isIndexLike(Class)} method or more
146   * than one parameter.</p>
147   *
148   * @param requestor the {@link Loader} seeking an environmental
149   * object; must not be {@code null}; ignored by the default
150   * implementation of this method
151   *
152   * @param absolutePath the {@link Path} {@linkplain Path#qualified()
153   * identifying the interface to be proxied}; must not be {@code
154   * null}; must be {@linkplain Path#absolute() absolute}
155   *
156   * @return {@code true} if the {@linkplain Path#qualified() type
157   * identified by the supplied <code>absolutePath</code>} can be
158   * proxied; {@code false} otherwise
159   *
160   * @exception NullPointerException if either argument is {@code
161   * null}
162   *
163   * @exception IllegalArgumentException if {@code absolutePath} is
164   * not {@linkplain Path#absolute() absolute}
165   *
166   * @threadsafety This method is, and its overrides must be, safe for
167   * concurrent use by multiple threads.
168   *
169   * @idempotency This method is, and its overrides must be,
170   * idempotent and deterministic.
171   *
172   * @see #isIndexLike(Class)
173   *
174   * @see #isProxiable(Class)
175   */
176  public boolean isProxiable(final Loader<?> requestor, final Path<? extends Type> absolutePath) {
177    return this.isProxiable(absolutePath.qualified());
178  }
179
180  /**
181   * Returns {@code true} if the supplied {@link Type} can be
182   * proxied.
183   *
184   * <p>A {@link Type} can be proxied if it:</p>
185   *
186   * <ul>
187   *
188   * <li>is not {@code null}</li>
189   *
190   * <li>represents an {@linkplain Class#isInterface() interface}</li>
191   *
192   * <li>is {@linkplain Class#isHidden() not hidden}</li>
193   *
194   * <li>is {@linkplain Class#isSealed() not sealed}</li>
195   *
196   * </ul>
197   *
198   * <p>In addition, the default implementation of this method rules
199   * out interfaces that declare or inherit {@code public} instance
200   * methods with either exactly one parameter that does not pass the
201   * test codified by the {@link #isIndexLike(Class)} method or more
202   * than one parameter.</p>
203   *
204   * <p>This method does not, and its overrides must not, call the
205   * {@link #isProxiable(Loader, Path)} method or undefined behavior
206   * (such as an infinite loop) may result.</p>
207   *
208   * @param type the {@link Type} to test; may be {@code null} in which
209   * case {@code false} will be returned
210   *
211   * @return {@code true} if the supplied {@link Type} can be
212   * proxied; {@code false} otherwise
213   *
214   * @threadsafety This method is, and its overrides must be, safe for
215   * concurrent use by multiple threads.
216   *
217   * @idempotency This method is, and its overrides must be,
218   * idempotent and deterministic.
219   *
220   * @see #isIndexLike(Class)
221   */
222  public boolean isProxiable(final Type type) {
223    final Class<?> c = JavaTypes.erase(type);
224    if (c != null && c.isInterface() && !c.isHidden() && !c.isSealed()) {
225      final LoaderFacade facadeAnnotation = c.getAnnotation(LoaderFacade.class);
226      if (facadeAnnotation == null || facadeAnnotation.value()) {
227        final Method[] methods = c.getMethods();
228        switch (methods.length) {
229        case 0:
230          // Interface with no methods.
231          return false;
232        default:
233          int getterCount = 0;
234          int defaultCount = 0;
235          for (final Method m : methods) {
236            if (m.isDefault()) {
237              // Found a default method.
238              ++defaultCount;
239            } else if (!Modifier.isStatic(m.getModifiers())) {
240              // Found an abstract instance method.
241              final Type returnType = m.getReturnType();
242              if (returnType != void.class && returnType != Void.class) {
243                // Found an abstract instance method that returns something other than void.
244                switch (m.getParameterCount()) {
245                case 0:
246                  // It has no parameters, so it's a getter.
247                  ++getterCount;
248                  break;
249                case 1:
250                  if (!this.isIndexLike(m.getParameterTypes()[0])) {
251                    // It has one parameter, and that parameter is not
252                    // "index-like", so we don't know what to do with
253                    // it.
254                    return false;
255                  }
256                  // It has one parameter, and that parameter is
257                  // "index-like", so we could conceivably implement
258                  // it with a map or a list.  Keep going.
259                  ++getterCount;
260                  break;
261                default:
262                  // It has more than one parameter so we don't know
263                  // what to do with it.
264                  return false;
265                }
266              }
267            }
268          }
269          return getterCount > 0 || defaultCount > 0;
270        }
271
272      }
273    }
274    return false;
275  }
276
277  /**
278   * Returns a {@link Path} suitable for the combination of the
279   * supplied {@link Loader} and requested {@link Path}.
280   *
281   * @param <T> the type of the requested and returned {@link Path}s
282   *
283   * @param requestor the {@link Loader} issuing the current request;
284   * must not be {@code null}; ignored by this implementation
285   *
286   * @param absolutePath the {@linkplain Path#absolute() absolute}
287   * {@link Path} representing the current request; must not be {@code
288   * null}
289   *
290   * @return a non-{@code null} {@link Path} with which any {@link
291   * Value} provided by this {@link ProxyingProvider} will be
292   * associated
293   *
294   * @nullability This method does not, and its overrides must not,
295   * return {@code null}.
296   *
297   * @idempotency This method is, and its overrides must be,
298   * idempotent but not necessarily deterministic.
299   *
300   * @threadsafety This method is, and its overrides must be, safe for
301   * concurrent use by multiple threads.
302   */
303  @Override // AbstractProvider
304  protected <T extends Type> Path<T> path(final Loader<?> requestor, final Path<T> absolutePath) {
305    return Path.of(absolutePath.qualified());
306  }
307
308  /**
309   * Returns {@code true} if the supplied {@link Class} representing a
310   * method parameter is <em>index-like</em>, i.e. if it is something
311   * typically used as an index into a larger collection or map.
312   *
313   * <p>The default implementation of this method returns {@code true}
314   * if {@code parameterType} represents either an {@code int}, an
315   * {@link Integer}, or a {@link CharSequence}.</p>
316   *
317   * <p>This method is called by the default implementation of the
318   * {@link #isProxiable(Loader, Path)} method.</p>
319   *
320   * @param parameterType the method parameter type to test; may be
321   * {@code null} in which case {@code false} will be returned
322   *
323   * @return {@code true} if the supplied {@link Class} representing a
324   * method parameter is <em>index-like</em>, i.e. if it is something
325   * typically used as an index into a larger collection or map
326   *
327   * @threadsafety This method is, and its overrides must be, safe for
328   * concurrent use by multiple threads.
329   *
330   * @idempotency This method is, and its overrides must be,
331   * idempotent and deterministic.
332   */
333  protected boolean isIndexLike(final Class<?> parameterType) {
334    return
335      parameterType == int.class ||
336      parameterType == Integer.class ||
337      CharSequence.class.isAssignableFrom(parameterType);
338  }
339
340  /**
341   * Invokes the {@link Proxy#newProxyInstance(ClassLoader, Class[],
342   * InvocationHandler)} method with appropriate arguments and returns
343   * the result.
344   *
345   * <p>The {@link Proxy#newProxyInstance(ClassLoader, Class[],
346   * InvocationHandler)} method is invoked with the following
347   * arguments:</p>
348   *
349   * <ol>
350   *
351   * <li>{@code interfaceToProxy.getClassLoader()}</li>
352   *
353   * <li>{@code new Class<?>[] { interfaceToProxy }}</li>
354   *
355   * <li>a special {@link InvocationHandler} backed by the supplied
356   * {@link Loader} and {@link Path}</li>
357   *
358   * </ol>
359   *
360   * @param requestor the {@link Loader} performing the current
361   * request; must not be {@code null}
362   *
363   * @param absolutePath an {@linkplain Path#absolute() absolute}
364   * {@link Path} representing the current request; must not be {@code
365   * null}
366   *
367   * @param interfaceToProxy the single interface the new proxy
368   * instance will implement; must not be {@code null}; must be an
369   * interface
370   *
371   * @return a new proxy instance as produced by the {@link
372   * Proxy#newProxyInstance(ClassLoader, Class[], InvocationHandler)}
373   * method; never {@code null}
374   *
375   * @exception NullPointerException if any argument is {@code null}
376   *
377   * @exception IllegalArgumentException if any argument is unsuitable
378   *
379   * @nullability This method does not, and its overrides must not,
380   * return {@code null}.
381   *
382   * @idempotency This method is, and its overrides must be,
383   * idempotent and deterministic.
384   *
385   * @threadsafety This method is, and its overrides must be, safe for
386   * concurrent use by multiple threads.
387   *
388   * @see Proxy#newProxyInstance(ClassLoader, Class[], InvocationHandler)
389   */
390  protected Object newProxyInstance(final Loader<?> requestor,
391                                    final Path<? extends Type> absolutePath,
392                                    final Class<?> interfaceToProxy) {
393    return
394      Proxy.newProxyInstance(interfaceToProxy.getClassLoader(),
395                             new Class<?>[] { interfaceToProxy },
396                             new Handler(requestor, absolutePath, (m, args) -> path(m, args)));
397  }
398
399
400  /*
401   * Static methods.
402   */
403
404
405  private static final Path<? extends Type> path(final Method m, final Object[] args) {
406    final Collection<Qualifier<String, Object>> c;
407    final Parameter[] parameters = m.getParameters();
408    if (parameters.length > 0) {
409      if (args.length != parameters.length) {
410        throw new IllegalArgumentException("args: " + args);
411      }
412      c = new TreeSet<>();      
413      for (int i = 0; i < parameters.length; i++) {
414        c.add(Qualifier.of(parameters[i].getName(), String.valueOf(args[i])));
415      }
416    } else {
417      c = Collections.emptySortedSet();
418    }
419    final Type type = m.getGenericReturnType();
420    return Path.of(Element.of(Qualifiers.of(c), type, propertyName(m.getName(), boolean.class == type)));
421  }
422
423  /**
424   * Given a {@link CharSequence} normally representing the name of a
425   * "getter" method, and a {@code boolean} indicating whether the
426   * method in question returns a {@code boolean}, applies the rules
427   * declared by the Java Beans specification to the name and yields
428   * the result.
429   *
430   * @param cs a {@link CharSequence} naming a "getter" method; may be
431   * {@code null} in which case {@code null} will be returned
432   *
433   * @param methodReturnsBoolean {@code true} if the method named by
434   * the supplied {@link CharSequence} has {@code boolean} as its
435   * return type
436   *
437   * @return the property name corresponding to the supplied method
438   * name, according to the rules of the Java Beans specification, or
439   * {@code null} (only if {@code cs} is {@code null})
440   *
441   * @nullability This method may return {@code null} but only when
442   * {@code cs} is {@code null}.
443   *
444   * @threadsafety This method is safe for concurrent use by multiple
445   * threads.
446   *
447   * @idempotency This method is idempotent and deterministic.
448   *
449   * @see #decapitalize(CharSequence)
450   */
451  @Convenience
452  public static final String propertyName(final CharSequence cs, final boolean methodReturnsBoolean) {
453    if (cs == null) {
454      return null;
455    } else {
456      final int length = cs.length();
457      if (length > 3) {
458        switch (cs.charAt(0)) {
459        case 'g':
460          if (cs.charAt(1) == 'e' && cs.charAt(2) == 't') {
461            // getFoo() -> decapitalize("Foo")
462            return decapitalize(cs.subSequence(3, length));
463          }
464          break;
465        default:
466          break;
467        }
468      } else if (methodReturnsBoolean && length > 2) {
469        switch (cs.charAt(0)) {
470        case 'i':
471          if (cs.charAt(1) == 's') {
472            // isFoo() -> decapitalize("Foo")
473            return decapitalize(cs.subSequence(2, length));
474          }
475          break;
476        default:
477          break;
478        }
479      }
480      return decapitalize(cs);
481    }
482  }
483
484  /**
485   * Decapitalizes the supplied {@link CharSequence} according to the
486   * rules of the Java Beans specification.
487   *
488   * @param cs the {@link CharSequence} to decapitalize; may be {@code
489   * null} in which case {@code null} will be returned
490   *
491   * @return the decapitalized {@link String} or {@code null}
492   *
493   * @nullability This method may return {@code null} but only when
494   * {@code cs} is {@code null}.
495   *
496   * @threadsafety This method is safe for concurrent use by multiple
497   * threads.
498   *
499   * @idempotency This method is idempotent and deterministic.
500   */
501  public static final String decapitalize(final CharSequence cs) {
502    if (cs == null) {
503      return null;
504    }
505    final String s = cs.toString(); // for atomicity
506    switch (s.length()) {
507    case 0:
508      return s;
509    case 1:
510      return s.toLowerCase();
511    default:
512      if (Character.isUpperCase(s.charAt(1))) {
513        if (Character.isUpperCase(s.charAt(0))) {
514          return s;
515        }
516      } else if (Character.isLowerCase(s.charAt(0))) {
517        return s;
518      }
519      final char[] chars = s.toCharArray();
520      chars[0] = Character.toLowerCase(chars[0]);
521      return String.valueOf(chars);
522    }
523  }
524
525
526  /*
527   * Inner and nested classes.
528   */
529
530
531  private static final class Handler implements InvocationHandler {
532
533    /**
534     * A {@link Loader} whose {@link Loader#of(Path)} method will
535     * eventually be called by the {@link #invoke(Object, Method,
536     * Object[])} method.
537     *
538     * <p>Note that this {@link Loader}'s {@link Loader#path()} method
539     * will return a {@link Path} that <em>does not identify</em> the
540     * actual interface being proxied, much less the {@link Method}
541     * being handled by this {@link Handler}.  The {@link
542     * #absolutePath} field, instead, contains the {@link Path}
543     * identifying the proxied interface (and it will {@linkplain
544     * Path#startsWith(Path) start with} the return value of {@link
545     * #requestor requestor.path()}).  During execution of the {@link
546     * #invoke(Object, Method, Object[])} method, the contents of the
547     * {@link #absolutePath} field will be appended with a relative
548     * {@link Path} corresponding to the {@link Method} being handled,
549     * and <em>that</em> resulting absolute {@link Path} will be
550     * supplied to the {@link Loader#of(Path)} method.  Note further
551     * that the {@link Loader#of(Path)} method will internally adjust
552     * the <em>actual</em> {@link Loader} used (see {@link
553     * Loader#loaderFor(Path)}).</p>
554     *
555     * <p>All of this to say: this {@link Loader} is just a handle of
556     * sorts to the proper {@link Loader} that will eventually be used
557     * to locate the environmental object corresponding to the return
558     * value of the {@link Method} being handled, and serves no other
559     * purpose.</p>
560     *
561     * @see #absolutePath
562     *
563     * @see #invoke(Object, Method, Object[])
564     */
565    private final Loader<?> requestor;
566
567    private final Path<? extends Type> absolutePath;
568
569    private final BiFunction<? super Method, ? super Object[], ? extends Path<? extends Type>> pathFunction;
570
571    private Handler(final Loader<?> requestor,
572                    final Path<? extends Type> absolutePath,
573                    final BiFunction<? super Method, ? super Object[], ? extends Path<? extends Type>> pathFunction) {
574      super();
575      if (!absolutePath.absolute()) {
576        throw new IllegalArgumentException("!absolutePath.absolute(): " + absolutePath);
577      } else if (!absolutePath.startsWith(requestor.path())) {
578        throw new IllegalArgumentException("!absolutePath.startsWith(requestor.path()); absolutePath: " + absolutePath +
579                                           "; requestor.path(): " + requestor.path());
580      } else if (absolutePath.equals(requestor.path())) {
581        throw new IllegalArgumentException("absolutePath.equals(requestor.path()): " + absolutePath);
582      }
583      this.requestor = requestor;
584      this.absolutePath = absolutePath;
585      this.pathFunction = Objects.requireNonNull(pathFunction, "pathFunction");
586
587    }
588
589    @Override // InvocationHandler
590    public final Object invoke(final Object proxy, final Method m, final Object[] args) throws ReflectiveOperationException {
591      if (m.getDeclaringClass() == Object.class) {
592        return
593          switch (m.getName()) {
594          case "hashCode" -> System.identityHashCode(proxy);
595          case "equals" -> proxy == args[0];
596          case "toString" -> proxy.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(proxy));
597          default -> throw new AssertionError("method: " + m);
598          };
599      } else {
600        final Object returnType = m.getReturnType();
601        if (returnType == void.class || returnType == Void.class) {
602          return defaultValue(proxy, m, args);
603        } else {
604          final Path<? extends Type> path = this.pathFunction.apply(m, args);
605          assert path.qualified() == returnType : "path.qualified() != returnType: " + path.qualified() + " != " + returnType;
606          assert !path.absolute() : "path.absolute(): " + path;
607          final OptionalSupplier<Object> s = this.requestor.load(this.absolutePath.plus(path));
608          return s.orElseGet(() -> defaultValue(proxy, m, args));
609        }
610      }
611    }
612
613    private static final Object defaultValue(final Object proxy, final Method m, final Object[] args) {
614      if (m.isDefault()) {
615        try {
616          // If the current method is a default method of the proxied
617          // interface, invoke it.
618          return InvocationHandler.invokeDefault(proxy, m, args);
619        } catch (final UnsupportedOperationException | Error e) {
620          throw e;
621        } catch (final Exception e) {
622          throw new UnsupportedOperationException(m.getName(), e);
623        } catch (final Throwable e) {
624          throw new AssertionError(e.getMessage(), e);
625        }
626      } else {
627        // We have no recourse.
628        throw new UnsupportedOperationException(m.toString());
629      }
630    }
631
632  }
633
634}