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