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