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