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     */
049    public final class IntrospectionSupport {
050    
051        private static final transient Logger LOG = LoggerFactory.getLogger(IntrospectionSupport.class);
052        private static final Pattern GETTER_PATTERN = Pattern.compile("(get|is)[A-Z].*");
053        private static final Pattern SETTER_PATTERN = Pattern.compile("set[A-Z].*");
054        private static final List<Method> EXCLUDED_METHODS = new ArrayList<Method>();
055    
056        static {
057            // exclude all java.lang.Object methods as we dont want to invoke them
058            EXCLUDED_METHODS.addAll(Arrays.asList(Object.class.getMethods()));
059            // exclude all java.lang.reflect.Proxy methods as we dont want to invoke them
060            EXCLUDED_METHODS.addAll(Arrays.asList(Proxy.class.getMethods()));
061        }
062    
063        private static final Set<Class> PRIMITIVE_CLASSES = new HashSet<Class>();
064    
065        static {
066            PRIMITIVE_CLASSES.add(String.class);
067            PRIMITIVE_CLASSES.add(Character.class);
068            PRIMITIVE_CLASSES.add(Boolean.class);
069            PRIMITIVE_CLASSES.add(Byte.class);
070            PRIMITIVE_CLASSES.add(Short.class);
071            PRIMITIVE_CLASSES.add(Integer.class);
072            PRIMITIVE_CLASSES.add(Long.class);
073            PRIMITIVE_CLASSES.add(Float.class);
074            PRIMITIVE_CLASSES.add(Double.class);
075            PRIMITIVE_CLASSES.add(char.class);
076            PRIMITIVE_CLASSES.add(boolean.class);
077            PRIMITIVE_CLASSES.add(byte.class);
078            PRIMITIVE_CLASSES.add(short.class);
079            PRIMITIVE_CLASSES.add(int.class);
080            PRIMITIVE_CLASSES.add(long.class);
081            PRIMITIVE_CLASSES.add(float.class);
082            PRIMITIVE_CLASSES.add(double.class);
083        }
084    
085        /**
086         * Utility classes should not have a public constructor.
087         */
088        private IntrospectionSupport() {
089        }
090    
091        public static boolean isGetter(Method method) {
092            String name = method.getName();
093            Class<?> type = method.getReturnType();
094            Class<?> params[] = method.getParameterTypes();
095    
096            if (!GETTER_PATTERN.matcher(name).matches()) {
097                return false;
098            }
099    
100            // special for isXXX boolean
101            if (name.startsWith("is")) {
102                return params.length == 0 && type.getSimpleName().equalsIgnoreCase("boolean");
103            }
104    
105            return params.length == 0 && !type.equals(Void.TYPE);
106        }
107    
108        public static String getGetterShorthandName(Method method) {
109            if (!isGetter(method)) {
110                return method.getName();
111            }
112    
113            String name = method.getName();
114            if (name.startsWith("get")) {
115                name = name.substring(3);
116                name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
117            } else if (name.startsWith("is")) {
118                name = name.substring(2);
119                name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
120            }
121    
122            return name;
123        }
124    
125        public static String getSetterShorthandName(Method method) {
126            if (!isSetter(method)) {
127                return method.getName();
128            }
129    
130            String name = method.getName();
131            if (name.startsWith("set")) {
132                name = name.substring(3);
133                name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
134            }
135    
136            return name;
137        }
138    
139        public static boolean isSetter(Method method, boolean allowBuilderPattern) {
140            String name = method.getName();
141            Class<?> type = method.getReturnType();
142            Class<?> params[] = method.getParameterTypes();
143    
144            if (!SETTER_PATTERN.matcher(name).matches()) {
145                return false;
146            }
147    
148            return params.length == 1 && (type.equals(Void.TYPE) || (allowBuilderPattern && method.getDeclaringClass().isAssignableFrom(type)));
149        }
150        
151        public static boolean isSetter(Method method) {
152            return isSetter(method, false);
153        }
154    
155        /**
156         * Will inspect the target for properties.
157         * <p/>
158         * Notice a property must have both a getter/setter method to be included.
159         *
160         * @param target         the target bean
161         * @param properties     the map to fill in found properties
162         * @param optionPrefix   an optional prefix to append the property key
163         * @return <tt>true</tt> if any properties was found, <tt>false</tt> otherwise.
164         */
165        public static boolean getProperties(Object target, Map<String, Object> properties, String optionPrefix) {
166            ObjectHelper.notNull(target, "target");
167            ObjectHelper.notNull(properties, "properties");
168            boolean rc = false;
169            if (optionPrefix == null) {
170                optionPrefix = "";
171            }
172    
173            Class<?> clazz = target.getClass();
174            Method[] methods = clazz.getMethods();
175            for (Method method : methods) {
176                if (EXCLUDED_METHODS.contains(method)) {
177                    continue;
178                }
179                try {
180                    // must be properties which have setters
181                    if (isGetter(method) && hasSetter(target, method)) {
182                        Object value = method.invoke(target);
183                        String name = getGetterShorthandName(method);
184                        properties.put(optionPrefix + name, value);
185                        rc = true;
186                    }
187                } catch (Exception e) {
188                    // ignore
189                }
190            }
191    
192            return rc;
193        }
194    
195        public static boolean hasSetter(Object target, Method getter) {
196            String name = getGetterShorthandName(getter);
197    
198            Class<?> clazz = target.getClass();
199            Method[] methods = clazz.getMethods();
200            for (Method method : methods) {
201                if (EXCLUDED_METHODS.contains(method)) {
202                    continue;
203                }
204                if (isSetter(method)) {
205                    if (name.equals(getSetterShorthandName(method))) {
206                        return true;
207                    }
208                }
209            }
210    
211            return false;
212        }
213    
214        public static boolean hasProperties(Map<String, Object> properties, String optionPrefix) {
215            ObjectHelper.notNull(properties, "properties");
216    
217            if (ObjectHelper.isNotEmpty(optionPrefix)) {
218                for (Object o : properties.keySet()) {
219                    String name = (String) o;
220                    if (name.startsWith(optionPrefix)) {
221                        return true;
222                    }
223                }
224                // no parameters with this prefix
225                return false;
226            } else {
227                return !properties.isEmpty();
228            }
229        }
230    
231        public static Object getProperty(Object target, String property) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
232            ObjectHelper.notNull(target, "target");
233            ObjectHelper.notNull(property, "property");
234    
235            property = property.substring(0, 1).toUpperCase(Locale.ENGLISH) + property.substring(1);
236    
237            Class<?> clazz = target.getClass();
238            Method method = getPropertyGetter(clazz, property);
239            return method.invoke(target);
240        }
241    
242        public static Method getPropertyGetter(Class<?> type, String propertyName) throws NoSuchMethodException {
243            if (isPropertyIsGetter(type, propertyName)) {
244                return type.getMethod("is" + ObjectHelper.capitalize(propertyName));
245            } else {
246                return type.getMethod("get" + ObjectHelper.capitalize(propertyName));
247            }
248        }
249    
250        public static Method getPropertySetter(Class<?> type, String propertyName) throws NoSuchMethodException {
251            String name = "set" + ObjectHelper.capitalize(propertyName);
252            for (Method method : type.getMethods()) {
253                if (isSetter(method) && method.getName().equals(name)) {
254                    return method;
255                }
256            }
257            throw new NoSuchMethodException(type.getCanonicalName() + "." + name);
258        }
259    
260        public static boolean isPropertyIsGetter(Class<?> type, String propertyName) {
261            try {
262                Method method = type.getMethod("is" + ObjectHelper.capitalize(propertyName));
263                if (method != null) {
264                    return method.getReturnType().isAssignableFrom(boolean.class) || method.getReturnType().isAssignableFrom(Boolean.class);
265                }
266            } catch (NoSuchMethodException e) {
267                // ignore
268            }
269            return false;
270        }
271        
272        public static boolean setProperties(Object target, Map<String, Object> properties, String optionPrefix, boolean allowBuilderPattern) throws Exception {
273            ObjectHelper.notNull(target, "target");
274            ObjectHelper.notNull(properties, "properties");
275            boolean rc = false;
276    
277            for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
278                Map.Entry<String, Object> entry = it.next();
279                String name = entry.getKey().toString();
280                if (name.startsWith(optionPrefix)) {
281                    Object value = properties.get(name);
282                    name = name.substring(optionPrefix.length());
283                    if (setProperty(target, name, value, allowBuilderPattern)) {
284                        it.remove();
285                        rc = true;
286                    }
287                }
288            }
289            
290            return rc;
291        }
292    
293        public static boolean setProperties(Object target, Map<String, Object> properties, String optionPrefix) throws Exception {
294            return setProperties(target, properties, optionPrefix, false);
295        }
296    
297        public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) {
298            ObjectHelper.notNull(properties, "properties");
299    
300            Map<String, Object> rc = new LinkedHashMap<String, Object>(properties.size());
301    
302            for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
303                Map.Entry<String, Object> entry = it.next();
304                String name = entry.getKey();
305                if (name.startsWith(optionPrefix)) {
306                    Object value = properties.get(name);
307                    name = name.substring(optionPrefix.length());
308                    rc.put(name, value);
309                    it.remove();
310                }
311            }
312    
313            return rc;
314        }
315    
316        public static boolean setProperties(TypeConverter typeConverter, Object target, Map<String, Object> properties) throws Exception {
317            ObjectHelper.notNull(target, "target");
318            ObjectHelper.notNull(properties, "properties");
319            boolean rc = false;
320    
321            for (Iterator<Map.Entry<String, Object>> iter = properties.entrySet().iterator(); iter.hasNext();) {
322                Map.Entry<String, Object> entry = iter.next();
323                if (setProperty(typeConverter, target, entry.getKey(), entry.getValue())) {
324                    iter.remove();
325                    rc = true;
326                }
327            }
328    
329            return rc;
330        }
331    
332        public static boolean setProperties(Object target, Map<String, Object> properties) throws Exception {
333            return setProperties(null, target, properties);
334        }
335    
336        /**
337         * This method supports two modes to set a property:
338         *
339         * 1. Setting a property that has already been resolved, this is the case when {@code context} and {@code refName} are
340         * NULL and {@code value} is non-NULL.
341         *
342         * 2. Setting a property that has not yet been resolved, the property will be resolved based on the suitable methods
343         * found matching the property name on the {@code target} bean. For this mode to be triggered the parameters
344         * {@code context} and {@code refName} must NOT be NULL, and {@code value} MUST be NULL.
345         *
346         */
347        public static boolean setProperty(CamelContext context, TypeConverter typeConverter, Object target, String name, Object value, String refName, boolean allowBuilderPattern) throws Exception {
348            Class<?> clazz = target.getClass();
349            Collection<Method> setters;
350    
351            // we need to lookup the value from the registry
352            if (context != null && refName != null && value == null) {
353                setters = findSetterMethodsOrderedByParameterType(clazz, name, allowBuilderPattern);
354            } else {
355                // find candidates of setter methods as there can be overloaded setters
356                setters = findSetterMethods(clazz, name, value, allowBuilderPattern);
357            }
358            if (setters.isEmpty()) {
359                return false;
360            }
361    
362            // loop and execute the best setter method
363            Exception typeConversionFailed = null;
364            for (Method setter : setters) {
365                Class parameterType = setter.getParameterTypes()[0];
366                Object ref = value;
367                // try and lookup the reference based on the method
368                if (context != null && refName != null && ref == null) {
369                    ref = CamelContextHelper.lookup(context, refName.replaceAll("#", ""), parameterType);
370                    if (ref == null) {
371                        continue; // try the next method if nothing was found
372                    }
373                }
374    
375                try {
376                    try {
377                        // If the type is null or it matches the needed type, just use the value directly
378                        if (value == null || parameterType.isAssignableFrom(ref.getClass())) {
379                            setter.invoke(target, ref);
380                            if (LOG.isDebugEnabled()) {
381                                LOG.debug("Configured property: {} on bean: {} with value: {}", new Object[]{name, target, ref});
382                            }
383                            return true;
384                        } else {
385                            // We need to convert it
386                            Object convertedValue = convert(typeConverter, parameterType, ref);
387                            setter.invoke(target, convertedValue);
388                            if (LOG.isDebugEnabled()) {
389                                LOG.debug("Configured property: {} on bean: {} with value: {}", new Object[]{name, target, ref});
390                            }
391                            return true;
392                        }
393                    } catch (InvocationTargetException e) {
394                        // lets unwrap the exception
395                        Throwable throwable = e.getCause();
396                        if (throwable instanceof Exception) {
397                            Exception exception = (Exception)throwable;
398                            throw exception;
399                        } else {
400                            Error error = (Error)throwable;
401                            throw error;
402                        }
403                    }
404                // ignore exceptions as there could be another setter method where we could type convert successfully
405                } catch (NoTypeConversionAvailableException e) {
406                    typeConversionFailed = e;
407                } catch (IllegalArgumentException e) {
408                    typeConversionFailed = e;
409                }
410                if (LOG.isTraceEnabled()) {
411                    LOG.trace("Setter \"{}\" with parameter type \"{}\" could not be used for type conversions of {}",
412                            new Object[]{setter, parameterType, ref});
413                }
414            }
415    
416            if (typeConversionFailed != null) {
417                // we did not find a setter method to use, and if we did try to use a type converter then throw
418                // this kind of exception as the caused by will hint this error
419                throw new IllegalArgumentException("Could not find a suitable setter for property: " + name
420                        + " as there isn't a setter method with same type: " + value.getClass().getCanonicalName()
421                        + " nor type conversion possible: " + typeConversionFailed.getMessage());
422            } else {
423                return false;
424            }
425        }
426    
427        public static boolean setProperty(TypeConverter typeConverter, Object target, String name, Object value) throws Exception {
428            // allow build pattern as a setter as well
429            return setProperty(null, typeConverter, target, name, value, null, true);
430        }
431        
432        public static boolean setProperty(Object target, String name, Object value, boolean allowBuilderPattern) throws Exception {
433            return setProperty(null, null, target, name, value, null, allowBuilderPattern);
434        }
435    
436        public static boolean setProperty(Object target, String name, Object value) throws Exception {
437            // allow build pattern as a setter as well
438            return setProperty(target, name, value, true);
439        }
440    
441        private static Object convert(TypeConverter typeConverter, Class<?> type, Object value)
442            throws URISyntaxException, NoTypeConversionAvailableException {
443            if (typeConverter != null) {
444                return typeConverter.mandatoryConvertTo(type, value);
445            }
446            PropertyEditor editor = PropertyEditorManager.findEditor(type);
447            if (editor != null) {
448                editor.setAsText(value.toString());
449                return editor.getValue();
450            }
451            if (type == URI.class) {
452                return new URI(value.toString());
453            }
454            return null;
455        }
456    
457        public static Set<Method> findSetterMethods(Class<?> clazz, String name, boolean allowBuilderPattern) {
458            Set<Method> candidates = new LinkedHashSet<Method>();
459    
460            // Build the method name.
461            name = "set" + ObjectHelper.capitalize(name);
462            while (clazz != Object.class) {
463                // Since Object.class.isInstance all the objects,
464                // here we just make sure it will be add to the bottom of the set.
465                Method objectSetMethod = null;
466                Method[] methods = clazz.getMethods();
467                for (Method method : methods) {
468                    if (method.getName().equals(name) && isSetter(method, allowBuilderPattern)) {
469                        Class<?> params[] = method.getParameterTypes();
470                        if (params[0].equals(Object.class)) {
471                            objectSetMethod = method;
472                        } else {
473                            candidates.add(method);
474                        }
475                    }
476                }
477                if (objectSetMethod != null) {
478                    candidates.add(objectSetMethod);
479                }
480                clazz = clazz.getSuperclass();
481            }
482            return candidates;
483        }
484    
485        private static Set<Method> findSetterMethods(Class<?> clazz, String name, Object value, boolean allowBuilderPattern) {
486            Set<Method> candidates = findSetterMethods(clazz, name, allowBuilderPattern);
487    
488            if (candidates.isEmpty()) {
489                return candidates;
490            } else if (candidates.size() == 1) {
491                // only one
492                return candidates;
493            } else {
494                // find the best match if possible
495                LOG.trace("Found {} suitable setter methods for setting {}", candidates.size(), name);
496                // prefer to use the one with the same instance if any exists
497                for (Method method : candidates) {                               
498                    if (method.getParameterTypes()[0].isInstance(value)) {
499                        LOG.trace("Method {} is the best candidate as it has parameter with same instance type", method);
500                        // retain only this method in the answer
501                        candidates.clear();
502                        candidates.add(method);
503                        return candidates;
504                    }
505                }
506                // fallback to return what we have found as candidates so far
507                return candidates;
508            }
509        }
510    
511        protected static List<Method> findSetterMethodsOrderedByParameterType(Class<?> target, String propertyName, boolean allowBuilderPattern) {
512            List<Method> answer = new LinkedList<Method>();
513            List<Method> primitives = new LinkedList<Method>();
514            Set<Method> setters = findSetterMethods(target, propertyName, allowBuilderPattern);
515            for (Method setter : setters) {
516                Class parameterType = setter.getParameterTypes()[0];
517                if (PRIMITIVE_CLASSES.contains(parameterType)) {
518                    primitives.add(setter);
519                } else {
520                    answer.add(setter);
521                }
522            }
523            // primitives get added last
524            answer.addAll(primitives);
525            return answer;
526        }
527    
528    }