View Javadoc
1   /*
2    * SPDX-License-Identifier: Apache-2.0
3    * See LICENSE file for details.
4    *
5    * Copyright 2012-2026 hazendaz
6    *
7    * Portions of initial baseline code (getter/setter test) by Rob Dawson (CodeBox)
8    */
9   package com.codebox.bean;
10  
11  import com.codebox.util.LombokBuilderUtil;
12  
13  import java.lang.reflect.Modifier;
14  
15  import net.bytebuddy.ByteBuddy;
16  import net.bytebuddy.NamingStrategy;
17  import net.bytebuddy.description.NamedElement;
18  import net.bytebuddy.description.modifier.Visibility;
19  import net.bytebuddy.description.type.TypeDescription;
20  import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
21  import net.bytebuddy.implementation.HashCodeMethod;
22  import net.bytebuddy.implementation.MethodDelegation;
23  import net.bytebuddy.implementation.ToStringMethod;
24  import net.bytebuddy.implementation.bind.annotation.Argument;
25  import net.bytebuddy.implementation.bind.annotation.This;
26  import net.bytebuddy.matcher.ElementMatchers;
27  
28  /**
29   * Entry point for configuring tests of JavaBean-style value objects.
30   *
31   * <p>
32   * Start with one of the {@code builder(...)} factory methods, enable any optional checks, and finish with a terminal
33   * method on {@link JavaBeanTesterBuilder}.
34   *
35   * <pre>
36   * JavaBeanTester.builder(SampleBean.class).checkEquals().loadData().test();
37   * </pre>
38   */
39  public enum JavaBeanTester {
40  
41      // Private Usage
42      ;
43  
44      /**
45       * Creates a builder for the supplied class using the default extension strategy.
46       *
47       * <p>
48       * This overload is the preferred entry point. Final classes fall back to {@link Object} for extension comparisons,
49       * Lombok builder classes skip generated extensions, and other classes receive a generated Byte Buddy extension when
50       * equality-related checks need one.
51       *
52       * @param <T>
53       *            the type under test
54       * @param clazz
55       *            the class under test
56       *
57       * @return a builder for configuring and running bean tests
58       */
59      public static <T> JavaBeanTesterBuilder<T, ?> builder(final Class<T> clazz) {
60          // If class is final, use Object.class for comparison needs
61          if (Modifier.isFinal(clazz.getModifiers())) {
62              return new JavaBeanTesterBuilder<>(clazz, Object.class);
63          }
64  
65          // Lombok builder not supported by byte buddy, do not attempt to build extension class
66          if (LombokBuilderUtil.getLombokBuilderMethod(clazz) != null) {
67              return builder(clazz, null);
68          }
69  
70          // Build extension from class using byte buddy
71          Class<? extends T> loaded = new ByteBuddy().with(new NamingStrategy.AbstractBase() {
72              @Override
73              protected String name(TypeDescription superClass) {
74                  return clazz.getPackageName() + ".ByteBuddyExt" + superClass.getSimpleName();
75              }
76          }).subclass(clazz).method(ElementMatchers.isEquals())
77                  .intercept(MethodDelegation.to(InstanceOfEqualsInterceptor.class)).method(ElementMatchers.isHashCode())
78                  .intercept(HashCodeMethod.usingSuperClassOffset()).method(ElementMatchers.isToString())
79                  .intercept(ToStringMethod.prefixedBySimpleClassName()).method(ElementMatchers.named("canEqual"))
80                  .intercept(MethodDelegation.to(CanEqualInterceptor.class))
81                  .defineField("javabeanExtension", String.class, Visibility.PACKAGE_PRIVATE).make()
82                  .load(clazz.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER).getLoaded();
83  
84          // Builder with proper extension class
85          return builder(clazz, loaded);
86      }
87  
88      /**
89       * Creates a builder with an explicitly supplied extension type.
90       *
91       * <p>
92       * Pass {@code null} to disable extension generation entirely, or provide a custom extension type when equality
93       * verification should use a known subclass instead of the generated one.
94       *
95       * @param <T>
96       *            the type under test
97       * @param <E>
98       *            the extension type used for equality-related checks
99       * @param clazz
100      *            the class under test
101      * @param extension
102      *            the extension type to use, or {@code null} to disable extension generation
103      *
104      * @return a builder for configuring and running bean tests
105      */
106     public static <T, E> JavaBeanTesterBuilder<T, E> builder(final Class<T> clazz, final Class<E> extension) {
107         return new JavaBeanTesterBuilder<>(clazz, extension);
108     }
109 
110     /**
111      * The Class CanEqualInterceptor.
112      */
113     public static final class CanEqualInterceptor {
114 
115         /**
116          * Prevents instantiation a new can equal interceptor.
117          */
118         CanEqualInterceptor() {
119             // Private constructor to prevent instantiation
120         }
121 
122         /**
123          * Can Equal Interceptor.
124          *
125          * @param object
126          *            The object to check can equals
127          * @return boolean of can equals
128          */
129         public static boolean canEqual(final Object object) {
130             return object instanceof NamedElement.WithRuntimeName;
131         }
132 
133     }
134 
135     /**
136      * The Class InstanceOfEqualsInterceptor.
137      */
138     public static final class InstanceOfEqualsInterceptor {
139 
140         /**
141          * Prevents instantiation a new instance of equals interceptor.
142          */
143         InstanceOfEqualsInterceptor() {
144             // Private constructor to prevent instantiation
145         }
146 
147         /**
148          * Equals.
149          *
150          * @param self
151          *            the self
152          * @param other
153          *            the other
154          * @return true, if successful
155          */
156         public static boolean equals(@This Object self, @Argument(0) Object other) {
157             if (self == other) {
158                 return true;
159             }
160             if ((other == null) || (!self.getClass().isInstance(other) && !other.getClass().isInstance(self))) {
161                 return false;
162             }
163             return self.hashCode() == other.hashCode();
164         }
165     }
166 
167 }