View Javadoc
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 }