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