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