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