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 }