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