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.clientproxy.bytebuddy;
015
016import java.util.Collection;
017import java.util.List;
018
019import net.bytebuddy.ByteBuddy;
020
021import net.bytebuddy.description.method.MethodDescription;
022import net.bytebuddy.description.method.ParameterDescription;
023
024import net.bytebuddy.description.modifier.FieldManifestation;
025import net.bytebuddy.description.modifier.MethodManifestation;
026import net.bytebuddy.description.modifier.ParameterManifestation;
027import net.bytebuddy.description.modifier.TypeManifestation;
028
029import net.bytebuddy.description.type.TypeDefinition;
030import net.bytebuddy.description.type.TypeDescription;
031
032import net.bytebuddy.dynamic.DynamicType;
033
034import net.bytebuddy.implementation.DefaultMethodCall;
035import net.bytebuddy.implementation.HashCodeMethod;
036import net.bytebuddy.implementation.EqualsMethod;
037import net.bytebuddy.implementation.FieldAccessor;
038import net.bytebuddy.implementation.MethodCall;
039
040import net.bytebuddy.implementation.bytecode.assign.Assigner;
041
042import net.bytebuddy.matcher.ElementMatcher;
043
044import net.bytebuddy.pool.TypePool;
045
046import static java.util.Objects.requireNonNull;
047
048import static net.bytebuddy.description.modifier.Ownership.STATIC;
049import static net.bytebuddy.description.modifier.SyntheticState.SYNTHETIC;
050import static net.bytebuddy.description.modifier.Visibility.PRIVATE;
051import static net.bytebuddy.description.modifier.Visibility.PUBLIC;
052
053import static net.bytebuddy.description.type.TypeDescription.Generic.Builder.parameterizedType;
054import static net.bytebuddy.description.type.TypeDescription.Generic.Builder.typeVariable;
055
056import static net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy.Default.NO_CONSTRUCTORS;
057
058import static net.bytebuddy.implementation.MethodCall.invoke;
059import static net.bytebuddy.implementation.MethodCall.invokeSelf;
060
061import static net.bytebuddy.matcher.ElementMatchers.any;
062import static net.bytebuddy.matcher.ElementMatchers.hasParameters;
063import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
064import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy;
065import static net.bytebuddy.matcher.ElementMatchers.isEquals;
066import static net.bytebuddy.matcher.ElementMatchers.isFinal;
067import static net.bytebuddy.matcher.ElementMatchers.isHashCode;
068import static net.bytebuddy.matcher.ElementMatchers.isPackagePrivate;
069import static net.bytebuddy.matcher.ElementMatchers.isPublic;
070import static net.bytebuddy.matcher.ElementMatchers.isToString;
071import static net.bytebuddy.matcher.ElementMatchers.isVirtual;
072import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
073import static net.bytebuddy.matcher.ElementMatchers.named;
074import static net.bytebuddy.matcher.ElementMatchers.not;
075import static net.bytebuddy.matcher.ElementMatchers.returns;
076import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
077import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;
078
079/**
080 * A class generator that uses <a href="https://bytebuddy.net/#/">Byte Buddy</a> to {@linkplain #generate(String,
081 * TypeDefinition, Collection) generate} {@linkplain org.microbean.proxy.Proxy client proxy} classes.
082 *
083 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
084 */
085public final class BBClientProxyClassGenerator {
086
087  private final TypePool typePool;
088
089  /**
090   * Creates a new {@link BBClientProxyClassGenerator}.
091   *
092   * @param typePool a {@link TypePool} (normally a {@link TypeElementTypePool}); must not be {@code null}
093   *
094   * @exception NullPointerException if {@code typePool} is {@code null}
095   */
096  public BBClientProxyClassGenerator(final TypePool typePool) {
097    super();
098    this.typePool = requireNonNull(typePool, "typePool");
099  }
100
101  /**
102   * Creates and returns a new {@link DynamicType.Unloaded} representing a client proxy class.
103   *
104   * @param name the name of the client proxy class; must not be {@code null}; must be a valid Java class <a
105   * href="https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/ClassLoader.html#binary-name">binary
106   * name</a>
107   *
108   * @param superclass a {@link TypeDefinition} representing a superclass; must not be {@code null}
109   *
110   * @param interfaces a {@link Collection} of {@link TypeDefinition}s representing interfaces the client proxy class
111   * will implement; must not be {@code null}
112   *
113   * @return a new, non-{@code null} {@link DynamicType.Unloaded} representing a client proxy class
114   *
115   * @exception NullPointerException if any argument is {@code null}
116   */
117  public final DynamicType.Unloaded<?> generate(final String name,
118                                                final TypeDefinition superclass,
119                                                final Collection<? extends TypeDefinition> interfaces) {
120
121    // Proxy<Superclass>
122    final TypeDescription.Generic proxyType =
123      parameterizedType(this.typeDescription("org.microbean.proxy.Proxy"),
124                        List.of(superclass))
125      .build();
126
127    // Supplier<? extends Superclass>
128    final TypeDescription.Generic supplierType =
129      parameterizedType(this.typeDescription("java.util.function.Supplier"),
130                        List.of(TypeDescription.Generic.Builder.of(superclass.asGenericType()).asWildcardUpperBound()))
131      .build();
132
133    // public /* synthetic */ final class Name extends Superclass implements Proxy<Superclass>, Interfaces { /* ... */ }
134    DynamicType.Builder<?> builder = new ByteBuddy()
135      .subclass(superclass, NO_CONSTRUCTORS)
136      .merge(List.of(PUBLIC, SYNTHETIC, TypeManifestation.FINAL))
137      .name(name)
138      .implement(proxyType)
139      .implement(interfaces)
140
141      // private final Supplier<? extends Superclass> $proxiedSupplier;
142      .defineField("$proxiedSupplier", supplierType, PRIVATE, SYNTHETIC, FieldManifestation.FINAL)
143
144      // public Name(final Supplier<? extends Superclass> proxiedSupplier) {
145      //   super();
146      //   Objects.requireNonNull(proxiedSupplier, "proxiedSupplier");
147      //   this.$proxiedSupplier = proxiedSupplier;
148      // }
149      .defineConstructor(PUBLIC, SYNTHETIC)
150      .withParameter(supplierType, "proxiedSupplier", ParameterManifestation.FINAL)
151      .intercept(invoke(superclass.getDeclaredMethods().filter(isConstructor().and(takesNoArguments())).getOnly())
152                 .andThen(invoke(this.typeDescription("java.util.Objects")
153                                 .getDeclaredMethods()
154                                 .filter(named("requireNonNull")
155                                         .and(takesArgument(1, this.typeDescription("java.lang.String"))))
156                                 .getOnly())
157                          .withArgument(0)
158                          .with("proxiedSupplier"))
159                 .andThen(FieldAccessor.ofField("$proxiedSupplier").setsArgumentAt(0)))
160
161      // @Override // Proxy<Superclass>
162      // public final Superclass $proxied() {
163      //   return this.$proxiedSupplier.get();
164      // }
165      .defineMethod("$proxied", superclass, PUBLIC, SYNTHETIC, MethodManifestation.FINAL)
166      .intercept(invoke(named("get"))
167                 .onField("$proxiedSupplier")
168                 .withAssigner(Assigner.DEFAULT, Assigner.Typing.DYNAMIC))
169
170      // @Override // Proxy<Superclass>
171      // public final Superclass $cast() {
172      //   return Proxy.super.$cast();
173      // }
174      .defineMethod("$cast", superclass, PUBLIC, SYNTHETIC, MethodManifestation.FINAL)
175      .intercept(DefaultMethodCall.prioritize(proxyType.asErasure()))
176
177      // Existing/inherited methods; remember that they form a stack, so the last .method() call below should be the
178      // most specific. See https://bytebuddy.net/#members for details.
179
180      // @Override // Superclass/interfaces
181      // public Bar foo() {
182      //   return $proxied().foo(); // so long as foo() meets certain requirements
183      // }
184      .method(isBusinessMethod()
185              .and(not(isJavaDeclaredMethod()
186                       .and(isPackagePrivate()
187                            .or(hasOneOrMorePackagePrivateParameters())))))
188      .intercept(invokeSelf()
189                 .onMethodCall(invoke(named("$proxied")))
190                 .withAllArguments())
191
192      // @Override // Superclass, Object
193      // public final boolean equals(final Object other) {
194      //   if (other == this) {
195      //     return true;
196      //   } else if (other != null && other.getClass() == this.getClass()) {
197      //     return this.$proxiedSupplier == ((Name)other).$proxiedSupplier;
198      //   } else {
199      //     return false;
200      //   }
201      // }
202      .method(isEquals())
203      .intercept(EqualsMethod.isolated()
204                 .withIdentityFields(any()) // there's only one
205                 .withNonNullableFields(any()))
206
207      // @Override // Superclass, Object
208      // public int hashCode() {
209      //   int offset = 31;
210      //   return offset * 17 + this.$proxiedSupplier.hashCode(); // or similar
211      // }
212      .method(isHashCode())
213      .intercept(HashCodeMethod.usingOffset(31)
214                 .withIdentityFields(any())
215                 .withNonNullableFields(any())
216                 .withMultiplier(17)) // see https://github.com/raphw/byte-buddy/issues/1764
217
218      // @Override // Superclass/interfaces/Object
219      // public String toString() {
220      //   return $proxied().toString();
221      // }
222      .method(isToString())
223      .intercept(invoke(named("toString"))
224                 .onMethodCall(invoke(named("$proxied"))));
225
226    return builder.make(this.typePool);
227  }
228
229
230  /*
231   * Static methods.
232   */
233
234
235  private static final ElementMatcher<MethodDescription> hasOneOrMorePackagePrivateParameters() {
236    return m -> {
237      for (final ParameterDescription pd : m.getParameters()) {
238        if (isPackagePrivate().matches(pd.getType())) {
239          return true;
240        }
241      }
242      return false;
243    };
244  }
245
246  private static final ElementMatcher.Junction<MethodDescription> isBusinessMethod() {
247    return isVirtual()
248      .and(not(isFinal()))
249      .and(not(isDeclaredBy(Object.class)));
250  }
251
252  private static final ElementMatcher.Junction<MethodDescription> isJavaDeclaredMethod() {
253    return isDeclaredBy(t -> t.getTypeName().startsWith("java."));
254  }
255
256  private final TypeDescription typeDescription(final String canonicalName) {
257    return this.typePool.describe(canonicalName).resolve();
258  }
259
260}