001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.util;
018
019import java.beans.PropertyEditor;
020import java.beans.PropertyEditorManager;
021import java.lang.reflect.InvocationTargetException;
022import java.lang.reflect.Method;
023import java.lang.reflect.Proxy;
024import java.net.URI;
025import java.net.URISyntaxException;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Collection;
029import java.util.HashSet;
030import java.util.Iterator;
031import java.util.LinkedHashMap;
032import java.util.LinkedHashSet;
033import java.util.LinkedList;
034import java.util.List;
035import java.util.Locale;
036import java.util.Map;
037import java.util.Set;
038import java.util.regex.Pattern;
039
040import org.apache.camel.CamelContext;
041import org.apache.camel.Component;
042import org.apache.camel.NoTypeConversionAvailableException;
043import org.apache.camel.TypeConverter;
044import org.apache.camel.component.properties.PropertiesComponent;
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047
048import static org.apache.camel.util.ObjectHelper.isAssignableFrom;
049
050/**
051 * Helper for introspections of beans.
052 * <p/>
053 * <b>Important: </b> Its recommended to call the {@link #stop()} method when
054 * {@link org.apache.camel.CamelContext} is being stopped. This allows to clear the introspection cache.
055 * <p/>
056 * This implementation will skip methods from <tt>java.lang.Object</tt> and <tt>java.lang.reflect.Proxy</tt>.
057 * <p/>
058 * This implementation will use a cache when the {@link #getProperties(Object, java.util.Map, String)}
059 * method is being used. Also the {@link #cacheClass(Class)} method gives access to the introspect cache.
060 */
061public final class IntrospectionSupport {
062
063    private static final Logger LOG = LoggerFactory.getLogger(IntrospectionSupport.class);
064    private static final Pattern GETTER_PATTERN = Pattern.compile("(get|is)[A-Z].*");
065    private static final Pattern SETTER_PATTERN = Pattern.compile("set[A-Z].*");
066    private static final List<Method> EXCLUDED_METHODS = new ArrayList<Method>();
067    // use a cache to speedup introspecting for known classes during startup
068    // use a weak cache as we dont want the cache to keep around as it reference classes
069    // which could prevent classloader to unload classes if being referenced from this cache
070    private static final LRUCache<Class<?>, ClassInfo> CACHE = new LRUWeakCache<Class<?>, ClassInfo>(1000);
071    private static final Object LOCK = new Object();
072    private static final Pattern SECRETS = Pattern.compile(".*(passphrase|password|secretKey).*", Pattern.CASE_INSENSITIVE);
073
074    static {
075        // exclude all java.lang.Object methods as we dont want to invoke them
076        EXCLUDED_METHODS.addAll(Arrays.asList(Object.class.getMethods()));
077        // exclude all java.lang.reflect.Proxy methods as we dont want to invoke them
078        EXCLUDED_METHODS.addAll(Arrays.asList(Proxy.class.getMethods()));
079    }
080
081    private static final Set<Class<?>> PRIMITIVE_CLASSES = new HashSet<Class<?>>();
082
083    static {
084        PRIMITIVE_CLASSES.add(String.class);
085        PRIMITIVE_CLASSES.add(Character.class);
086        PRIMITIVE_CLASSES.add(Boolean.class);
087        PRIMITIVE_CLASSES.add(Byte.class);
088        PRIMITIVE_CLASSES.add(Short.class);
089        PRIMITIVE_CLASSES.add(Integer.class);
090        PRIMITIVE_CLASSES.add(Long.class);
091        PRIMITIVE_CLASSES.add(Float.class);
092        PRIMITIVE_CLASSES.add(Double.class);
093        PRIMITIVE_CLASSES.add(char.class);
094        PRIMITIVE_CLASSES.add(boolean.class);
095        PRIMITIVE_CLASSES.add(byte.class);
096        PRIMITIVE_CLASSES.add(short.class);
097        PRIMITIVE_CLASSES.add(int.class);
098        PRIMITIVE_CLASSES.add(long.class);
099        PRIMITIVE_CLASSES.add(float.class);
100        PRIMITIVE_CLASSES.add(double.class);
101    }
102
103    /**
104     * Structure of an introspected class.
105     */
106    public static final class ClassInfo {
107        public Class<?> clazz;
108        public MethodInfo[] methods;
109    }
110
111    /**
112     * Structure of an introspected method.
113     */
114    public static final class MethodInfo {
115        public Method method;
116        public Boolean isGetter;
117        public Boolean isSetter;
118        public String getterOrSetterShorthandName;
119        public Boolean hasGetterAndSetter;
120    }
121
122    /**
123     * Utility classes should not have a public constructor.
124     */
125    private IntrospectionSupport() {
126    }
127
128    /**
129     * {@link org.apache.camel.CamelContext} should call this stop method when its stopping.
130     * <p/>
131     * This implementation will clear its introspection cache.
132     */
133    public static void stop() {
134        if (LOG.isDebugEnabled()) {
135            LOG.debug("Clearing cache[size={}, hits={}, misses={}, evicted={}]", new Object[]{CACHE.size(), CACHE.getHits(), CACHE.getMisses(), CACHE.getEvicted()});
136        }
137        CACHE.clear();
138
139        // flush java beans introspector as it may be in use by the PropertyEditor
140        java.beans.Introspector.flushCaches();
141    }
142
143    public static boolean isGetter(Method method) {
144        String name = method.getName();
145        Class<?> type = method.getReturnType();
146        Class<?> params[] = method.getParameterTypes();
147
148        if (!GETTER_PATTERN.matcher(name).matches()) {
149            return false;
150        }
151
152        // special for isXXX boolean
153        if (name.startsWith("is")) {
154            return params.length == 0 && type.getSimpleName().equalsIgnoreCase("boolean");
155        }
156
157        return params.length == 0 && !type.equals(Void.TYPE);
158    }
159
160    public static String getGetterShorthandName(Method method) {
161        if (!isGetter(method)) {
162            return method.getName();
163        }
164
165        String name = method.getName();
166        if (name.startsWith("get")) {
167            name = name.substring(3);
168            name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
169        } else if (name.startsWith("is")) {
170            name = name.substring(2);
171            name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
172        }
173
174        return name;
175    }
176
177    public static String getSetterShorthandName(Method method) {
178        if (!isSetter(method)) {
179            return method.getName();
180        }
181
182        String name = method.getName();
183        if (name.startsWith("set")) {
184            name = name.substring(3);
185            name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
186        }
187
188        return name;
189    }
190
191    public static boolean isSetter(Method method, boolean allowBuilderPattern) {
192        String name = method.getName();
193        Class<?> type = method.getReturnType();
194        Class<?> params[] = method.getParameterTypes();
195
196        if (!SETTER_PATTERN.matcher(name).matches()) {
197            return false;
198        }
199
200        return params.length == 1 && (type.equals(Void.TYPE) || (allowBuilderPattern && method.getDeclaringClass().isAssignableFrom(type)));
201    }
202    
203    public static boolean isSetter(Method method) {
204        return isSetter(method, false);
205    }
206
207
208    /**
209     * Will inspect the target for properties.
210     * <p/>
211     * Notice a property must have both a getter/setter method to be included.
212     * Notice all <tt>null</tt> values will be included.
213     *
214     * @param target         the target bean
215     * @param properties     the map to fill in found properties
216     * @param optionPrefix   an optional prefix to append the property key
217     * @return <tt>true</tt> if any properties was found, <tt>false</tt> otherwise.
218     */
219    public static boolean getProperties(Object target, Map<String, Object> properties, String optionPrefix) {
220        return getProperties(target, properties, optionPrefix, true);
221    }
222
223    /**
224     * Will inspect the target for properties.
225     * <p/>
226     * Notice a property must have both a getter/setter method to be included.
227     *
228     * @param target         the target bean
229     * @param properties     the map to fill in found properties
230     * @param optionPrefix   an optional prefix to append the property key
231     * @param includeNull    whether to include <tt>null</tt> values
232     * @return <tt>true</tt> if any properties was found, <tt>false</tt> otherwise.
233     */
234    public static boolean getProperties(Object target, Map<String, Object> properties, String optionPrefix, boolean includeNull) {
235        ObjectHelper.notNull(target, "target");
236        ObjectHelper.notNull(properties, "properties");
237        boolean rc = false;
238        if (optionPrefix == null) {
239            optionPrefix = "";
240        }
241
242        ClassInfo cache = cacheClass(target.getClass());
243
244        for (MethodInfo info : cache.methods) {
245            Method method = info.method;
246            // we can only get properties if we have both a getter and a setter
247            if (info.isGetter && info.hasGetterAndSetter) {
248                String name = info.getterOrSetterShorthandName;
249                try {
250                    // we may want to set options on classes that has package view visibility, so override the accessible
251                    method.setAccessible(true);
252                    Object value = method.invoke(target);
253                    if (value != null || includeNull) {
254                        properties.put(optionPrefix + name, value);
255                        rc = true;
256                    }
257                } catch (Exception e) {
258                    if (LOG.isTraceEnabled()) {
259                        LOG.trace("Error invoking getter method " + method + ". This exception is ignored.", e);
260                    }
261                }
262            }
263        }
264
265        return rc;
266    }
267
268    /**
269     * Introspects the given class.
270     *
271     * @param clazz the class
272     * @return the introspection result as a {@link ClassInfo} structure.
273     */
274    public static ClassInfo cacheClass(Class<?> clazz) {
275        ClassInfo cache = CACHE.get(clazz);
276        if (cache == null) {
277            cache = doIntrospectClass(clazz);
278            CACHE.put(clazz, cache);
279        }
280        return cache;
281    }
282
283    private static ClassInfo doIntrospectClass(Class<?> clazz) {
284        ClassInfo answer = new ClassInfo();
285        answer.clazz = clazz;
286
287        // loop each method on the class and gather details about the method
288        // especially about getter/setters
289        List<MethodInfo> found = new ArrayList<MethodInfo>();
290        Method[] methods = clazz.getMethods();
291        for (Method method : methods) {
292            if (EXCLUDED_METHODS.contains(method)) {
293                continue;
294            }
295
296            MethodInfo cache = new MethodInfo();
297            cache.method = method;
298            if (isGetter(method)) {
299                cache.isGetter = true;
300                cache.isSetter = false;
301                cache.getterOrSetterShorthandName = getGetterShorthandName(method);
302            } else if (isSetter(method)) {
303                cache.isGetter = false;
304                cache.isSetter = true;
305                cache.getterOrSetterShorthandName = getSetterShorthandName(method);
306            } else {
307                cache.isGetter = false;
308                cache.isSetter = false;
309                cache.hasGetterAndSetter = false;
310            }
311            found.add(cache);
312        }
313
314        // for all getter/setter, find out if there is a corresponding getter/setter,
315        // so we have a read/write bean property.
316        for (MethodInfo info : found) {
317            info.hasGetterAndSetter = false;
318            if (info.isGetter) {
319                // loop and find the matching setter
320                for (MethodInfo info2 : found) {
321                    if (info2.isSetter && info.getterOrSetterShorthandName.equals(info2.getterOrSetterShorthandName)) {
322                        info.hasGetterAndSetter = true;
323                        break;
324                    }
325                }
326            } else if (info.isSetter) {
327                // loop and find the matching getter
328                for (MethodInfo info2 : found) {
329                    if (info2.isGetter && info.getterOrSetterShorthandName.equals(info2.getterOrSetterShorthandName)) {
330                        info.hasGetterAndSetter = true;
331                        break;
332                    }
333                }
334            }
335        }
336
337        answer.methods = found.toArray(new MethodInfo[found.size()]);
338        return answer;
339    }
340
341    public static boolean hasProperties(Map<String, Object> properties, String optionPrefix) {
342        ObjectHelper.notNull(properties, "properties");
343
344        if (ObjectHelper.isNotEmpty(optionPrefix)) {
345            for (Object o : properties.keySet()) {
346                String name = (String) o;
347                if (name.startsWith(optionPrefix)) {
348                    return true;
349                }
350            }
351            // no parameters with this prefix
352            return false;
353        } else {
354            return !properties.isEmpty();
355        }
356    }
357
358    public static Object getProperty(Object target, String property) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
359        ObjectHelper.notNull(target, "target");
360        ObjectHelper.notNull(property, "property");
361
362        property = property.substring(0, 1).toUpperCase(Locale.ENGLISH) + property.substring(1);
363
364        Class<?> clazz = target.getClass();
365        Method method = getPropertyGetter(clazz, property);
366        return method.invoke(target);
367    }
368
369    public static Object getOrElseProperty(Object target, String property, Object defaultValue) {
370        try {
371            return getProperty(target, property);
372        } catch (Exception e) {
373            return defaultValue;
374        }
375    }
376
377    public static Method getPropertyGetter(Class<?> type, String propertyName) throws NoSuchMethodException {
378        if (isPropertyIsGetter(type, propertyName)) {
379            return type.getMethod("is" + ObjectHelper.capitalize(propertyName));
380        } else {
381            return type.getMethod("get" + ObjectHelper.capitalize(propertyName));
382        }
383    }
384
385    public static Method getPropertySetter(Class<?> type, String propertyName) throws NoSuchMethodException {
386        String name = "set" + ObjectHelper.capitalize(propertyName);
387        for (Method method : type.getMethods()) {
388            if (isSetter(method) && method.getName().equals(name)) {
389                return method;
390            }
391        }
392        throw new NoSuchMethodException(type.getCanonicalName() + "." + name);
393    }
394
395    public static boolean isPropertyIsGetter(Class<?> type, String propertyName) {
396        try {
397            Method method = type.getMethod("is" + ObjectHelper.capitalize(propertyName));
398            if (method != null) {
399                return method.getReturnType().isAssignableFrom(boolean.class) || method.getReturnType().isAssignableFrom(Boolean.class);
400            }
401        } catch (NoSuchMethodException e) {
402            // ignore
403        }
404        return false;
405    }
406    
407    public static boolean setProperties(Object target, Map<String, Object> properties, String optionPrefix, boolean allowBuilderPattern) throws Exception {
408        ObjectHelper.notNull(target, "target");
409        ObjectHelper.notNull(properties, "properties");
410        boolean rc = false;
411
412        for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
413            Map.Entry<String, Object> entry = it.next();
414            String name = entry.getKey().toString();
415            if (name.startsWith(optionPrefix)) {
416                Object value = properties.get(name);
417                name = name.substring(optionPrefix.length());
418                if (setProperty(target, name, value, allowBuilderPattern)) {
419                    it.remove();
420                    rc = true;
421                }
422            }
423        }
424        
425        return rc;
426    }
427
428    public static boolean setProperties(Object target, Map<String, Object> properties, String optionPrefix) throws Exception {
429        ObjectHelper.notEmpty(optionPrefix, "optionPrefix");
430        return setProperties(target, properties, optionPrefix, false);
431    }
432
433    public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) {
434        ObjectHelper.notNull(properties, "properties");
435
436        Map<String, Object> rc = new LinkedHashMap<String, Object>(properties.size());
437
438        for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
439            Map.Entry<String, Object> entry = it.next();
440            String name = entry.getKey();
441            if (name.startsWith(optionPrefix)) {
442                Object value = properties.get(name);
443                name = name.substring(optionPrefix.length());
444                rc.put(name, value);
445                it.remove();
446            }
447        }
448
449        return rc;
450    }
451
452    public static Map<String, String> extractStringProperties(Map<String, Object> properties) {
453        ObjectHelper.notNull(properties, "properties");
454
455        Map<String, String> rc = new LinkedHashMap<String, String>(properties.size());
456
457        for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
458            Map.Entry<String, Object> entry = it.next();
459            String name = entry.getKey();
460            String value = entry.getValue().toString();
461            rc.put(name, value);
462        }
463
464        return rc;
465    }
466
467    public static boolean setProperties(CamelContext context, TypeConverter typeConverter, Object target, Map<String, Object> properties) throws Exception {
468        ObjectHelper.notNull(target, "target");
469        ObjectHelper.notNull(properties, "properties");
470        boolean rc = false;
471
472        for (Iterator<Map.Entry<String, Object>> iter = properties.entrySet().iterator(); iter.hasNext();) {
473            Map.Entry<String, Object> entry = iter.next();
474            if (setProperty(context, typeConverter, target, entry.getKey(), entry.getValue())) {
475                iter.remove();
476                rc = true;
477            }
478        }
479
480        return rc;
481    }
482    
483    public static boolean setProperties(TypeConverter typeConverter, Object target, Map<String, Object> properties) throws Exception {
484        return setProperties(null, typeConverter, target, properties);
485    }
486
487    public static boolean setProperties(Object target, Map<String, Object> properties) throws Exception {
488        return setProperties(null, target, properties);
489    }
490
491    /**
492     * This method supports two modes to set a property:
493     *
494     * 1. Setting a property that has already been resolved, this is the case when {@code context} and {@code refName} are
495     * NULL and {@code value} is non-NULL.
496     *
497     * 2. Setting a property that has not yet been resolved, the property will be resolved based on the suitable methods
498     * found matching the property name on the {@code target} bean. For this mode to be triggered the parameters
499     * {@code context} and {@code refName} must NOT be NULL, and {@code value} MUST be NULL.
500     *
501     */
502    public static boolean setProperty(CamelContext context, TypeConverter typeConverter, Object target, String name, Object value, String refName, boolean allowBuilderPattern) throws Exception {
503        Class<?> clazz = target.getClass();
504        Collection<Method> setters;
505
506        // we need to lookup the value from the registry
507        if (context != null && refName != null && value == null) {
508            setters = findSetterMethodsOrderedByParameterType(clazz, name, allowBuilderPattern);
509        } else {
510            // find candidates of setter methods as there can be overloaded setters
511            setters = findSetterMethods(clazz, name, value, allowBuilderPattern);
512        }
513        if (setters.isEmpty()) {
514            return false;
515        }
516
517        // loop and execute the best setter method
518        Exception typeConversionFailed = null;
519        for (Method setter : setters) {
520            Class<?> parameterType = setter.getParameterTypes()[0];
521            Object ref = value;
522            // try and lookup the reference based on the method
523            if (context != null && refName != null && ref == null) {
524                String s = StringHelper.replaceAll(refName, "#", "");
525                ref = CamelContextHelper.lookup(context, s);
526                if (ref == null) {
527                    // try the next method if nothing was found
528                    continue;
529                } else {
530                    // setter method has not the correct type
531                    // (must use ObjectHelper.isAssignableFrom which takes primitive types into account)
532                    boolean assignable = isAssignableFrom(parameterType, ref.getClass());
533                    if (!assignable) {
534                        continue;
535                    }
536                }
537            }
538
539            try {
540                try {
541                    // If the type is null or it matches the needed type, just use the value directly
542                    if (value == null || isAssignableFrom(parameterType, ref.getClass())) {
543                        // we may want to set options on classes that has package view visibility, so override the accessible
544                        setter.setAccessible(true);
545                        setter.invoke(target, ref);
546                        if (LOG.isTraceEnabled()) {
547                            // hide sensitive data
548                            String val = ref != null ? ref.toString() : "";
549                            if (SECRETS.matcher(name).find()) {
550                                val = "xxxxxx";
551                            }
552                            LOG.trace("Configured property: {} on bean: {} with value: {}", new Object[]{name, target, val});
553                        }
554                        return true;
555                    } else {
556                        // We need to convert it
557                        Object convertedValue = convert(typeConverter, parameterType, ref);
558                        // we may want to set options on classes that has package view visibility, so override the accessible
559                        setter.setAccessible(true);
560                        setter.invoke(target, convertedValue);
561                        if (LOG.isTraceEnabled()) {
562                            // hide sensitive data
563                            String val = ref != null ? ref.toString() : "";
564                            if (SECRETS.matcher(name).find()) {
565                                val = "xxxxxx";
566                            }
567                            LOG.trace("Configured property: {} on bean: {} with value: {}", new Object[]{name, target, val});
568                        }
569                        return true;
570                    }
571                } catch (InvocationTargetException e) {
572                    // lets unwrap the exception
573                    Throwable throwable = e.getCause();
574                    if (throwable instanceof Exception) {
575                        Exception exception = (Exception)throwable;
576                        throw exception;
577                    } else {
578                        Error error = (Error)throwable;
579                        throw error;
580                    }
581                }
582            // ignore exceptions as there could be another setter method where we could type convert successfully
583            } catch (SecurityException e) {
584                typeConversionFailed = e;
585            } catch (NoTypeConversionAvailableException e) {
586                typeConversionFailed = e;
587            } catch (IllegalArgumentException e) {
588                typeConversionFailed = e;
589            }
590            if (LOG.isTraceEnabled()) {
591                LOG.trace("Setter \"{}\" with parameter type \"{}\" could not be used for type conversions of {}",
592                        new Object[]{setter, parameterType, ref});
593            }
594        }
595
596        if (typeConversionFailed != null && !isPropertyPlaceholder(context, value)) {
597            // we did not find a setter method to use, and if we did try to use a type converter then throw
598            // this kind of exception as the caused by will hint this error
599            throw new IllegalArgumentException("Could not find a suitable setter for property: " + name
600                    + " as there isn't a setter method with same type: " + (value != null ? value.getClass().getCanonicalName() : "[null]")
601                    + " nor type conversion possible: " + typeConversionFailed.getMessage());
602        } else {
603            return false;
604        }
605    }
606
607    private static boolean isPropertyPlaceholder(CamelContext context, Object value) {
608        if (context != null) {
609            Component component = context.hasComponent("properties");
610            if (component != null) {
611                PropertiesComponent pc;
612                try {
613                    pc = context.getTypeConverter().mandatoryConvertTo(PropertiesComponent.class, component);
614                } catch (Exception e) {
615                    return false;
616                }
617                if (value.toString().contains(pc.getPrefixToken()) && value.toString().contains(pc.getSuffixToken())) {
618                    return true;
619                }
620            }
621        }
622        return false;
623    }
624
625    public static boolean setProperty(CamelContext context, TypeConverter typeConverter, Object target, String name, Object value) throws Exception {
626        // allow build pattern as a setter as well
627        return setProperty(context, typeConverter, target, name, value, null, true);
628    }
629    
630    public static boolean setProperty(TypeConverter typeConverter, Object target, String name, Object value) throws Exception {
631        // allow build pattern as a setter as well
632        return setProperty(null, typeConverter, target, name, value, null, true);
633    }
634    
635    public static boolean setProperty(Object target, String name, Object value, boolean allowBuilderPattern) throws Exception {
636        return setProperty(null, null, target, name, value, null, allowBuilderPattern);
637    }
638
639    public static boolean setProperty(Object target, String name, Object value) throws Exception {
640        // allow build pattern as a setter as well
641        return setProperty(target, name, value, true);
642    }
643
644    private static Object convert(TypeConverter typeConverter, Class<?> type, Object value)
645        throws URISyntaxException, NoTypeConversionAvailableException {
646        if (typeConverter != null) {
647            return typeConverter.mandatoryConvertTo(type, value);
648        }
649        if (type == URI.class) {
650            return new URI(value.toString());
651        }
652        PropertyEditor editor = PropertyEditorManager.findEditor(type);
653        if (editor != null) {
654            // property editor is not thread safe, so we need to lock
655            Object answer;
656            synchronized (LOCK) {
657                editor.setAsText(value.toString());
658                answer = editor.getValue();
659            }
660            return answer;
661        }
662        return null;
663    }
664
665    public static Set<Method> findSetterMethods(Class<?> clazz, String name, boolean allowBuilderPattern) {
666        Set<Method> candidates = new LinkedHashSet<Method>();
667
668        // Build the method name.
669        name = "set" + ObjectHelper.capitalize(name);
670        while (clazz != Object.class) {
671            // Since Object.class.isInstance all the objects,
672            // here we just make sure it will be add to the bottom of the set.
673            Method objectSetMethod = null;
674            Method[] methods = clazz.getMethods();
675            for (Method method : methods) {
676                if (method.getName().equals(name) && isSetter(method, allowBuilderPattern)) {
677                    Class<?> params[] = method.getParameterTypes();
678                    if (params[0].equals(Object.class)) {
679                        objectSetMethod = method;
680                    } else {
681                        candidates.add(method);
682                    }
683                }
684            }
685            if (objectSetMethod != null) {
686                candidates.add(objectSetMethod);
687            }
688            clazz = clazz.getSuperclass();
689        }
690        return candidates;
691    }
692
693    private static Set<Method> findSetterMethods(Class<?> clazz, String name, Object value, boolean allowBuilderPattern) {
694        Set<Method> candidates = findSetterMethods(clazz, name, allowBuilderPattern);
695
696        if (candidates.isEmpty()) {
697            return candidates;
698        } else if (candidates.size() == 1) {
699            // only one
700            return candidates;
701        } else {
702            // find the best match if possible
703            LOG.trace("Found {} suitable setter methods for setting {}", candidates.size(), name);
704            // prefer to use the one with the same instance if any exists
705            for (Method method : candidates) {                               
706                if (method.getParameterTypes()[0].isInstance(value)) {
707                    LOG.trace("Method {} is the best candidate as it has parameter with same instance type", method);
708                    // retain only this method in the answer
709                    candidates.clear();
710                    candidates.add(method);
711                    return candidates;
712                }
713            }
714            // fallback to return what we have found as candidates so far
715            return candidates;
716        }
717    }
718
719    protected static List<Method> findSetterMethodsOrderedByParameterType(Class<?> target, String propertyName, boolean allowBuilderPattern) {
720        List<Method> answer = new LinkedList<Method>();
721        List<Method> primitives = new LinkedList<Method>();
722        Set<Method> setters = findSetterMethods(target, propertyName, allowBuilderPattern);
723        for (Method setter : setters) {
724            Class<?> parameterType = setter.getParameterTypes()[0];
725            if (PRIMITIVE_CLASSES.contains(parameterType)) {
726                primitives.add(setter);
727            } else {
728                answer.add(setter);
729            }
730        }
731        // primitives get added last
732        answer.addAll(primitives);
733        return answer;
734    }
735
736}