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