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) throws URISyntaxException {
264            if (typeConverter != null) {
265                return typeConverter.convertTo(type, value);
266            }
267            PropertyEditor editor = PropertyEditorManager.findEditor(type);
268            if (editor != null) {
269                editor.setAsText(value.toString());
270                return editor.getValue();
271            }
272            if (type == URI.class) {
273                return new URI(value.toString());
274            }
275            return null;
276        }
277    
278        private static String convertToString(Object value, Class type) throws URISyntaxException {
279            PropertyEditor editor = PropertyEditorManager.findEditor(type);
280            if (editor != null) {
281                editor.setValue(value);
282                return editor.getAsText();
283            }
284            if (type == URI.class) {
285                return value.toString();
286            }
287            return null;
288        }
289    
290        private static Set<Method> findSetterMethods(TypeConverter typeConverter, Class clazz, String name, Object value) {
291            Set<Method> candidates = new LinkedHashSet<Method>();
292    
293            // Build the method name.
294            name = "set" + ObjectHelper.capitalize(name);
295            while (clazz != Object.class) {
296                // Since Object.class.isInstance all the objects,
297                // here we just make sure it will be add to the bottom of the set.
298                Method objectSetMethod = null;
299                Method[] methods = clazz.getMethods();
300                for (Method method : methods) {
301                    Class params[] = method.getParameterTypes();
302                    if (method.getName().equals(name) && params.length == 1) {
303                        Class paramType = params[0];
304                        if (paramType.equals(Object.class)) {                        
305                            objectSetMethod = method;
306                        } else if (typeConverter != null || isSettableType(paramType) || paramType.isInstance(value)) {
307                            candidates.add(method);
308                        }
309                    }
310                }
311                if (objectSetMethod != null) {
312                    candidates.add(objectSetMethod);
313                }
314                clazz = clazz.getSuperclass();
315            }
316    
317            if (candidates.isEmpty()) {
318                return candidates;
319            } else if (candidates.size() == 1) {
320                // only one
321                return candidates;
322            } else {
323                // find the best match if possible
324                if (LOG.isTraceEnabled()) {
325                    LOG.trace("Found " + candidates.size() + " suitable setter methods for setting " + name);
326                }
327                // prefer to use the one with the same instance if any exists
328                for (Method method : candidates) {                               
329                    if (method.getParameterTypes()[0].isInstance(value)) {
330                        if (LOG.isTraceEnabled()) {
331                            LOG.trace("Method " + method + " is the best candidate as it has parameter with same instance type");
332                        }
333                        // retain only this method in the answer
334                        candidates.clear();
335                        candidates.add(method);
336                        return candidates;
337                    }
338                }
339                // fallback to return what we have found as candidates so far
340                return candidates;
341            }
342        }
343    
344        private static boolean isSettableType(Class clazz) {
345            if (PropertyEditorManager.findEditor(clazz) != null) {
346                return true;
347            }
348            if (clazz == URI.class) {
349                return true;
350            }
351            if (clazz == Boolean.class) {
352                return true;
353            }
354            return false;
355        }
356    
357        public static String toString(Object target) {
358            return toString(target, Object.class);
359        }
360    
361        public static String toString(Object target, Class stopClass) {
362            LinkedHashMap map = new LinkedHashMap();
363            addFields(target, target.getClass(), stopClass, map);
364            StringBuffer buffer = new StringBuffer(simpleName(target.getClass()));
365            buffer.append(" {");
366            Set entrySet = map.entrySet();
367            boolean first = true;
368            for (Iterator iter = entrySet.iterator(); iter.hasNext();) {
369                Map.Entry entry = (Map.Entry)iter.next();
370                if (first) {
371                    first = false;
372                } else {
373                    buffer.append(", ");
374                }
375                buffer.append(entry.getKey());
376                buffer.append(" = ");
377                appendToString(buffer, entry.getValue());
378            }
379            buffer.append("}");
380            return buffer.toString();
381        }
382    
383        protected static void appendToString(StringBuffer buffer, Object value) {
384            buffer.append(value);
385        }
386    
387        public static String simpleName(Class clazz) {
388            String name = clazz.getName();
389            int p = name.lastIndexOf(".");
390            if (p >= 0) {
391                name = name.substring(p + 1);
392            }
393            return name;
394        }
395    
396        @SuppressWarnings("unchecked")
397        private static void addFields(Object target, Class startClass, Class stopClass, LinkedHashMap map) {
398            if (startClass != stopClass) {
399                addFields(target, startClass.getSuperclass(), stopClass, map);
400            }
401    
402            Field[] fields = startClass.getDeclaredFields();
403            for (Field field : fields) {
404                if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())
405                    || Modifier.isPrivate(field.getModifiers())) {
406                    continue;
407                }
408    
409                try {
410                    field.setAccessible(true);
411                    Object o = field.get(target);
412                    if (o != null && o.getClass().isArray()) {
413                        try {
414                            o = Arrays.asList((Object[])o);
415                        } catch (Throwable e) {
416                            // ignore
417                        }
418                    }
419                    map.put(field.getName(), o);
420                } catch (Throwable e) {
421                    throw ObjectHelper.wrapRuntimeCamelException(e);
422                }
423            }
424        }
425    
426    }