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