001package com.identityworksllc.iiq.common; 002 003import org.apache.commons.logging.Log; 004import org.apache.commons.logging.LogFactory; 005import sailpoint.tools.GeneralException; 006 007import java.lang.annotation.Annotation; 008import java.lang.annotation.ElementType; 009import java.lang.annotation.Retention; 010import java.lang.annotation.RetentionPolicy; 011import java.lang.annotation.Target; 012import java.lang.invoke.MethodHandle; 013import java.lang.invoke.MethodHandles; 014import java.lang.reflect.Field; 015import java.lang.reflect.Method; 016import java.lang.reflect.Modifier; 017import java.util.*; 018import java.util.concurrent.ConcurrentHashMap; 019import java.util.function.Supplier; 020 021/** 022 * A utility to decode a Map structure into the fields of a POJO object. This makes 023 * configurations type-safe and allows code accessing them to be verified. 024 * 025 * While similar to Jackson's own ObjectMapper, this class also natively supports 026 * a variety of SailPoint object types. It is intended for use with _Custom_ and 027 * _Configuration_ objects. 028 * 029 * ---- 030 * 031 * TYPE MAPPINGS: 032 * 033 * This class can coerce the following field types natively: 034 * 035 * - Boolean 036 * - String 037 * - Numeric values 038 * - List of strings 039 * - List of nested objects 040 * - Map from string to nested objects 041 * - Date 042 * 043 * The {@link SailpointObjectMapper} subclass (the default) also handles: 044 * 045 * - IdentitySelector 046 * - Filter 047 * - Script 048 * - A Reference object 049 * - Any SailPointObject type that can be looked up via a string ID or name 050 * 051 * Inaccessible fields and extra Map keys will be safely ignored, but a debug message 052 * will be logged if debug logging is enabled for this class. 053 * ---- 054 * 055 * NESTING and ANNOTATIONS: 056 * 057 * You must use the @Nested annotation to indicate that a field is to be ObjectMapped 058 * itself. If the field is a list of objects or a map, you must specify the type of 059 * objects in the list or the map values as the argument to @Nested. 060 * 061 * \@Nested(SomeType.class) 062 * private Map<String, SomeType> map; 063 * 064 * \@Nested(ListType.class) 065 * private List<ListType> list; 066 * 067 * Maps with key types other than strings are not supported. (TODO) 068 * 069 * If the field's type is itself annotated with @{@link javax.xml.bind.annotation.XmlRootElement} 070 * (from JAXB), it will be assumed to be @Nested automatically. 071 * 072 * Fields annotated with {@link Ignore} will always be unmapped. For example, a field 073 * that you set in the default constructor may be ignored. 074 * 075 * If a class is annotated with {@link IgnoreSuper}, all superclass fields will be 076 * ignored above that class in the hierarchy. This allows you to extend provided 077 * classes without having to worry about the ObjectMapper scanning their fields. 078 * 079 * By default, a field X is set via the broadest method named setX that is compatible 080 * with the field's type. For example, if a field is of type Collection, a setX that 081 * takes a List will be preferred over a setX that takes an ArrayList. You may also 082 * specify a different setter method name using @SetterName on the field. 083 * 084 * ---- 085 * 086 * CACHING: 087 * 088 * If the second parameter to any of the decode() methods is true, a cached copy of 089 * the decoded object will be returned. That is, two calls to decode with an identical 090 * Map input will produce the same (==) object output. 091 * 092 * The default for the Configuration and Custom decode() methods is to true (yes do 093 * use the cache), and for the Map input it is false. 094 * 095 * Cached decoded objects will be retained forever. 096 * 097 * ---- 098 * 099 * SHARING: 100 * 101 * By default, this class has a strong dependency on its subclass, {@link SailpointObjectMapper}, 102 * in that an instance of that class is always returned from {@link #get(Class)}. However, it is 103 * designed so that it can be fairly easily detached with only minimal modifications. It has no 104 * other external dependencies except Apache Commons Logging. 105 * 106 * @param <T> The type of object being mapped 107 */ 108public class ObjectMapper<T> { 109 110 /** 111 * Implementing this interface allows a class to coerce itself from one or more 112 * input types. When converting an object of this type, a new instance will be 113 * constructed using the no-args constructor. Then, {@link #canCoerce(Object)} 114 * will be invoked to verify that the object is supported. If so, the object 115 * will be initialized via {@link #initializeFrom(Object)}. 116 */ 117 public interface Convertible { 118 /** 119 * Invoked by the ObjectMapper to ensure that the given input is appropriate 120 * for coercion 121 * 122 * @param input The input object for testing 123 * @return True if the input can be coerced into this type 124 */ 125 boolean canCoerce(Object input); 126 127 /** 128 * Initializes this object from the given input 129 * @param input The input object 130 */ 131 void initializeFrom(Object input) throws ObjectMapperException; 132 } 133 134 /** 135 * The method annotated with this annotation will be invoked after all mapped attributes 136 * are set. This can be used to initialize the mapping. 137 */ 138 @Retention(RetentionPolicy.RUNTIME) 139 @Target(ElementType.METHOD) 140 public @interface AfterMapperMethod { 141 142 } 143 144 @Retention(RetentionPolicy.RUNTIME) 145 @Target(ElementType.FIELD) 146 public @interface Aliases { 147 String[] value(); 148 } 149 150 /** 151 * Annotation to indicate that the given field should be ignored and 152 * not mapped. 153 */ 154 @Retention(RetentionPolicy.RUNTIME) 155 @Target(ElementType.FIELD) 156 public @interface Ignore { 157 158 } 159 160 /** 161 * Indicates that the annotated class should be the stopping point for 162 * walking up the class hierarchy to find setters. This is important if you 163 * are extending out of box classes you have no control over. 164 */ 165 @Retention(RetentionPolicy.RUNTIME) 166 @Target(ElementType.TYPE) 167 public @interface IgnoreSuper { 168 169 } 170 171 /** 172 * Annotation to indicate that the given element is a nested instance of the 173 * mapped class, such as an identity with a list of identities. 174 */ 175 @Retention(RetentionPolicy.RUNTIME) 176 @Target({ElementType.FIELD}) 177 public @interface Nested { 178 Class<?> value() default Nested.class; 179 } 180 181 @Retention(RetentionPolicy.RUNTIME) 182 @Target(ElementType.FIELD) 183 public @interface RawMap { 184 185 } 186 187 @Retention(RetentionPolicy.RUNTIME) 188 @Target(ElementType.FIELD) 189 public @interface SetterMethod { 190 String value() default ""; 191 } 192 193 /** 194 * A functional interface to translate a Class into its name. This may just 195 * be Class::getName, but may be a more complex behavior as needed. 196 */ 197 @FunctionalInterface 198 public interface TypeNamer { 199 String getTypeName(Class<?> type); 200 } 201 202 /** 203 * The exception type thrown by all mapper methods 204 */ 205 public static class ObjectMapperException extends Exception { 206 public ObjectMapperException() { 207 super(); 208 } 209 210 public ObjectMapperException(String message) { 211 super(message); 212 } 213 214 public ObjectMapperException(String message, Throwable cause) { 215 super(message, cause); 216 } 217 218 public ObjectMapperException(Throwable cause) { 219 super(cause); 220 } 221 } 222 223 /** 224 * Translates from a Class type to a name. By default, just retrieves the name of the class. 225 * Subclasses may extend this behavior if additional behavior is required. For example, 226 * the SailPointTypeNamer also takes plugin classloader refreshes into account. 227 */ 228 protected static class DefaultTypeNamer implements TypeNamer { 229 public String getTypeName(Class<?> type) { 230 return type.getName(); 231 } 232 } 233 234 /** 235 * The type-to-name converter to use, namely the Sailpoint one 236 */ 237 private static final TypeNamer TYPE_NAMER = new SailpointObjectMapper.SailPointTypeNamer(); 238 239 /** 240 * The list of cached mapper objects 241 */ 242 private static final Map<String, ObjectMapper<?>> cachedMappers = new ConcurrentHashMap<>(); 243 244 /** 245 * Gets a predefined mapper for the given type. Mappers will be recalculated when 246 * the plugin cache is updated, allowing mapped classes to be changed at runtime. 247 * 248 * @param type The type of class to map 249 * @param <T> The type parameter 250 * @return A mapper for that type 251 */ 252 @SuppressWarnings("unchecked") 253 public static <T> SailpointObjectMapper<T> get(Class<T> type) { 254 String typeKey = TYPE_NAMER.getTypeName(type); 255 if (!cachedMappers.containsKey(typeKey)) { 256 synchronized(cachedMappers) { 257 if (!cachedMappers.containsKey(typeKey)) { 258 SailpointObjectMapper<T> mapper = new SailpointObjectMapper<>(type); 259 cachedMappers.put(typeKey, mapper); 260 } 261 } 262 } 263 return (SailpointObjectMapper<T>)cachedMappers.get(typeKey); 264 } 265 266 /** 267 * Returns true if the input string is not null or empty. This is a replica of the 268 * IIQ version of this class so that this class can be separated from IIQ code 269 * if needed. 270 * 271 * @param in The input string 272 * @return True if the input string is not null or empty 273 */ 274 public static boolean isNotNullOrEmpty(String in) { 275 return !(in == null || in.trim().isEmpty()); 276 } 277 278 /** 279 * Returns a string representation of the input. If the input is null, returns null. 280 * If the input is a string, returns the input. If the input is not a string, returns 281 * the input as passed through {@link String#valueOf(Object)}. 282 * @param in The input object 283 * @return The input object converted to a string 284 */ 285 public static String otoa(Object in) { 286 if (in == null) { 287 return null; 288 } else if (in instanceof String) { 289 return (String)in; 290 } else { 291 return String.valueOf(in); 292 } 293 } 294 295 /** 296 * Returns a Boolean reprsentation of the object. If the object is a Boolean, 297 * it will be returned as-is. If the object is a String, the result will be the 298 * outcome of {@link Boolean#parseBoolean(String)}. Otherwise, the result is false. 299 * @param in The input object 300 * @return The output as described above 301 */ 302 public static boolean otob(Object in) { 303 if (in == null) { 304 return false; 305 } else if (in instanceof Boolean) { 306 return (Boolean) in; 307 } else if (in instanceof String) { 308 return Boolean.parseBoolean((String)in); 309 } 310 return false; 311 } 312 313 /** 314 * Transforms an arbitrary object into a List. If the input is null, or if none 315 * of the below conditions match, the output is null. If the input is a List, it 316 * is returned as-is. If the input is another Collection, it is copied into a 317 * List and returned. If the input is a String, it is split on commas as a CSV 318 * and returned. 319 * 320 * This method uses {@link CommonConstants#FUNC_CSV_PARSER}, so if you copy it 321 * outside of a Sailpoint environment, you'll want to change that. 322 * 323 * @param in The input object 324 * @return The input converted to a list, or null if it could not be converted 325 */ 326 @SuppressWarnings("unchecked") 327 public static List<String> otol(Object in) { 328 if (in == null) { 329 return null; 330 } else if (in instanceof List) { 331 return (List<String>)in; 332 } else if (in instanceof Collection) { 333 Collection<String> strings = (Collection<String>)in; 334 return new ArrayList<>(strings); 335 } else if (in instanceof String) { 336 String str = (String)in; 337 return CommonConstants.FUNC_CSV_PARSER.apply(str); 338 } 339 return null; 340 } 341 /** 342 * Caches already-seen converted Maps so that we don't have to continually reconvert 343 * the same maps into the same objects. Two subsequent calls to decode() with an identical 344 * map should result in the same (==) object. 345 */ 346 private final ConcurrentHashMap<Integer, T> cachedConfigs; 347 /** 348 * The initializer method (if defined) to invoke after all setup is done 349 */ 350 private MethodHandle initializer; 351 /** 352 * Logger 353 */ 354 private final Log log; 355 /** 356 * The lookup of field names that are nested types, indicated by the @Nested 357 * annotation on the field. 358 */ 359 private final ConcurrentHashMap<String, ObjectMapper<?>> nested; 360 /** 361 * The type to which each field value must be coerced 362 */ 363 private final ConcurrentHashMap<String, Class<?>> setterTypes; 364 /** 365 * The setter method handles, equivalent to either obj.setX(v) or obj.x = v, 366 * depending on what's available at scan time 367 */ 368 private final ConcurrentHashMap<String, MethodHandle> setters; 369 /** 370 * The target class being mapped 371 */ 372 private final Class<T> targetClass; 373 374 /** 375 * Basic constructor. You should prefer {@link ObjectMapper#get(Class)} to this. 376 * @param targetClass the target class 377 */ 378 public ObjectMapper(Class<T> targetClass) { 379 this.targetClass = targetClass; 380 this.cachedConfigs = new ConcurrentHashMap<>(); 381 this.setters = new ConcurrentHashMap<>(); 382 this.setterTypes = new ConcurrentHashMap<>(); 383 this.nested = new ConcurrentHashMap<>(); 384 this.log = LogFactory.getLog(this.getClass()); 385 this.initializer = null; 386 } 387 388 /** 389 * Converts the given object to the expected type. If the input is null, a null 390 * will be returned. If the input is already compatible with the expected type, 391 * the existing object will be returned. If the input cannot be converted, an 392 * exception will be thrown. 393 * 394 * @param value The input value 395 * @param expectedType The expected type of the input 396 * @return The converted object 397 */ 398 public Object convertObject(Object value, Class<?> expectedType) throws ObjectMapperException { 399 if (value == null) { 400 return null; 401 } 402 if (expectedType.isAssignableFrom(value.getClass())) { 403 return value; 404 } 405 if (Convertible.class.isAssignableFrom(expectedType)) { 406 try { 407 Convertible instance = (Convertible) expectedType.getConstructor().newInstance(); 408 if (instance.canCoerce(value)) { 409 instance.initializeFrom(value); 410 return instance; 411 } 412 } catch(Exception e) { 413 throw new ObjectMapperException(e); 414 } 415 } 416 if (expectedType.equals(Boolean.TYPE) || expectedType.equals(Boolean.class)) { 417 value = otob(value); 418 } else if (expectedType.equals(String.class)) { 419 value = otoa(value); 420 } else if (expectedType.equals(Long.TYPE) || expectedType.equals(Long.class)) { 421 value = Long.parseLong(otoa(value)); 422 } else if (expectedType.equals(Integer.TYPE) || expectedType.equals(Integer.class)) { 423 value = Integer.parseInt(otoa(value)); 424 } else if (expectedType.equals(Date.class)) { 425 if (value instanceof Long) { 426 value = new Date((Long) value); 427 } else if (value instanceof java.sql.Date) { 428 value = new Date(((java.sql.Date)value).getTime()); 429 } else { 430 throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + expectedType.getName()); 431 } 432 } else if (Collection.class.isAssignableFrom(expectedType)) { 433 value = otol(value); 434 } else { 435 throw new IllegalArgumentException("Cannot convert " + value.getClass().getName() + " to " + expectedType.getName()); 436 } 437 return value; 438 } 439 440 /** 441 * Decodes the given Map object into an instance of the mapped type. If the introspection 442 * code is not initialized, it will be initialized at this time. 443 * 444 * If a null map is passed, it will be swapped for an empty map. 445 * 446 * Equivalent to decode(map, false) 447 * 448 * @param map The map object to convert 449 * @return An object of the expected type 450 * @throws ObjectMapperException if any failure occur 451 */ 452 public T decode(Map<String, Object> map) throws ObjectMapperException { 453 return decode(map, false); 454 } 455 456 /** 457 * Decodes the given Map object into an instance of the mapped type. If the introspection 458 * code is not initialized, it will be initialized at this time. 459 * 460 * If a null map is passed, it will be swapped for an empty map. 461 * 462 * @param map The map object to convert 463 * @param cache If true, the cached value will be returned if possible 464 * @return An object of the expected type 465 * @throws ObjectMapperException if any failure occur 466 */ 467 public T decode(Map<String, Object> map, boolean cache) throws ObjectMapperException { 468 if (map == null) { 469 map = new HashMap<>(); 470 } 471 initSetters(); 472 T result = cache ? cachedConfigs.get(map.hashCode()) : null; 473 if (result != null) { 474 return result; 475 } 476 try { 477 result = targetClass.newInstance(); 478 } catch(Exception e) { 479 throw new ObjectMapperException(e); 480 } 481 for(String key : map.keySet()) { 482 Object value = map.get(key); 483 if (value != null) { 484 Class<?> expectedType = setterTypes.get(key); 485 if (log.isTraceEnabled()) { 486 log.trace("Decoding value " + value + " into field " + key + " with expected type " + expectedType); 487 } 488 if (expectedType != null) { 489 if (nested.containsKey(key)) { 490 if (log.isTraceEnabled()) { 491 log.trace("Field " + key + " is a nested field with type " + nested.get(key).getTargetClass().getName()); 492 } 493 ObjectMapper<?> nestedMapper = nested.get(key); 494 if (value instanceof Iterable) { 495 value = decodeNestedIterable(key, value, expectedType, nestedMapper); 496 } else if (value instanceof Map) { 497 if (Hashtable.class.isAssignableFrom(expectedType)) { 498 value = decodeNestedMap(value, null, nestedMapper, Hashtable::new); 499 } else if (Map.class.isAssignableFrom(expectedType)) { 500 value = decodeNestedMap(value, expectedType, nestedMapper, null); 501 } else { 502 @SuppressWarnings("unchecked") 503 Map<String, Object> m = (Map<String, Object>) value; 504 value = nestedMapper.decode(m, cache); 505 } 506 } else { 507 throw new IllegalArgumentException("The field " + key + " requires a map or a list of nested maps"); 508 } 509 } else { 510 Object converted = convertObject(value, expectedType); 511 if (converted == null) { 512 throw new IllegalArgumentException("For field " + key + ", could not convert object of type " + value.getClass().getName() + " to type " + expectedType.getName()); 513 } 514 value = converted; 515 } 516 try { 517 MethodHandle mh = setters.get(key); 518 if (mh != null) { 519 Object[] params = new Object[2]; 520 params[0] = result; 521 params[1] = value; 522 mh.invokeWithArguments(params); 523 } 524 } catch (Throwable t) { 525 throw new ObjectMapperException(t); 526 } 527 } else { 528 if (log.isTraceEnabled()) { 529 log.trace("Ignoring unrecognized map key " + key); 530 } 531 } 532 } 533 } 534 535 if (initializer != null) { 536 try { 537 Object[] params = new Object[1]; 538 params[0] = result; 539 initializer.invokeWithArguments(params); 540 } catch(Throwable t) { 541 throw new ObjectMapperException(t); 542 } 543 } 544 545 if (result instanceof MapDecodable) { 546 ((MapDecodable) result).initializeFromMap(map); 547 } 548 549 if (cache) { 550 // Why am I copying this? Why didn't I comment on this when I wrote it? 551 Map<String, Object> copy = new HashMap<>(map); 552 cachedConfigs.put(copy.hashCode(), result); 553 } 554 555 return result; 556 } 557 558 /** 559 * Decodes the input value into an Iterable of the expected type. This method supports 560 * decoding Lists, Sets, and Queues. 561 * 562 * @param key The key, used only for logging purposes 563 * @param value The value to decode 564 * @param expectedType The expected type of the output, such as a List 565 * @param nestedMapper The nested mapper to use to decode values, if any 566 * @return The resulting Iterable object 567 * @throws ObjectMapperException if any failures occur 568 */ 569 private Object decodeNestedIterable(String key, Object value, Class<?> expectedType, ObjectMapper<?> nestedMapper) throws ObjectMapperException { 570 Collection<Object> nested; 571 if (expectedType.equals(Collection.class) || expectedType.equals(Iterable.class) || expectedType.equals(List.class) || expectedType.equals(ArrayList.class) || expectedType.equals(LinkedList.class)) { 572 nested = new ArrayList<>(); 573 } else if (expectedType.equals(Set.class) || expectedType.equals(HashSet.class) || expectedType.equals(TreeSet.class)) { 574 nested = new HashSet<>(); 575 } else if (Collection.class.isAssignableFrom(expectedType)) { 576 try { 577 @SuppressWarnings("unchecked") 578 Collection<Object> exemplar = (Collection<Object>) expectedType.newInstance(); 579 if (exemplar instanceof Queue) { 580 nested = new LinkedList<>(); 581 } else if (exemplar instanceof List) { 582 nested = new ArrayList<>(); 583 } else if (exemplar instanceof Set) { 584 nested = new HashSet<>(); 585 } else { 586 throw new ObjectMapperException("Illegal destination type of a nested mapped list: " + expectedType); 587 } 588 } catch(Exception e) { 589 throw new ObjectMapperException("Non-standard collection type must have a no-args constructor: " + expectedType); 590 } 591 } else { 592 throw new ObjectMapperException("Illegal destination type of a nested mapped list: " + expectedType); 593 } 594 Class<?> newTargetClass = nestedMapper.targetClass; 595 @SuppressWarnings("unchecked") 596 Iterable<Object> input = (Iterable<Object>) value; 597 for (Object member : input) { 598 boolean handled = false; 599 if (member == null) { 600 nested.add(null); 601 } else { 602 if (newTargetClass.isAssignableFrom(member.getClass())) { 603 nested.add(member); 604 handled = true; 605 } else if (Convertible.class.isAssignableFrom(newTargetClass)) { 606 try { 607 Convertible target = (Convertible) newTargetClass.getConstructor().newInstance(); 608 if (target.canCoerce(member)) { 609 target.initializeFrom(member); 610 nested.add(target); 611 handled = true; 612 } 613 } catch (Exception e) { 614 throw new ObjectMapperException(e); 615 } 616 } 617 if (!handled) { 618 if (member instanceof Map) { 619 @SuppressWarnings("unchecked") 620 Map<String, Object> m = (Map<String, Object>) member; 621 Object sub = nestedMapper.decode(m); 622 nested.add(sub); 623 } else { 624 throw new IllegalArgumentException("The field " + key + " requires a nested map"); 625 } 626 } 627 } 628 } 629 630 value = nested; 631 return value; 632 } 633 634 private Object decodeNestedMap(Object value, Class<?> expectedType, ObjectMapper<?> nestedMapper, Supplier<? extends Map<String, Object>> mapCreator) throws ObjectMapperException { 635 if (!(value instanceof Map)) { 636 throw new ObjectMapperException("The value passed to decodeNestedMap() must, in fact, be a Map"); 637 } 638 Map<String, Object> nestedMap = new HashMap<>(); 639 640 if (mapCreator != null) { 641 nestedMap = mapCreator.get(); 642 } else if (expectedType != null) { 643 Class<?>[] mapClasses = new Class[]{ 644 HashMap.class, 645 TreeMap.class 646 }; 647 for (Class<?> possibleClass : mapClasses) { 648 if (expectedType.isAssignableFrom(possibleClass)) { 649 try { 650 nestedMap = (Map<String, Object>) possibleClass.newInstance(); 651 } catch (Exception e) { 652 throw new ObjectMapperException(e); 653 } 654 } 655 } 656 } 657 Class<?> newTargetClass = nestedMapper.targetClass; 658 @SuppressWarnings("unchecked") 659 Map<String, Object> input = (Map<String, Object>) value; 660 for (String nestedKey : input.keySet()) { 661 Object encoded = input.get(nestedKey); 662 if (encoded == null) { 663 continue; 664 } 665 boolean handled = false; 666 if (encoded instanceof Map) { 667 Map<String, Object> encodedMap = (Map<String, Object>)encoded; 668 Object decoded = nestedMapper.decode(encodedMap); 669 nestedMap.put(nestedKey, decoded); 670 handled = true; 671 } else if (Convertible.class.isAssignableFrom(newTargetClass)) { 672 try { 673 Convertible target = (Convertible) newTargetClass.getConstructor().newInstance(); 674 if (target.canCoerce(encoded)) { 675 target.initializeFrom(encoded); 676 nestedMap.put(nestedKey, target); 677 handled = true; 678 } 679 } catch(Exception e) { 680 throw new ObjectMapperException(e); 681 } 682 } else { 683 Object decoded = convertObject(encoded, newTargetClass); 684 nestedMap.put(nestedKey, decoded); 685 handled = true; 686 } 687 if (!handled) { 688 throw new ObjectMapperException("The encoded object of class " + encoded.getClass() + " could not be decoded to target type " + newTargetClass.getName()); 689 } 690 } 691 value = nestedMap; 692 return value; 693 } 694 695 /** 696 * Finds an annotation anywhere in the class hierarchy at or above this type 697 * @param type The type to can 698 * @param annotation The annotation to look for 699 * @return True if the annotation is present on this class or any superclass 700 */ 701 private boolean findAnnotation(Class<?> type, Class<? extends Annotation> annotation) { 702 if (annotation == null || type == null) { 703 return false; 704 } 705 Class<?> cur = type; 706 while(cur.getSuperclass() != null) { 707 if (cur.isAnnotationPresent(annotation)) { 708 return true; 709 } 710 cur = cur.getSuperclass(); 711 } 712 return false; 713 } 714 715 /** 716 * Gets the field names and any of its aliases from the {@link Aliases} annotation 717 * @param fieldName The field name 718 * @param field The field being analyzed 719 * @return The list of names for this field, including any aliases 720 */ 721 private List<String> getNamesWithAliases(String fieldName, Field field) { 722 List<String> names = new ArrayList<>(); 723 names.add(fieldName); 724 725 if (field.isAnnotationPresent(Aliases.class)) { 726 Aliases aliasAnnotation = field.getAnnotation(Aliases.class); 727 if (aliasAnnotation != null && aliasAnnotation.value() != null) { 728 names.addAll(Arrays.asList(aliasAnnotation.value())); 729 } 730 } 731 732 return names; 733 734 } 735 736 /** 737 * Gets the target class 738 * @return The target class 739 */ 740 public Class<T> getTargetClass() { 741 return targetClass; 742 } 743 744 /** 745 * Initializes the setter for the given field, locating the least narrow setter 746 * method with the appropriate name. So, if there are several set methods: 747 * 748 * setField(ArrayList) 749 * setField(List) 750 * setField(Collection) 751 * 752 * The final one, taking a Collection, will be selected here. 753 * 754 * @param setterMap The map to which the setters need to be added 755 * @param lookupUtility The JDK MethodHandle lookup utility 756 * @param field The field being analyzed 757 * @throws NoSuchFieldException if there are any issues getting the field accessor 758 */ 759 private void initSetterForField(Map<String, MethodHandle> setterMap, MethodHandles.Lookup lookupUtility, Field field) throws NoSuchFieldException { 760 String fieldName = field.getName(); 761 try { 762 // Find a legit setter method. We don't care about capitalization 763 String setterName = "set" + field.getName(); 764 if (field.isAnnotationPresent(SetterMethod.class)) { 765 SetterMethod setterAnnotation = field.getAnnotation(SetterMethod.class); 766 if (isNotNullOrEmpty(setterAnnotation.value())) { 767 setterName = setterAnnotation.value(); 768 } 769 } 770 // Use reflection to find the setter method; using MethodType is not as 771 // flexible because we would need to specify a return type. We only care 772 // about the name and the parameter type. We loop over the methods from 773 // 'targetClass' and not 'cls' because we want to call inherited methods 774 // if they are available, rather than the superclass abstract ones. 775 Method leastNarrow = null; 776 for(Method m : targetClass.getMethods()) { 777 if (m.getName().equalsIgnoreCase(setterName)) { 778 if (m.getParameterCount() == 1 && field.getType().isAssignableFrom(m.getParameterTypes()[0])) { 779 if (leastNarrow == null || m.getParameterTypes()[0].equals(field.getType())) { 780 leastNarrow = m; 781 } else { 782 Class<?> existingParamType = leastNarrow.getParameterTypes()[0]; 783 Class<?> newParamType = m.getParameterTypes()[0]; 784 if (!existingParamType.isAssignableFrom(newParamType) && newParamType.isAssignableFrom(existingParamType)) { 785 if (log.isTraceEnabled()) { 786 log.trace("For field " + fieldName + " setter method with param type " + newParamType + " is more general than setter with param type " + existingParamType); 787 } 788 leastNarrow = m; 789 } 790 } 791 } 792 } 793 } 794 if (leastNarrow == null) { 795 // If we can't find a legit setter method, attempt a direct field write 796 if (log.isTraceEnabled()) { 797 log.trace("For field " + fieldName + " could not find setter method " + setterName + ", attempting direct field write access"); 798 } 799 insertDirectFieldWrite(setterMap, lookupUtility, field, fieldName); 800 } else { 801 insertSetterMethodCall(setterMap, lookupUtility, field, fieldName, leastNarrow); 802 } 803 } catch (IllegalAccessException e) { 804 // Quietly log and continue 805 if (log.isDebugEnabled()) { 806 log.debug("For mapped type " + targetClass.getName() + ", no accessible setter or field found for name " + field.getName()); 807 } 808 } 809 } 810 811 /** 812 * Initializes the setters of this class and any of its superclasses in 813 * a synchronized block on the first call to decode(). 814 * 815 * TODO do we want to cache this? 816 * 817 * @throws ObjectMapperException If any failures occur looking up method handles 818 */ 819 private void initSetters() throws ObjectMapperException { 820 if (setters.isEmpty()) { 821 synchronized (setters) { 822 if (setters.isEmpty()) { 823 try { 824 Class<? extends Annotation> rootElemAnnotation = null; 825 try { 826 rootElemAnnotation = (Class<? extends Annotation>) Class.forName("javax.xml.bind.annotation.XmlRootElement"); 827 } catch(Exception ignored) { 828 /* Ignore it */ 829 } 830 Class<?> cls = targetClass; 831 Map<String, MethodHandle> tempSetters = new HashMap<>(); 832 MethodHandles.Lookup lookup = MethodHandles.lookup(); 833 while (cls.getSuperclass() != null) { 834 if (log.isTraceEnabled()) { 835 log.trace("Scanning fields in class " + cls.getName()); 836 } 837 for (Field f : cls.getDeclaredFields()) { 838 initAnalyzeField(rootElemAnnotation, tempSetters, lookup, f); 839 } // end field loop 840 if (cls.isAnnotationPresent(IgnoreSuper.class)) { 841 if (log.isTraceEnabled()) { 842 log.trace("Class " + cls.getName() + " specifies IgnoreSuper; stopping hierarchy scan here"); 843 } 844 break; 845 } 846 cls = cls.getSuperclass(); 847 } // walk up class hierarchy 848 setters.putAll(tempSetters); 849 for(Method m : targetClass.getMethods()) { 850 if (m.isAnnotationPresent(AfterMapperMethod.class)) { 851 initializer = lookup.unreflect(m); 852 break; 853 } 854 } 855 } catch(Exception e){ 856 throw new ObjectMapperException(e); 857 } 858 } 859 } 860 } 861 } 862 863 /** 864 * For the given field, initializes the setter map if it's accessible and 865 * is not ignored. 866 * 867 * @param rootElemAnnotation The cached XMLRootElement annotation, just for easier type checking 868 * @param tempSetters The temporary setters 869 * @param lookup The MethodHandle lookup utility from the JDK 870 * @param field The field being analyzed 871 * @throws NoSuchFieldException if there is a problem accessing the field 872 */ 873 private void initAnalyzeField(Class<? extends Annotation> rootElemAnnotation, Map<String, MethodHandle> tempSetters, MethodHandles.Lookup lookup, Field field) throws NoSuchFieldException { 874 if (Modifier.isStatic(field.getModifiers())) { 875 if (log.isTraceEnabled()) { 876 log.trace("Ignore field " + field.getName() + " because it is static"); 877 } 878 return; 879 } 880 if (field.isAnnotationPresent(Ignore.class)) { 881 if (log.isTraceEnabled()) { 882 log.trace("Ignore field " + field.getName() + " because it specifies @Ignore"); 883 } 884 return; 885 } 886 if (field.isAnnotationPresent(Nested.class) || (findAnnotation(field.getType(), rootElemAnnotation))) { 887 Class<?> nestedType = field.getType(); 888 Nested nestedAnnotation = field.getAnnotation(Nested.class); 889 if (nestedAnnotation != null) { 890 if (nestedAnnotation.value().equals(Nested.class)) { 891 // This is the default if you leave the Nested annotation empty. You should probably not do this. 892 if (List.class.isAssignableFrom(field.getType())) { 893 nestedType = targetClass; 894 } 895 } else { 896 // Explicit mapped type set 897 nestedType = nestedAnnotation.value(); 898 } 899 } 900 nested.put(field.getName(), get(nestedType)); 901 } 902 initSetterForField(tempSetters, lookup, field); 903 } 904 905 /** 906 * Inserts a MethodHandle directly setting the value for the given field, or 907 * any of its alias names. 908 * 909 * @param setterMap The setter map 910 * @param lookupUtility The lookup utility for getting the MethodHandle 911 * @param field The field being evaluated 912 * @param fieldName The field name 913 * @throws IllegalAccessException if any failure occurs accessing the field 914 */ 915 private void insertDirectFieldWrite(Map<String, MethodHandle> setterMap, MethodHandles.Lookup lookupUtility, Field field, String fieldName) throws NoSuchFieldException, IllegalAccessException { 916 List<String> names = getNamesWithAliases(fieldName, field); 917 918 for(String name: names) { 919 setterTypes.put(name, field.getType()); 920 setterMap.put(name, lookupUtility.findSetter(targetClass, fieldName, field.getType())); 921 } 922 } 923 924 /** 925 * Inserts a MethodHandle invoking the setter method for the given field, or 926 * any of its alias names. 927 * 928 * @param setterMap The setter map 929 * @param lookupUtility The lookup utility for getting the MethodHandle 930 * @param field The field being evaluated 931 * @param fieldName The field name 932 * @param setterMethod The setter method located 933 * @throws IllegalAccessException if any failure occurs accessing the setter 934 */ 935 private void insertSetterMethodCall(Map<String, MethodHandle> setterMap, MethodHandles.Lookup lookupUtility, Field field, String fieldName, Method setterMethod) throws IllegalAccessException { 936 if (log.isTraceEnabled()) { 937 log.trace("For field " + fieldName + " found most general setter method " + setterMethod.getName() + " with param type " + setterMethod.getParameterTypes()[0].getName()); 938 } 939 940 List<String> names = getNamesWithAliases(fieldName, field); 941 942 for(String name: names) { 943 setterTypes.put(name, setterMethod.getParameterTypes()[0]); 944 setterMap.put(name, lookupUtility.unreflect(setterMethod)); 945 } 946 } 947 948}