ExtensionBuilder.java
/*
* JavaBean Tester (https://github.com/hazendaz/javabean-tester)
*
* Copyright 2012-2025 Hazendaz.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of The Apache Software License,
* Version 2.0 which accompanies this distribution, and is available at
* http://www.apache.org/licenses/LICENSE-2.0.txt
*
* Contributors:
* CodeBox (Rob Dawson).
* Hazendaz (Jeremy Landis).
*/
package com.codebox.builders;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.NotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ExtensionBuilder dynamically generates a subclass of the given class with additional String properties and their
* getters/setters.
* <p>
* Uses Javassist to create a new class at runtime named <code>clazz.getName() + "Extension"</code>. The generated class
* will have four String properties: jbExtension1, jbExtension2, jbExtension3, jbExtension4.
*
* @param <T>
* the type to extend
*/
public class ExtensionBuilder<T> {
/** The Constant logger. */
private static final Logger logger = LoggerFactory.getLogger(ExtensionBuilder.class);
/**
* Generates a dynamic subclass of the given class with additional String properties and their getters/setters.
*
* @param clazz
* the class to extend
* @return the generated class
* @throws NotFoundException
* if the class cannot be found in the class pool
* @throws CannotCompileException
* if the class cannot be compiled
*/
public Class<?> generate(final Class<T> clazz) throws NotFoundException, CannotCompileException {
try {
// If extension already recreated, return it
return Class.forName(clazz.getName() + "Extension");
} catch (final ClassNotFoundException e) {
// No extension exists, so create it
ExtensionBuilder.logger.trace("No extension exists, so create it", e);
}
final ClassPool pool = ClassPool.getDefault();
final CtClass cc = pool.makeClass(clazz.getName() + "Extension");
// add super class
cc.setSuperclass(ExtensionBuilder.resolveCtClass(clazz));
final Map<String, Class<?>> properties = new HashMap<>();
properties.put("jbExtension1", String.class);
properties.put("jbExtension2", String.class);
properties.put("jbExtension3", String.class);
properties.put("jbExtension4", String.class);
for (final Entry<String, Class<?>> entry : properties.entrySet()) {
// Add field
cc.addField(new CtField(ExtensionBuilder.resolveCtClass(entry.getValue()), entry.getKey(), cc));
// Add getter
cc.addMethod(ExtensionBuilder.generateGetter(cc, entry.getKey(), entry.getValue()));
// Add setter
cc.addMethod(ExtensionBuilder.generateSetter(cc, entry.getKey(), entry.getValue()));
}
return cc.toClass();
}
/**
* Generates a getter method for the specified field.
*
* @param declaringClass
* the class to add the method to
* @param fieldName
* the name of the field
* @param fieldClass
* the type of the field
* @return the generated getter method
* @throws CannotCompileException
* if the method cannot be compiled
*/
private static CtMethod generateGetter(final CtClass declaringClass, final String fieldName,
final Class<?> fieldClass) throws CannotCompileException {
String methodSrc = """
public %s get%s() {
return this.%s;
}
""".formatted(fieldClass.getName(), fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1),
fieldName);
return CtMethod.make(methodSrc, declaringClass);
}
/**
* Generates a setter method for the specified field.
*
* @param declaringClass
* the class to add the method to
* @param fieldName
* the name of the field
* @param fieldClass
* the type of the field
* @return the generated setter method
* @throws CannotCompileException
* if the method cannot be compiled
*/
private static CtMethod generateSetter(final CtClass declaringClass, final String fieldName,
final Class<?> fieldClass) throws CannotCompileException {
String methodSrc = """
public void set%s(%s %s) {
this.%s = %s;
}
""".formatted(fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1), fieldClass.getName(),
fieldName, fieldName, fieldName);
return CtMethod.make(methodSrc, declaringClass);
}
/**
* Resolves a CtClass from a Java Class using the default class pool.
*
* @param clazz
* the Java class to resolve
* @return the corresponding CtClass
* @throws NotFoundException
* if the class cannot be found in the class pool
*/
private static CtClass resolveCtClass(final Class<?> clazz) throws NotFoundException {
return ClassPool.getDefault().get(clazz.getName());
}
}