001/* 002 * This file is copyright Bitronix Software. 003 * 004 * Bitronix Transaction Manager 005 * 006 * https://github.com/brettwooldridge/bitronix-hp/blob/master/btm/src/main/java/bitronix/tm/utils/PropertyUtils.java 007 * 008 * Copyright (c) 2010, Bitronix Software. 009 * 010 * This copyrighted material is made available to anyone wishing to use, modify, 011 * copy, or redistribute it subject to the terms and conditions of the GNU 012 * Lesser General Public License, as published by the Free Software Foundation. 013 * 014 * This program is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 017 * for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public License 020 * along with this distribution; if not, write to: 021 * Free Software Foundation, Inc. 022 * 51 Franklin Street, Fifth Floor 023 * Boston, MA 02110-1301 USA 024 */ 025 026package io.stallion.reflection; 027 028import io.stallion.dataAccess.MappedModel; 029import io.stallion.plugins.javascript.BaseJavascriptModel; 030import io.stallion.utils.GeneralUtils; 031import org.apache.commons.lang3.StringUtils; 032 033import static io.stallion.utils.Literals.*; 034 035import java.lang.annotation.Annotation; 036import java.lang.reflect.InvocationTargetException; 037import java.lang.reflect.Method; 038import java.lang.reflect.Modifier; 039import java.time.Instant; 040import java.time.ZonedDateTime; 041import java.util.*; 042 043/** 044 * Utility class for getting and setting bean properties via reflection. 045 * 046 */ 047public final class PropertyUtils { 048 049 private static HashMap<String, Object> lookupCache = new HashMap<>(); 050 051 private PropertyUtils() { 052 } 053 054 public static void resetCache() { 055 lookupCache = new HashMap<>(); 056 } 057 058 /** 059 * Set a direct or indirect property (dotted property: prop1.prop2.prop3) on the target object. This method tries 060 * to be smart in the way that intermediate properties currently set to null are set if it is possible to create 061 * and set an object. Conversions from propertyValue to the proper destination type are performed when possible. 062 * @param target the target object on which to set the property. 063 * @param propertyName the name of the property to set. 064 * @param propertyValue the value of the property to set. 065 * @throws PropertyException if an error happened while trying to set the property. 066 */ 067 public static void setProperty(Object target, String propertyName, Object propertyValue) throws PropertyException { 068 String[] propertyNames = propertyName.split("\\."); 069 070 StringBuffer visitedPropertyName = new StringBuffer(); 071 Object currentTarget = target; 072 int i = 0; 073 while (i < propertyNames.length -1) { 074 String name = propertyNames[i]; 075 Object result = callGetter(currentTarget, name); 076 if (result == null) { 077 // try to instanciate the object & set it in place 078 Class propertyType = getPropertyType(target, name); 079 try { 080 result = propertyType.newInstance(); 081 } catch (InstantiationException ex) { 082 throw new PropertyException("cannot set property '" + propertyName + "' - '" + name + "' is null and cannot be auto-filled", ex); 083 } catch (IllegalAccessException ex) { 084 throw new PropertyException("cannot set property '" + propertyName + "' - '" + name + "' is null and cannot be auto-filled", ex); 085 } 086 callSetter(currentTarget, name, result); 087 } 088 089 currentTarget = result; 090 visitedPropertyName.append(name); 091 visitedPropertyName.append('.'); 092 i++; 093 094 // if it's a Properties object -> the non-visited part of the key should be used 095 // as this Properties' object key so stop iterating over the dotted properties. 096 if (currentTarget instanceof Properties) 097 break; 098 } 099 100 String lastPropertyName = propertyName.substring(visitedPropertyName.length(), propertyName.length()); 101 if (currentTarget instanceof Properties) { 102 Properties p = (Properties) currentTarget; 103 p.setProperty(lastPropertyName, propertyValue.toString()); 104 105 } else { 106 setDirectProperty(currentTarget, lastPropertyName, propertyValue); 107 } 108 } 109 110 /** 111 * Build a map of direct javabeans properties of the target object. Only read/write properties (ie: those who have 112 * both a getter and a setter) are returned. 113 * @param target the target object from which to get properties names. 114 * @return a Map of String with properties names as key and their values 115 * @throws PropertyException if an error happened while trying to get a property. 116 */ 117 public static Map<String, Object> getProperties(Object target, Class<? extends Annotation>...excludeAnnotations) throws PropertyException { 118 Map<String, Object> properties = new HashMap<String, Object>(); 119 Class clazz = target.getClass(); 120 Method[] methods = clazz.getMethods(); 121 for (int i = 0; i < methods.length; i++) { 122 Method method = methods[i]; 123 String name = method.getName(); 124 Boolean hasExcludeAnno = false; 125 if (excludeAnnotations.length > 0) { 126 for(Class<? extends Annotation> anno: excludeAnnotations) { 127 if (method.isAnnotationPresent(anno)) { 128 hasExcludeAnno = true; 129 } 130 } 131 } 132 if (hasExcludeAnno) { 133 continue; 134 } 135 if (method.getModifiers() == Modifier.PUBLIC && method.getParameterTypes().length == 0 && (name.startsWith("get") || name.startsWith("is")) 136 && containsSetterForGetter(clazz, method)) { 137 String propertyName; 138 if (name.startsWith("get")) 139 propertyName = Character.toLowerCase(name.charAt(3)) + name.substring(4); 140 else if (name.startsWith("is")) 141 propertyName = Character.toLowerCase(name.charAt(2)) + name.substring(3); 142 else 143 throw new PropertyException("method '" + name + "' is not a getter, thereof no setter can be found"); 144 145 try { 146 Object propertyValue = method.invoke(target, (Object[]) null); // casting to (Object[]) b/c of javac 1.5 warning 147 if (propertyValue != null && propertyValue instanceof Properties) { 148 Map propertiesContent = getNestedProperties(propertyName, (Properties) propertyValue); 149 properties.putAll(propertiesContent); 150 } 151 else { 152 properties.put(propertyName, propertyValue); 153 } 154 } catch (IllegalAccessException ex) { 155 throw new PropertyException("cannot set property '" + propertyName + "' - '" + name + "' is null and cannot be auto-filled", ex); 156 } catch (InvocationTargetException ex) { 157 throw new PropertyException("cannot set property '" + propertyName + "' - '" + name + "' is null and cannot be auto-filled", ex); 158 } 159 160 } // if 161 } // for 162 163 return properties; 164 } 165 166 public static List<String> getPropertyNames(Class clazz) throws PropertyException { 167 Method[] methods = clazz.getMethods(); 168 List<String> names = list(); 169 for (int i = 0; i < methods.length; i++) { 170 Method method = methods[i]; 171 String name = method.getName(); 172 if (method.getModifiers() == Modifier.PUBLIC && method.getParameterTypes().length == 0 && (name.startsWith("get") || name.startsWith("is")) 173 && containsSetterForGetter(clazz, method)) { 174 String propertyName; 175 if (name.startsWith("get")) 176 propertyName = Character.toLowerCase(name.charAt(3)) + name.substring(4); 177 else 178 propertyName = Character.toLowerCase(name.charAt(2)) + name.substring(3); 179 names.add(propertyName); 180 } 181 } 182 return names; 183 } 184 185 public static boolean propertyHasAnnotation(Class cls, String name, Class<? extends Annotation> anno) { 186 return getAnnotationForProperty(cls, name, anno) != null; 187 } 188 189 public static <T extends Annotation> T getAnnotationForProperty(Class cls, String name, Class<T> anno) { 190 String postFix = name.toUpperCase(); 191 if (name.length() > 1) { 192 postFix = name.substring(0, 1).toUpperCase() + name.substring(1); 193 } 194 Method method = null; 195 try { 196 method = cls.getMethod("get" + postFix); 197 } catch (NoSuchMethodException e) { 198 199 } 200 if (method == null) { 201 try { 202 method = cls.getMethod("is" + postFix); 203 } catch (NoSuchMethodException e) { 204 205 } 206 } 207 if (method == null) { 208 return null; 209 } 210 if (method.getModifiers() != Modifier.PUBLIC) { 211 return null; 212 } 213 if (method.isAnnotationPresent(anno)) { 214 return method.getDeclaredAnnotation(anno); 215 } 216 return null; 217 } 218 219 220 221 222 223 public static Boolean isReadable(Object obj, String name) { 224 String cacheKey = obj.getClass().getCanonicalName() + "|" + name; 225 if (lookupCache.containsKey(cacheKey)) { 226 return (Boolean)lookupCache.get(cacheKey); 227 } 228 229 String postFix = name.toUpperCase(); 230 if (name.length() > 1) { 231 postFix = name.substring(0, 1).toUpperCase() + name.substring(1); 232 } 233 Method method = null; 234 try { 235 method = obj.getClass().getMethod("get" + postFix, (Class<?>[]) null); 236 } catch (NoSuchMethodException e) { 237 238 } 239 if (method == null) { 240 try { 241 method = obj.getClass().getMethod("is" + postFix, (Class<?>[]) null); 242 } catch (NoSuchMethodException e) { 243 244 } 245 } 246 if (method == null) { 247 lookupCache.put(cacheKey, false); 248 return false; 249 } 250 if (method.getModifiers() == Modifier.PUBLIC) { 251 lookupCache.put(cacheKey, true); 252 return true; 253 } 254 lookupCache.put(cacheKey, false); 255 return false; 256 } 257 258 public static Boolean isWriteable(Object target, String propertyName) { 259 if (propertyName == null || "".equals(propertyName)) 260 throw new PropertyException("encountered invalid null or empty property name"); 261 String setterName = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); 262 Method[] methods = target.getClass().getMethods(); 263 for (int i = 0; i < methods.length; i++) { 264 Method method = methods[i]; 265 if (method.getName().equals(setterName) && method.getParameterTypes().length == 1) { 266 return true; 267 } 268 } 269 return false; 270 } 271 272 private static boolean containsSetterForGetter(Class clazz, Method method) { 273 String methodName = method.getName(); 274 String setterName; 275 276 if (methodName.startsWith("get")) 277 setterName = "set" + methodName.substring(3); 278 else if (methodName.startsWith("is")) 279 setterName = "set" + methodName.substring(2); 280 else 281 throw new PropertyException("method '" + methodName + "' is not a getter, thereof no setter can be found"); 282 283 Method[] methods = clazz.getMethods(); 284 for (int i = 0; i < methods.length; i++) { 285 Method method1 = methods[i]; 286 if (method1.getName().equals(setterName)) 287 return true; 288 } 289 return false; 290 } 291 292 /** 293 * Get a direct or indirect property (dotted property: prop1.prop2.prop3) on the target object. 294 * 295 * @param target the target object from which to get the property. 296 * @param propertyName the name of the property to get. 297 * @return the value of the specified property. 298 * @throws PropertyException if an error happened while trying to get the property. 299 */ 300 public static Object getProperty(Object target, String propertyName) throws PropertyException { 301 String[] propertyNames = propertyName.split("\\."); 302 Object currentTarget = target; 303 304 for (int i = 0; i < propertyNames.length; i++) { 305 String name = propertyNames[i]; 306 Object result = callGetter(currentTarget, name); 307 308 if (result == null && i < propertyNames.length -1) 309 throw new PropertyException("cannot get property '" + propertyName + "' - '" + name + "' is null"); 310 currentTarget = result; 311 } 312 313 return currentTarget; 314 } 315 316 public static Object getDotProperty(Object target, String propertyName) throws PropertyException { 317 if (!propertyName.contains(".")) { 318 return getProperty(target, propertyName); 319 } 320 String[] parts = StringUtils.split(propertyName, '.'); 321 Object thing = target; 322 for (String part: parts) { 323 Object oldThing = thing; 324 if (PropertyUtils.isReadable(thing, part)) { 325 thing = PropertyUtils.getProperty(thing, part); 326 } else if (thing instanceof Map) { 327 thing = ((Map)thing).get(part); 328 } else { 329 throw new PropertyException("Cannot read property " + part + " of object " + thing + " of class " + thing.getClass().getName()); 330 } 331 if (thing == null) { 332 return null; 333 } 334 } 335 return thing; 336 } 337 338 public static Object getPropertyOrMappedValue(Object target, String propertyName) throws PropertyException { 339 if (isReadable(target, propertyName)) { 340 return getProperty(target, propertyName); 341 } else if (target instanceof MappedModel) { 342 MappedModel model = (MappedModel)target; 343 return model.get(propertyName); 344 } else { 345 throw new PropertyException("Cannot read property " + propertyName + " of object " + target + " of class " + target.getClass().getName()); 346 } 347 } 348 349 /** 350 * Set a {@link Map} of direct or indirect properties on the target object. 351 * @param target the target object on which to set the properties. 352 * @param properties a {@link Map} of String/Object pairs. 353 * 354 * @throws PropertyException if an error happened while trying to set a property. 355 */ 356 public static void setProperties(Object target, Map properties) throws PropertyException { 357 Iterator it = properties.entrySet().iterator(); 358 while (it.hasNext()) { 359 Map.Entry entry = (Map.Entry) it.next(); 360 String name = (String) entry.getKey(); 361 Object value = entry.getValue(); 362 setProperty(target, name, value); 363 } 364 } 365 366 /** 367 * Return a comma-separated String of r/w properties of the specified object. 368 * @param obj the object to introspect. 369 * @return a a comma-separated String of r/w properties. 370 */ 371 public static String propertiesToString(Object obj) { 372 StringBuffer sb = new StringBuffer(); 373 Map properties = new TreeMap(getProperties(obj)); 374 Iterator it = properties.keySet().iterator(); 375 while (it.hasNext()) { 376 String property = (String) it.next(); 377 Object val = getProperty(obj, property); 378 sb.append(property); 379 sb.append("="); 380 sb.append(val); 381 if (it.hasNext()) 382 sb.append(", "); 383 } 384 return sb.toString(); 385 } 386 387 /** 388 * Set a direct property on the target object. Conversions from propertyValue to the proper destination type 389 * are performed whenever possible. 390 * 391 * @param target the target object on which to set the property. 392 * @param propertyName the name of the property to set. 393 * @param propertyValue the value of the property to set. 394 * @throws PropertyException if an error happened while trying to set the property. 395 */ 396 private static void setDirectProperty(Object target, String propertyName, Object propertyValue) throws PropertyException { 397 Method setter = getSetter(target, propertyName); 398 if (setter == null && !(target instanceof Map)) { 399 throw new PropertyException("no writable setter for '" + propertyName + "' in class '" + target.getClass().getName() + "'"); 400 } 401 Object transformedPropertyValue = propertyValue; 402 if (setter != null && propertyValue != null) { 403 Class parameterType = setter.getParameterTypes()[0]; 404 if (parameterType != null) { 405 transformedPropertyValue = transform(propertyValue, parameterType); 406 } 407 } 408 try { 409 if (propertyValue != null) { 410 if (setter == null) { 411 ((Map)target).put(propertyName, transformedPropertyValue); 412 } else { 413 setter.invoke(target, new Object[]{transformedPropertyValue}); 414 } 415 } else { 416 if (setter == null) { 417 ((Map)target).put(propertyName, transformedPropertyValue); 418 } else { 419 setter.invoke(target, new Object[]{null}); 420 } 421 } 422 } catch (IllegalAccessException ex) { 423 throw new PropertyException("property '" + propertyName + "' is not accessible", ex); 424 } catch (InvocationTargetException ex) { 425 throw new PropertyException("property '" + propertyName + "' access threw an exception", ex); 426 } catch (Exception ex) { 427 String msg = "Error setting property " + target.getClass().getSimpleName() + "." + propertyName + " to value " + transformedPropertyValue; 428 throw new PropertyException(msg, ex); 429 } 430 } 431 432 private static Map getNestedProperties(String prefix, Properties properties) { 433 Map result = new HashMap(); 434 Iterator it = properties.entrySet().iterator(); 435 while (it.hasNext()) { 436 Map.Entry entry = (Map.Entry) it.next(); 437 String name = (String) entry.getKey(); 438 String value = (String) entry.getValue(); 439 result.put(prefix + '.' + name, value); 440 } 441 return result; 442 } 443 444 /** 445 * Try to transform the passed in value into the destinationClass, via a applying a boatload of 446 * heuristics. 447 * 448 * @param value 449 * @param destinationClass 450 * @return 451 */ 452 public static Object transform(Object value, Class destinationClass) { 453 if (value == null) { 454 return null; 455 } 456 if (value.getClass() == destinationClass) 457 return value; 458 if (destinationClass.isInstance(value)) { 459 return value; 460 } 461 462 // If target type is Date and json was a long, convert the long to a date 463 if (destinationClass == Date.class && (value.getClass() == long.class || value.getClass() == Long.class)) { 464 return new Date((long)value); 465 } 466 // Convert integers to longs, if target type is long 467 if ((destinationClass == Long.class || destinationClass == long.class) && (value.getClass() == int.class || value.getClass() == Integer.class)) { 468 return new Long((int)value); 469 } 470 // Convert ints and longs to ZonedDateTime, if ZonedDateTime was a long 471 if (destinationClass == ZonedDateTime.class && ( 472 value.getClass() == long.class || 473 value.getClass() == Long.class || 474 value.getClass() == int.class || 475 value.getClass() == Integer.class 476 ) 477 ) { 478 return ZonedDateTime.ofInstant(Instant.ofEpochMilli((long) value), GeneralUtils.UTC); 479 } 480 if (destinationClass == ZonedDateTime.class && (value.getClass() == double.class || value.getClass() == Double.class)) { 481 return ZonedDateTime.ofInstant(Instant.ofEpochMilli(Math.round((Double)value)), GeneralUtils.UTC); 482 } 483 484 // Convert Strings to Enums, if target type was an enum 485 if (destinationClass.isEnum()) { 486 return Enum.valueOf(destinationClass, value.toString()); 487 } 488 489 if ((destinationClass == boolean.class || destinationClass == Boolean.class)) { 490 if (value instanceof String) { 491 return Boolean.valueOf((String) value); 492 } else if (value instanceof Integer) { 493 return (Integer)value > 0; 494 } else if (value instanceof Long) { 495 return (Long)value > 0; 496 } 497 } 498 499 if ((destinationClass == byte.class || destinationClass == Byte.class) && value.getClass() == String.class) { 500 return new Byte((String) value); 501 } 502 if ((destinationClass == short.class || destinationClass == Short.class) && value.getClass() == String.class) { 503 return new Short((String) value); 504 } 505 if ((destinationClass == int.class || destinationClass == Integer.class) && value.getClass() == String.class) { 506 return new Integer((String) value); 507 } 508 if ((destinationClass == long.class || destinationClass == Long.class) && value.getClass() == String.class) { 509 return new Long((String) value); 510 } 511 if ((destinationClass == float.class || destinationClass == Float.class) && value.getClass() == String.class) { 512 return new Float((String) value); 513 } 514 if ((destinationClass == float.class || destinationClass == Float.class) && value.getClass() == Integer.class) { 515 return ((Integer)value).floatValue(); 516 } 517 if ((destinationClass == float.class || destinationClass == Float.class) && value.getClass() == Long.class) { 518 return ((Long)value).floatValue(); 519 } 520 if ((destinationClass == float.class || destinationClass == Float.class) && value.getClass() == Double.class) { 521 return ((Double)value).floatValue(); 522 } 523 524 525 if ((destinationClass == double.class || destinationClass == Double.class) && value.getClass() == Long.class) { 526 return ((Long)value).floatValue(); 527 } 528 529 530 if ((destinationClass == float.class || destinationClass == Float.class) && value.getClass() == String.class) { 531 return new Float((String) value); 532 } 533 534 if ((destinationClass == double.class || destinationClass == Double.class) && value.getClass() == String.class) { 535 return new Double((String) value); 536 } 537 538 // If the type mis-match is due to boxing, just return the value 539 if ( value.getClass() == boolean.class || value.getClass() == Boolean.class || 540 value.getClass() == byte.class || value.getClass() == Byte.class || 541 value.getClass() == short.class || value.getClass() == Short.class || 542 value.getClass() == int.class || value.getClass() == Integer.class || 543 value.getClass() == long.class || value.getClass() == Long.class || 544 value.getClass() == float.class || value.getClass() == Float.class || 545 value.getClass() == double.class || value.getClass() == Double.class 546 ) 547 return value; 548 549 throw new PropertyException("cannot convert values of type '" + value.getClass().getName() + "' into type '" + destinationClass + "'"); 550 } 551 552 private static void callSetter(Object target, String propertyName, Object parameter) throws PropertyException { 553 Method setter = getSetter(target, propertyName); 554 try { 555 setter.invoke(target, new Object[] {parameter}); 556 } catch (IllegalAccessException ex) { 557 throw new PropertyException("property '" + propertyName + "' is not accessible", ex); 558 } catch (InvocationTargetException ex) { 559 throw new PropertyException("property '" + propertyName + "' access threw an exception", ex); 560 } 561 } 562 563 private static Object callGetter(Object target, String propertyName) throws PropertyException { 564 Method getter = getGetter(target, propertyName); 565 try { 566 return getter.invoke(target, (Object[]) null); // casting to (Object[]) b/c of javac 1.5 warning 567 } catch (IllegalAccessException ex) { 568 throw new PropertyException("property '" + propertyName + "' is not accessible", ex); 569 } catch (InvocationTargetException ex) { 570 throw new PropertyException("property '" + propertyName + "' access threw an exception", ex); 571 } 572 } 573 574 private static Method getSetter(Object target, String propertyName) { 575 if (propertyName == null || "".equals(propertyName)) 576 throw new PropertyException("encountered invalid null or empty property name"); 577 String setterName = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); 578 Method[] methods = target.getClass().getMethods(); 579 for (int i = 0; i < methods.length; i++) { 580 Method method = methods[i]; 581 if (method.getName().equals(setterName) && method.getParameterTypes().length == 1) { 582 return method; 583 } 584 } 585 return null; 586 } 587 588 public static Method getGetter(Object target, String propertyName) { 589 String cacheKey = "getGetter" + "|" + target.getClass().getCanonicalName() + "|" + propertyName; 590 if (target instanceof BaseJavascriptModel) { 591 cacheKey = "getGetter" + "|jsModel" + ((BaseJavascriptModel) target).getBucketName() + "|" + propertyName; 592 } 593 if (lookupCache.containsKey(cacheKey)) { 594 return (Method)lookupCache.get(cacheKey); 595 } 596 String getterName = "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); 597 String getterIsName = "is" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); 598 Method[] methods = target.getClass().getMethods(); 599 for (int i = 0; i < methods.length; i++) { 600 Method method = methods[i]; 601 if ((method.getName().equals(getterName) || method.getName().equals(getterIsName)) && !method.getReturnType().equals(void.class) && method.getParameterTypes().length == 0) { 602 lookupCache.put(cacheKey, method); 603 return method; 604 } 605 } 606 lookupCache.put(cacheKey, null); 607 throw new PropertyException("no readable property '" + propertyName + "' in class '" + target.getClass().getName() + "'"); 608 } 609 610 private static Class getPropertyType(Object target, String propertyName) { 611 String getterName = "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); 612 String getterIsName = "is" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); 613 Method[] methods = target.getClass().getMethods(); 614 for (int i = 0; i < methods.length; i++) { 615 Method method = methods[i]; 616 if ((method.getName().equals(getterName) || method.getName().equals(getterIsName)) && !method.getReturnType().equals(void.class) && method.getParameterTypes().length == 0) { 617 return method.getReturnType(); 618 } 619 } 620 throw new PropertyException("no property '" + propertyName + "' in class '" + target.getClass().getName() + "'"); 621 } 622 623}