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}