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}