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}