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