JavaBeanTester.java
/*
* SPDX-License-Identifier: Apache-2.0
* See LICENSE file for details.
*
* Copyright 2012-2026 hazendaz
*
* Portions of initial baseline code (getter/setter test) by Rob Dawson (CodeBox)
*/
package com.codebox.bean;
import com.codebox.util.LombokBuilderUtil;
import java.lang.reflect.Modifier;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.NamingStrategy;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.HashCodeMethod;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.ToStringMethod;
import net.bytebuddy.implementation.bind.annotation.Argument;
import net.bytebuddy.implementation.bind.annotation.This;
import net.bytebuddy.matcher.ElementMatchers;
/**
* Entry point for configuring tests of JavaBean-style value objects.
*
* <p>
* Start with one of the {@code builder(...)} factory methods, enable any optional checks, and finish with a terminal
* method on {@link JavaBeanTesterBuilder}.
*
* <pre>
* JavaBeanTester.builder(SampleBean.class).checkEquals().loadData().test();
* </pre>
*/
public enum JavaBeanTester {
// Private Usage
;
/**
* Creates a builder for the supplied class using the default extension strategy.
*
* <p>
* This overload is the preferred entry point. Final classes fall back to {@link Object} for extension comparisons,
* Lombok builder classes skip generated extensions, and other classes receive a generated Byte Buddy extension when
* equality-related checks need one.
*
* @param <T>
* the type under test
* @param clazz
* the class under test
*
* @return a builder for configuring and running bean tests
*/
public static <T> JavaBeanTesterBuilder<T, ?> builder(final Class<T> clazz) {
// If class is final, use Object.class for comparison needs
if (Modifier.isFinal(clazz.getModifiers())) {
return new JavaBeanTesterBuilder<>(clazz, Object.class);
}
// Lombok builder not supported by byte buddy, do not attempt to build extension class
if (LombokBuilderUtil.getLombokBuilderMethod(clazz) != null) {
return builder(clazz, null);
}
// Build extension from class using byte buddy
Class<? extends T> loaded = new ByteBuddy().with(new NamingStrategy.AbstractBase() {
@Override
protected String name(TypeDescription superClass) {
return clazz.getPackageName() + ".ByteBuddyExt" + superClass.getSimpleName();
}
}).subclass(clazz).method(ElementMatchers.isEquals())
.intercept(MethodDelegation.to(InstanceOfEqualsInterceptor.class)).method(ElementMatchers.isHashCode())
.intercept(HashCodeMethod.usingSuperClassOffset()).method(ElementMatchers.isToString())
.intercept(ToStringMethod.prefixedBySimpleClassName()).method(ElementMatchers.named("canEqual"))
.intercept(MethodDelegation.to(CanEqualInterceptor.class))
.defineField("javabeanExtension", String.class, Visibility.PACKAGE_PRIVATE).make()
.load(clazz.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER).getLoaded();
// Builder with proper extension class
return builder(clazz, loaded);
}
/**
* Creates a builder with an explicitly supplied extension type.
*
* <p>
* Pass {@code null} to disable extension generation entirely, or provide a custom extension type when equality
* verification should use a known subclass instead of the generated one.
*
* @param <T>
* the type under test
* @param <E>
* the extension type used for equality-related checks
* @param clazz
* the class under test
* @param extension
* the extension type to use, or {@code null} to disable extension generation
*
* @return a builder for configuring and running bean tests
*/
public static <T, E> JavaBeanTesterBuilder<T, E> builder(final Class<T> clazz, final Class<E> extension) {
return new JavaBeanTesterBuilder<>(clazz, extension);
}
/**
* The Class CanEqualInterceptor.
*/
public static final class CanEqualInterceptor {
/**
* Prevents instantiation a new can equal interceptor.
*/
CanEqualInterceptor() {
// Private constructor to prevent instantiation
}
/**
* Can Equal Interceptor.
*
* @param object
* The object to check can equals
* @return boolean of can equals
*/
public static boolean canEqual(final Object object) {
return object instanceof NamedElement.WithRuntimeName;
}
}
/**
* The Class InstanceOfEqualsInterceptor.
*/
public static final class InstanceOfEqualsInterceptor {
/**
* Prevents instantiation a new instance of equals interceptor.
*/
InstanceOfEqualsInterceptor() {
// Private constructor to prevent instantiation
}
/**
* Equals.
*
* @param self
* the self
* @param other
* the other
* @return true, if successful
*/
public static boolean equals(@This Object self, @Argument(0) Object other) {
if (self == other) {
return true;
}
if ((other == null) || (!self.getClass().isInstance(other) && !other.getClass().isInstance(self))) {
return false;
}
return self.hashCode() == other.hashCode();
}
}
}