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.bean;
10  
11  import java.lang.reflect.Method;
12  import java.util.HashMap;
13  import java.util.Map;
14  
15  /**
16   * The Class ByteBuddyBeanCopier.
17   */
18  public final class ByteBuddyBeanCopier {
19  
20      /**
21       * Instantiates a new byte buddy bean copier.
22       */
23      private ByteBuddyBeanCopier() {
24          throw new AssertionError("Utility class should not be instantiated");
25      }
26  
27      /**
28       * Copy.
29       *
30       * @param source
31       *            the source
32       * @param target
33       *            the target
34       * @param converter
35       *            the converter
36       */
37      public static void copy(Object source, Object target, BooleanConverter converter) {
38          if (source == null || target == null) {
39              return;
40          }
41  
42          Class<?> clazz = source.getClass();
43          Map<String, Method> getters = new HashMap<>();
44          Map<String, Method> setters = new HashMap<>();
45  
46          for (Method method : clazz.getMethods()) {
47              if (ByteBuddyBeanCopier.isGetter(method)) {
48                  String property = ByteBuddyBeanCopier.decapitalize(ByteBuddyBeanCopier.extractPropertyName(method));
49                  getters.put(property, method);
50              } else if (ByteBuddyBeanCopier.isSetter(method)) {
51                  String property = ByteBuddyBeanCopier.decapitalize(method.getName().substring(3));
52                  setters.put(property, method);
53              }
54          }
55  
56          for (Map.Entry<String, Method> entry : setters.entrySet()) {
57              String property = entry.getKey();
58              Method setter = entry.getValue();
59              Method getter = getters.get(property);
60  
61              try {
62                  Object value = null;
63                  if (getter != null) {
64                      value = getter.invoke(source);
65                  } else if (isBooleanType(setter.getParameterTypes()[0])) {
66                      // Try 'is' method for Boolean if getter is missing
67                      String isMethodName = "is" + ByteBuddyBeanCopier.capitalize(property);
68                      try {
69                          Method isMethod = clazz.getMethod(isMethodName);
70                          if (isBooleanType(isMethod.getReturnType())) {
71                              value = isMethod.invoke(source);
72                          }
73                      } catch (NoSuchMethodException ignored) {
74                          // No 'is' method, skip
75                      }
76                  }
77                  if (converter != null && isBooleanType(setter.getParameterTypes()[0])) {
78                      value = converter.convert(value, setter.getParameterTypes()[0]);
79                  }
80                  if (value != null || !setter.getParameterTypes()[0].isPrimitive()) {
81                      setter.invoke(target, value);
82                  }
83              } catch (Exception e) {
84                  throw new RuntimeException("Failed to copy property: " + property, e);
85              }
86          }
87      }
88  
89      /**
90       * Checks if is getter.
91       *
92       * @param method
93       *            the method
94       * @return true, if is getter
95       */
96      private static boolean isGetter(Method method) {
97          if (method.getParameterCount() != 0) {
98              return false;
99          }
100         String name = method.getName();
101         return name.startsWith("get") && !method.getReturnType().equals(void.class) && name.length() > 3
102                 || name.startsWith("is") && isBooleanType(method.getReturnType()) && name.length() > 2;
103     }
104 
105     /**
106      * Checks if is setter.
107      *
108      * @param method
109      *            the method
110      * @return true, if is setter
111      */
112     private static boolean isSetter(Method method) {
113         return method.getName().startsWith("set") && method.getParameterCount() == 1
114                 && method.getReturnType().equals(void.class);
115     }
116 
117     /**
118      * Checks if is boolean type.
119      *
120      * @param type
121      *            the type
122      * @return true, if is boolean type
123      */
124     private static boolean isBooleanType(Class<?> type) {
125         return type == boolean.class || type == Boolean.class;
126     }
127 
128     /**
129      * Extract property name.
130      *
131      * @param getter
132      *            the getter
133      * @return the string
134      */
135     private static String extractPropertyName(Method getter) {
136         String name = getter.getName();
137         if (name.startsWith("get")) {
138             return name.substring(3);
139         }
140         if (name.startsWith("is")) {
141             return name.substring(2);
142         }
143         throw new IllegalArgumentException("Not a getter method: " + name);
144     }
145 
146     /**
147      * Decapitalize.
148      *
149      * @param name
150      *            the name
151      * @return the string
152      */
153     private static String decapitalize(String name) {
154         if (name == null || name.isEmpty()) {
155             return name;
156         }
157         if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))) {
158             return name;
159         }
160         return Character.toLowerCase(name.charAt(0)) + name.substring(1);
161     }
162 
163     /**
164      * Capitalize.
165      *
166      * @param name
167      *            the name
168      * @return the string
169      */
170     private static String capitalize(String name) {
171         if (name == null || name.isEmpty()) {
172             return name;
173         }
174         return Character.toUpperCase(name.charAt(0)) + name.substring(1);
175     }
176 
177     /**
178      * The Interface BooleanConverter.
179      */
180     public interface BooleanConverter {
181 
182         /**
183          * Convert.
184          *
185          * @param value
186          *            the value
187          * @param targetType
188          *            the target type
189          * @return the object
190          */
191         Object convert(Object value, Class<?> targetType);
192     }
193 
194 }