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        public static boolean setProperties(Object target, Map properties, String optionPrefix) throws Exception {
145            ObjectHelper.notNull(target, "target");
146            ObjectHelper.notNull(properties, "properties");
147            boolean rc = false;
148    
149            for (Iterator iter = properties.keySet().iterator(); iter.hasNext();) {
150                String name = (String)iter.next();
151                if (name.startsWith(optionPrefix)) {
152                    Object value = properties.get(name);
153                    name = name.substring(optionPrefix.length());
154                    if (setProperty(target, name, value)) {
155                        iter.remove();
156                        rc = true;
157                    }
158                }
159            }
160            return rc;
161        }
162    
163        @SuppressWarnings("unchecked")
164        public static Map extractProperties(Map properties, String optionPrefix) {
165            ObjectHelper.notNull(properties, "properties");
166    
167            HashMap rc = new LinkedHashMap(properties.size());
168    
169            for (Iterator iter = properties.keySet().iterator(); iter.hasNext();) {
170                String name = (String)iter.next();
171                if (name.startsWith(optionPrefix)) {
172                    Object value = properties.get(name);
173                    name = name.substring(optionPrefix.length());
174                    rc.put(name, value);
175                    iter.remove();
176                }
177            }
178    
179            return rc;
180        }
181    
182        public static boolean setProperties(TypeConverter typeConverter, Object target, Map properties) throws Exception {
183            ObjectHelper.notNull(target, "target");
184            ObjectHelper.notNull(properties, "properties");
185            boolean rc = false;
186    
187            for (Iterator iter = properties.entrySet().iterator(); iter.hasNext();) {
188                Map.Entry entry = (Map.Entry)iter.next();
189                if (setProperty(typeConverter, target, (String)entry.getKey(), entry.getValue())) {
190                    iter.remove();
191                    rc = true;
192                }
193            }
194    
195            return rc;
196        }
197    
198        public static boolean setProperties(Object target, Map props) throws Exception {
199            return setProperties(null, target, props);
200        }
201    
202        public static boolean setProperty(TypeConverter typeConverter, Object target, String name, Object value) throws Exception {
203            try {
204                Class clazz = target.getClass();
205                // find candidates of setter methods as there can be overloaded setters
206                Set<Method> setters = findSetterMethods(typeConverter, clazz, name, value);
207                if (setters.isEmpty()) {
208                    return false;
209                }
210    
211                // loop and execute the best setter method
212                Exception typeConvertionFailed = null;
213                for (Method setter : setters) {
214                    // If the type is null or it matches the needed type, just use the value directly
215                    if (value == null || setter.getParameterTypes()[0].isAssignableFrom(value.getClass())) {
216                        setter.invoke(target, value);
217                        return true;
218                    } else {
219                        // We need to convert it
220                        try {
221                            // ignore exceptions as there could be another setter method where we could type convert successfully
222                            Object convertedValue = convert(typeConverter, setter.getParameterTypes()[0], value);
223                            setter.invoke(target, convertedValue);
224                            return true;
225                        } catch (NoTypeConversionAvailableException e) {
226                            typeConvertionFailed = e;
227                        } catch (IllegalArgumentException e) {
228                            typeConvertionFailed = e;
229                        }
230                        if (LOG.isTraceEnabled()) {
231                            LOG.trace("Setter \"" + setter + "\" with parameter type \""
232                                      + setter.getParameterTypes()[0] + "\" could not be used for type conversions of " + value);
233                        }
234                    }
235                }
236                // we did not find a setter method to use, and if we did try to use a type converter then throw
237                // this kind of exception as the caused by will hint this error
238                if (typeConvertionFailed != null) {
239                    throw new IllegalArgumentException("Could not find a suitable setter for property: " + name
240                            + " as there isn't a setter method with same type: " + value.getClass().getCanonicalName()
241                            + " nor type conversion possible: " + typeConvertionFailed.getMessage());
242                } else {
243                    return false;
244                }
245            } catch (InvocationTargetException e) {
246                // lets unwrap the exception
247                Throwable throwable = e.getCause();
248                if (throwable instanceof Exception) {
249                    Exception exception = (Exception)throwable;
250                    throw exception;
251                } else {
252                    Error error = (Error)throwable;
253                    throw error;
254                }
255            }
256        }
257    
258        public static boolean setProperty(Object target, String name, Object value) throws Exception {
259            return setProperty(null, target, name, value);
260        }
261    
262        @SuppressWarnings("unchecked")
263        private static Object convert(TypeConverter typeConverter, Class type, Object value)
264            throws URISyntaxException, NoTypeConversionAvailableException {
265            if (typeConverter != null) {
266                return typeConverter.mandatoryConvertTo(type, value);
267            }
268            PropertyEditor editor = PropertyEditorManager.findEditor(type);
269            if (editor != null) {
270                editor.setAsText(value.toString());
271                return editor.getValue();
272            }
273            if (type == URI.class) {
274                return new URI(value.toString());
275            }
276            return null;
277        }
278    
279        private static String convertToString(Object value, Class type) throws URISyntaxException {
280            PropertyEditor editor = PropertyEditorManager.findEditor(type);
281            if (editor != null) {
282                editor.setValue(value);
283                return editor.getAsText();
284            }
285            if (type == URI.class) {
286                return value.toString();
287            }
288            return null;
289        }
290    
291        private static Set<Method> findSetterMethods(TypeConverter typeConverter, Class clazz, String name, Object value) {
292            Set<Method> candidates = new LinkedHashSet<Method>();
293    
294            // Build the method name.
295            name = "set" + ObjectHelper.capitalize(name);
296            while (clazz != Object.class) {
297                // Since Object.class.isInstance all the objects,
298                // here we just make sure it will be add to the bottom of the set.
299                Method objectSetMethod = null;
300                Method[] methods = clazz.getMethods();
301                for (Method method : methods) {
302                    Class params[] = method.getParameterTypes();
303                    if (method.getName().equals(name) && params.length == 1) {
304                        Class paramType = params[0];
305                        if (paramType.equals(Object.class)) {                        
306                            objectSetMethod = method;
307                        } else if (typeConverter != null || isSettableType(paramType) || paramType.isInstance(value)) {
308                            candidates.add(method);
309                        }
310                    }
311                }
312                if (objectSetMethod != null) {
313                    candidates.add(objectSetMethod);
314                }
315                clazz = clazz.getSuperclass();
316            }
317    
318            if (candidates.isEmpty()) {
319                return candidates;
320            } else if (candidates.size() == 1) {
321                // only one
322                return candidates;
323            } else {
324                // find the best match if possible
325                if (LOG.isTraceEnabled()) {
326                    LOG.trace("Found " + candidates.size() + " suitable setter methods for setting " + name);
327                }
328                // prefer to use the one with the same instance if any exists
329                for (Method method : candidates) {                               
330                    if (method.getParameterTypes()[0].isInstance(value)) {
331                        if (LOG.isTraceEnabled()) {
332                            LOG.trace("Method " + method + " is the best candidate as it has parameter with same instance type");
333                        }
334                        // retain only this method in the answer
335                        candidates.clear();
336                        candidates.add(method);
337                        return candidates;
338                    }
339                }
340                // fallback to return what we have found as candidates so far
341                return candidates;
342            }
343        }
344    
345        private static boolean isSettableType(Class clazz) {
346            if (PropertyEditorManager.findEditor(clazz) != null) {
347                return true;
348            }
349            if (clazz == URI.class) {
350                return true;
351            }
352            if (clazz == Boolean.class) {
353                return true;
354            }
355            return false;
356        }
357    
358        public static String toString(Object target) {
359            return toString(target, Object.class);
360        }
361    
362        public static String toString(Object target, Class stopClass) {
363            LinkedHashMap map = new LinkedHashMap();
364            addFields(target, target.getClass(), stopClass, map);
365            StringBuffer buffer = new StringBuffer(simpleName(target.getClass()));
366            buffer.append(" {");
367            Set entrySet = map.entrySet();
368            boolean first = true;
369            for (Iterator iter = entrySet.iterator(); iter.hasNext();) {
370                Map.Entry entry = (Map.Entry)iter.next();
371                if (first) {
372                    first = false;
373                } else {
374                    buffer.append(", ");
375                }
376                buffer.append(entry.getKey());
377                buffer.append(" = ");
378                appendToString(buffer, entry.getValue());
379            }
380            buffer.append("}");
381            return buffer.toString();
382        }
383    
384        protected static void appendToString(StringBuffer buffer, Object value) {
385            buffer.append(value);
386        }
387    
388        public static String simpleName(Class clazz) {
389            String name = clazz.getName();
390            int p = name.lastIndexOf(".");
391            if (p >= 0) {
392                name = name.substring(p + 1);
393            }
394            return name;
395        }
396    
397        @SuppressWarnings("unchecked")
398        private static void addFields(Object target, Class startClass, Class stopClass, LinkedHashMap map) {
399            if (startClass != stopClass) {
400                addFields(target, startClass.getSuperclass(), stopClass, map);
401            }
402    
403            Field[] fields = startClass.getDeclaredFields();
404            for (Field field : fields) {
405                if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())
406                    || Modifier.isPrivate(field.getModifiers())) {
407                    continue;
408                }
409    
410                try {
411                    field.setAccessible(true);
412                    Object o = field.get(target);
413                    if (o != null && o.getClass().isArray()) {
414                        try {
415                            o = Arrays.asList((Object[])o);
416                        } catch (Throwable e) {
417                            // ignore
418                        }
419                    }
420                    map.put(field.getName(), o);
421                } catch (Throwable e) {
422                    throw ObjectHelper.wrapRuntimeCamelException(e);
423                }
424            }
425        }
426    
427    }