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