1 /*
2 * JavaBean Tester (https://github.com/hazendaz/javabean-tester)
3 *
4 * Copyright 2012-2025 Hazendaz.
5 *
6 * All rights reserved. This program and the accompanying materials
7 * are made available under the terms of The Apache Software License,
8 * Version 2.0 which accompanies this distribution, and is available at
9 * http://www.apache.org/licenses/LICENSE-2.0.txt
10 *
11 * Contributors:
12 * CodeBox (Rob Dawson).
13 * Hazendaz (Jeremy Landis).
14 */
15 package com.codebox.builders;
16
17 import java.util.HashMap;
18 import java.util.Map;
19 import java.util.Map.Entry;
20 import javassist.CannotCompileException;
21 import javassist.ClassPool;
22 import javassist.CtClass;
23 import javassist.CtField;
24 import javassist.CtMethod;
25 import javassist.NotFoundException;
26
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 /**
31 * ExtensionBuilder dynamically generates a subclass of the given class with additional String properties and their
32 * getters/setters.
33 * <p>
34 * Uses Javassist to create a new class at runtime named <code>clazz.getName() + "Extension"</code>. The generated class
35 * will have four String properties: jbExtension1, jbExtension2, jbExtension3, jbExtension4.
36 *
37 * @param <T>
38 * the type to extend
39 */
40 public class ExtensionBuilder<T> {
41
42 /** The Constant logger. */
43 private static final Logger logger = LoggerFactory.getLogger(ExtensionBuilder.class);
44
45 /**
46 * Generates a dynamic subclass of the given class with additional String properties and their getters/setters.
47 *
48 * @param clazz
49 * the class to extend
50 * @return the generated class
51 * @throws NotFoundException
52 * if the class cannot be found in the class pool
53 * @throws CannotCompileException
54 * if the class cannot be compiled
55 */
56 public Class<?> generate(final Class<T> clazz) throws NotFoundException, CannotCompileException {
57 try {
58 // If extension already recreated, return it
59 return Class.forName(clazz.getName() + "Extension");
60 } catch (final ClassNotFoundException e) {
61 // No extension exists, so create it
62 ExtensionBuilder.logger.trace("No extension exists, so create it", e);
63 }
64
65 final ClassPool pool = ClassPool.getDefault();
66 final CtClass cc = pool.makeClass(clazz.getName() + "Extension");
67
68 // add super class
69 cc.setSuperclass(ExtensionBuilder.resolveCtClass(clazz));
70
71 final Map<String, Class<?>> properties = new HashMap<>();
72 properties.put("jbExtension1", String.class);
73 properties.put("jbExtension2", String.class);
74 properties.put("jbExtension3", String.class);
75 properties.put("jbExtension4", String.class);
76
77 for (final Entry<String, Class<?>> entry : properties.entrySet()) {
78
79 // Add field
80 cc.addField(new CtField(ExtensionBuilder.resolveCtClass(entry.getValue()), entry.getKey(), cc));
81
82 // Add getter
83 cc.addMethod(ExtensionBuilder.generateGetter(cc, entry.getKey(), entry.getValue()));
84
85 // Add setter
86 cc.addMethod(ExtensionBuilder.generateSetter(cc, entry.getKey(), entry.getValue()));
87 }
88
89 return cc.toClass();
90 }
91
92 /**
93 * Generates a getter method for the specified field.
94 *
95 * @param declaringClass
96 * the class to add the method to
97 * @param fieldName
98 * the name of the field
99 * @param fieldClass
100 * the type of the field
101 * @return the generated getter method
102 * @throws CannotCompileException
103 * if the method cannot be compiled
104 */
105 private static CtMethod generateGetter(final CtClass declaringClass, final String fieldName,
106 final Class<?> fieldClass) throws CannotCompileException {
107 String methodSrc = """
108 public %s get%s() {
109 return this.%s;
110 }
111 """.formatted(fieldClass.getName(), fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1),
112 fieldName);
113 return CtMethod.make(methodSrc, declaringClass);
114 }
115
116 /**
117 * Generates a setter method for the specified field.
118 *
119 * @param declaringClass
120 * the class to add the method to
121 * @param fieldName
122 * the name of the field
123 * @param fieldClass
124 * the type of the field
125 * @return the generated setter method
126 * @throws CannotCompileException
127 * if the method cannot be compiled
128 */
129 private static CtMethod generateSetter(final CtClass declaringClass, final String fieldName,
130 final Class<?> fieldClass) throws CannotCompileException {
131 String methodSrc = """
132 public void set%s(%s %s) {
133 this.%s = %s;
134 }
135 """.formatted(fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1), fieldClass.getName(),
136 fieldName, fieldName, fieldName);
137 return CtMethod.make(methodSrc, declaringClass);
138 }
139
140 /**
141 * Resolves a CtClass from a Java Class using the default class pool.
142 *
143 * @param clazz
144 * the Java class to resolve
145 * @return the corresponding CtClass
146 * @throws NotFoundException
147 * if the class cannot be found in the class pool
148 */
149 private static CtClass resolveCtClass(final Class<?> clazz) throws NotFoundException {
150 return ClassPool.getDefault().get(clazz.getName());
151 }
152
153 }