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