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.Iterator;
029    import java.util.LinkedHashMap;
030    import java.util.LinkedHashSet;
031    import java.util.List;
032    import java.util.Locale;
033    import java.util.Map;
034    import java.util.Set;
035    import java.util.regex.Pattern;
036    
037    import org.apache.camel.NoTypeConversionAvailableException;
038    import org.apache.camel.TypeConverter;
039    import org.slf4j.Logger;
040    import org.slf4j.LoggerFactory;
041    
042    /**
043     * Helper for introspections of beans.
044     */
045    public final class IntrospectionSupport {
046    
047        private static final transient Logger LOG = LoggerFactory.getLogger(IntrospectionSupport.class);
048        private static final Pattern GETTER_PATTERN = Pattern.compile("(get|is)[A-Z].*");
049        private static final Pattern SETTER_PATTERN = Pattern.compile("set[A-Z].*");
050        private static final List<Method> EXCLUDED_METHODS = new ArrayList<Method>();
051    
052        static {
053            // exclude all java.lang.Object methods as we dont want to invoke them
054            EXCLUDED_METHODS.addAll(Arrays.asList(Object.class.getMethods()));
055            // exclude all java.lang.reflect.Proxy methods as we dont want to invoke them
056            EXCLUDED_METHODS.addAll(Arrays.asList(Proxy.class.getMethods()));
057        }
058    
059        /**
060         * Utility classes should not have a public constructor.
061         */
062        private IntrospectionSupport() {
063        }
064    
065        public static boolean isGetter(Method method) {
066            String name = method.getName();
067            Class<?> type = method.getReturnType();
068            Class<?> params[] = method.getParameterTypes();
069    
070            if (!GETTER_PATTERN.matcher(name).matches()) {
071                return false;
072            }
073    
074            // special for isXXX boolean
075            if (name.startsWith("is")) {
076                return params.length == 0 && type.getSimpleName().equalsIgnoreCase("boolean");
077            }
078    
079            return params.length == 0 && !type.equals(Void.TYPE);
080        }
081    
082        public static String getGetterShorthandName(Method method) {
083            if (!isGetter(method)) {
084                return method.getName();
085            }
086    
087            String name = method.getName();
088            if (name.startsWith("get")) {
089                name = name.substring(3);
090                name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
091            } else if (name.startsWith("is")) {
092                name = name.substring(2);
093                name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
094            }
095    
096            return name;
097        }
098    
099        public static String getSetterShorthandName(Method method) {
100            if (!isSetter(method)) {
101                return method.getName();
102            }
103    
104            String name = method.getName();
105            if (name.startsWith("set")) {
106                name = name.substring(3);
107                name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
108            }
109    
110            return name;
111        }
112    
113        public static boolean isSetter(Method method, boolean allowBuilderPattern) {
114            String name = method.getName();
115            Class<?> type = method.getReturnType();
116            Class<?> params[] = method.getParameterTypes();
117    
118            if (!SETTER_PATTERN.matcher(name).matches()) {
119                return false;
120            }
121    
122            return params.length == 1 && (type.equals(Void.TYPE) || (allowBuilderPattern && method.getDeclaringClass().isAssignableFrom(type)));
123        }
124        
125        public static boolean isSetter(Method method) {
126            return isSetter(method, false);
127        }
128    
129        /**
130         * Will inspect the target for properties.
131         * <p/>
132         * Notice a property must have both a getter/setter method to be included.
133         *
134         * @param target         the target bean
135         * @param properties     the map to fill in found properties
136         * @param optionPrefix   an optional prefix to append the property key
137         * @return <tt>true</tt> if any properties was found, <tt>false</tt> otherwise.
138         */
139        public static boolean getProperties(Object target, Map<String, Object> properties, String optionPrefix) {
140            ObjectHelper.notNull(target, "target");
141            ObjectHelper.notNull(properties, "properties");
142            boolean rc = false;
143            if (optionPrefix == null) {
144                optionPrefix = "";
145            }
146    
147            Class<?> clazz = target.getClass();
148            Method[] methods = clazz.getMethods();
149            for (Method method : methods) {
150                if (EXCLUDED_METHODS.contains(method)) {
151                    continue;
152                }
153                try {
154                    // must be properties which have setters
155                    if (isGetter(method) && hasSetter(target, method)) {
156                        Object value = method.invoke(target);
157                        String name = getGetterShorthandName(method);
158                        properties.put(optionPrefix + name, value);
159                        rc = true;
160                    }
161                } catch (Exception e) {
162                    // ignore
163                }
164            }
165    
166            return rc;
167        }
168    
169        public static boolean hasSetter(Object target, Method getter) {
170            String name = getGetterShorthandName(getter);
171    
172            Class<?> clazz = target.getClass();
173            Method[] methods = clazz.getMethods();
174            for (Method method : methods) {
175                if (EXCLUDED_METHODS.contains(method)) {
176                    continue;
177                }
178                if (isSetter(method)) {
179                    if (name.equals(getSetterShorthandName(method))) {
180                        return true;
181                    }
182                }
183            }
184    
185            return false;
186        }
187    
188        public static boolean hasProperties(Map<String, Object> properties, String optionPrefix) {
189            ObjectHelper.notNull(properties, "properties");
190    
191            if (ObjectHelper.isNotEmpty(optionPrefix)) {
192                for (Object o : properties.keySet()) {
193                    String name = (String) o;
194                    if (name.startsWith(optionPrefix)) {
195                        return true;
196                    }
197                }
198                // no parameters with this prefix
199                return false;
200            } else {
201                return !properties.isEmpty();
202            }
203        }
204    
205        public static Object getProperty(Object target, String property) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
206            ObjectHelper.notNull(target, "target");
207            ObjectHelper.notNull(property, "property");
208    
209            property = property.substring(0, 1).toUpperCase(Locale.ENGLISH) + property.substring(1);
210    
211            Class<?> clazz = target.getClass();
212            Method method = getPropertyGetter(clazz, property);
213            return method.invoke(target);
214        }
215    
216        public static Method getPropertyGetter(Class<?> type, String propertyName) throws NoSuchMethodException {
217            if (isPropertyIsGetter(type, propertyName)) {
218                return type.getMethod("is" + ObjectHelper.capitalize(propertyName));
219            } else {
220                return type.getMethod("get" + ObjectHelper.capitalize(propertyName));
221            }
222        }
223    
224        public static Method getPropertySetter(Class<?> type, String propertyName) throws NoSuchMethodException {
225            String name = "set" + ObjectHelper.capitalize(propertyName);
226            for (Method method : type.getMethods()) {
227                if (isSetter(method) && method.getName().equals(name)) {
228                    return method;
229                }
230            }
231            throw new NoSuchMethodException(type.getCanonicalName() + "." + name);
232        }
233    
234        public static boolean isPropertyIsGetter(Class<?> type, String propertyName) {
235            try {
236                Method method = type.getMethod("is" + ObjectHelper.capitalize(propertyName));
237                if (method != null) {
238                    return method.getReturnType().isAssignableFrom(boolean.class) || method.getReturnType().isAssignableFrom(Boolean.class);
239                }
240            } catch (NoSuchMethodException e) {
241                // ignore
242            }
243            return false;
244        }
245        
246        public static boolean setProperties(Object target, Map<String, Object> properties, String optionPrefix, boolean allowBuilderPattern) throws Exception {
247            ObjectHelper.notNull(target, "target");
248            ObjectHelper.notNull(properties, "properties");
249            boolean rc = false;
250    
251            for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
252                Map.Entry<String, Object> entry = it.next();
253                String name = entry.getKey().toString();
254                if (name.startsWith(optionPrefix)) {
255                    Object value = properties.get(name);
256                    name = name.substring(optionPrefix.length());
257                    if (setProperty(target, name, value, allowBuilderPattern)) {
258                        it.remove();
259                        rc = true;
260                    }
261                }
262            }
263            
264            return rc;
265        }
266    
267        public static boolean setProperties(Object target, Map<String, Object> properties, String optionPrefix) throws Exception {
268            return setProperties(target, properties, optionPrefix, false);
269        }
270    
271        public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) {
272            ObjectHelper.notNull(properties, "properties");
273    
274            Map<String, Object> rc = new LinkedHashMap<String, Object>(properties.size());
275    
276            for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
277                Map.Entry<String, Object> entry = it.next();
278                String name = entry.getKey();
279                if (name.startsWith(optionPrefix)) {
280                    Object value = properties.get(name);
281                    name = name.substring(optionPrefix.length());
282                    rc.put(name, value);
283                    it.remove();
284                }
285            }
286    
287            return rc;
288        }
289    
290        public static boolean setProperties(TypeConverter typeConverter, Object target, Map<String, Object> properties) throws Exception {
291            ObjectHelper.notNull(target, "target");
292            ObjectHelper.notNull(properties, "properties");
293            boolean rc = false;
294    
295            for (Iterator<Map.Entry<String, Object>> iter = properties.entrySet().iterator(); iter.hasNext();) {
296                Map.Entry<String, Object> entry = iter.next();
297                if (setProperty(typeConverter, target, entry.getKey(), entry.getValue())) {
298                    iter.remove();
299                    rc = true;
300                }
301            }
302    
303            return rc;
304        }
305    
306        public static boolean setProperties(Object target, Map<String, Object> properties) throws Exception {
307            return setProperties(null, target, properties);
308        }
309    
310        public static boolean setProperty(TypeConverter typeConverter, Object target, String name, Object value, boolean allowBuilderPattern) throws Exception {
311            try {
312                Class<?> clazz = target.getClass();
313                // find candidates of setter methods as there can be overloaded setters
314                Set<Method> setters = findSetterMethods(typeConverter, clazz, name, value, allowBuilderPattern);
315                if (setters.isEmpty()) {
316                    return false;
317                }
318    
319                // loop and execute the best setter method
320                Exception typeConversionFailed = null;
321                for (Method setter : setters) {
322                    // If the type is null or it matches the needed type, just use the value directly
323                    if (value == null || setter.getParameterTypes()[0].isAssignableFrom(value.getClass())) {
324                        setter.invoke(target, value);
325                        return true;
326                    } else {
327                        // We need to convert it
328                        try {
329                            // ignore exceptions as there could be another setter method where we could type convert successfully
330                            Object convertedValue = convert(typeConverter, setter.getParameterTypes()[0], value);
331                            setter.invoke(target, convertedValue);
332                            return true;
333                        } catch (NoTypeConversionAvailableException e) {
334                            typeConversionFailed = e;
335                        } catch (IllegalArgumentException e) {
336                            typeConversionFailed = e;
337                        }
338                        if (LOG.isTraceEnabled()) {
339                            LOG.trace("Setter \"{}\" with parameter type \"{}\" could not be used for type conversions of {}",
340                                    new Object[]{setter, setter.getParameterTypes()[0], value});
341                        }
342                    }
343                }
344                // we did not find a setter method to use, and if we did try to use a type converter then throw
345                // this kind of exception as the caused by will hint this error
346                if (typeConversionFailed != null) {
347                    throw new IllegalArgumentException("Could not find a suitable setter for property: " + name
348                            + " as there isn't a setter method with same type: " + value.getClass().getCanonicalName()
349                            + " nor type conversion possible: " + typeConversionFailed.getMessage());
350                } else {
351                    return false;
352                }
353            } catch (InvocationTargetException e) {
354                // lets unwrap the exception
355                Throwable throwable = e.getCause();
356                if (throwable instanceof Exception) {
357                    Exception exception = (Exception)throwable;
358                    throw exception;
359                } else {
360                    Error error = (Error)throwable;
361                    throw error;
362                }
363            }
364        }
365        
366        public static boolean setProperty(TypeConverter typeConverter, Object target, String name, Object value) throws Exception {
367            return setProperty(typeConverter, target, name, value, false);
368        }
369        
370        public static boolean setProperty(Object target, String name, Object value, boolean allowBuilderPattern) throws Exception {
371            return setProperty(null, target, name, value, allowBuilderPattern);
372        }
373    
374        public static boolean setProperty(Object target, String name, Object value) throws Exception {
375            return setProperty(target, name, value, false);
376        }
377    
378        private static Object convert(TypeConverter typeConverter, Class<?> type, Object value)
379            throws URISyntaxException, NoTypeConversionAvailableException {
380            if (typeConverter != null) {
381                return typeConverter.mandatoryConvertTo(type, value);
382            }
383            PropertyEditor editor = PropertyEditorManager.findEditor(type);
384            if (editor != null) {
385                editor.setAsText(value.toString());
386                return editor.getValue();
387            }
388            if (type == URI.class) {
389                return new URI(value.toString());
390            }
391            return null;
392        }
393        
394        private static Set<Method> findSetterMethods(TypeConverter typeConverter, Class<?> clazz, String name, Object value, boolean allowBuilderPattern) {
395            Set<Method> candidates = new LinkedHashSet<Method>();
396    
397            // Build the method name.
398            name = "set" + ObjectHelper.capitalize(name);
399            while (clazz != Object.class) {
400                // Since Object.class.isInstance all the objects,
401                // here we just make sure it will be add to the bottom of the set.
402                Method objectSetMethod = null;
403                Method[] methods = clazz.getMethods();
404                for (Method method : methods) {
405                    Class<?> params[] = method.getParameterTypes();
406                    if (method.getName().equals(name) && params.length == 1) {
407                        Class<?> paramType = params[0];
408                        if (paramType.equals(Object.class)) {                        
409                            objectSetMethod = method;
410                        } else if (typeConverter != null || isSetter(method, allowBuilderPattern) || paramType.isInstance(value)) {
411                            candidates.add(method);
412                        }
413                    }
414                }
415                if (objectSetMethod != null) {
416                    candidates.add(objectSetMethod);
417                }
418                clazz = clazz.getSuperclass();
419            }
420    
421            if (candidates.isEmpty()) {
422                return candidates;
423            } else if (candidates.size() == 1) {
424                // only one
425                return candidates;
426            } else {
427                // find the best match if possible
428                LOG.trace("Found {} suitable setter methods for setting {}", candidates.size(), name);
429                // prefer to use the one with the same instance if any exists
430                for (Method method : candidates) {                               
431                    if (method.getParameterTypes()[0].isInstance(value)) {
432                        LOG.trace("Method {} is the best candidate as it has parameter with same instance type", method);
433                        // retain only this method in the answer
434                        candidates.clear();
435                        candidates.add(method);
436                        return candidates;
437                    }
438                }
439                // fallback to return what we have found as candidates so far
440                return candidates;
441            }
442        }
443        
444    }