View Javadoc

1   /*
2    * Copyright (c) 2005-2007 Creative Sphere Limited.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v1.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v10.html
7    *
8    * Contributors:
9    *
10   *   Creative Sphere - initial API and implementation
11   *
12   */
13  package org.abstracthorizon.danube.beanconsole;
14  
15  import java.beans.BeanInfo;
16  import java.beans.IntrospectionException;
17  import java.beans.Introspector;
18  import java.beans.MethodDescriptor;
19  import java.beans.PropertyDescriptor;
20  import java.beans.PropertyEditor;
21  import java.beans.PropertyEditorManager;
22  import java.io.PrintWriter;
23  import java.io.StringWriter;
24  import java.lang.reflect.Array;
25  import java.lang.reflect.Method;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.HashSet;
29  import java.util.Iterator;
30  import java.util.LinkedHashSet;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Set;
34  
35  import org.springframework.beans.BeansException;
36  import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
37  import org.springframework.beans.factory.support.AbstractBeanDefinition;
38  import org.springframework.context.ApplicationContext;
39  import org.springframework.context.ConfigurableApplicationContext;
40  
41  /**
42   * Helper class for manipulation of beans.
43   *
44   * @author Daniel Sendula
45   */
46  public class BeanHelper {
47      /** Empty object array */
48      public static final Object[] EMPTY_ARRAY = new Object[0];
49  
50      /**
51       * This method navigates through objects from given root object to the object specified by path.
52       * <p>
53       *   Algorithm for navigating through objects is as following (in that order):
54       * </p>
55       * <ul>
56       *   <li>If path element is numeric then it is expected for object to be a list {@link List} or an array.</li>
57       *   <li>If current object is of {@link ApplicationContext} type then a bean of path element's name is tried to be retrieved.</li>
58       *   <li>If current object is a map ({@link Map}) then an entry of path's entry name is fetched</li>
59       *   <li>Bean inspector is used for getter method of given path name attribute to be invoked.</li>
60       *   <li></li>
61       * </ul>
62       * <p>
63       *   Each step is performed if previous returns <code>null</code> (or an exception that is in this case ignored).
64       * </p>
65       * @param from root object
66       * @param path path
67       * @return resulted object
68       *
69       * @throws BeanAccessException if there is a problem in dereferencing
70       */
71      @SuppressWarnings("unchecked")
72      public static Object navigate(Object from, String path)
73          throws BeanAccessException {
74  
75          if (path.length() == 0) {
76              return from;
77          }
78  
79          Object current = from;
80          int l = 0;
81          if (path.startsWith("/")) {
82              l = 1;
83          }
84          while (l < path.length()) {
85              int i = path.indexOf('/', l);
86              String p;
87              if (i >= 0) {
88                  p = path.substring(l, i);
89                  l = i + 1;
90              } else {
91                  p = path.substring(l);
92                  l = path.length();
93              }
94  
95              Object next = null;
96              if ((p.length() > 2) && p.startsWith("[") && p.endsWith("]")) {
97                  p = p.substring(1, p.length() - 1);
98                  try {
99                      int n = Integer.parseInt(p);
100                     if (n < 0) {
101                         throw new BeanAccessException(path.substring(1, l), new IndexOutOfBoundsException(p));
102                     }
103                     if (current instanceof List) {
104                         next = ((List<?>)current).get(n);
105                     } else if (current.getClass().isArray()) {
106                         next = Array.get(current, n);
107                     } else if (current instanceof Collection) {
108                         int ii = -1;
109                         Iterator<?> iterator = ((Collection)current).iterator();
110                         while (iterator.hasNext() && (ii != n)) {
111                             Object o = iterator.next();
112                             ii++;
113                             if (ii == n) {
114                                 next = o;
115                             }
116                         }
117                     }
118                 } catch (NumberFormatException e) {
119                     if (current instanceof ApplicationContext) {
120                         ApplicationContext context = (ApplicationContext)current;
121                         try {
122                             Object bean = context.getBean(p);
123                             next = bean;
124                         } catch (BeansException ignore) {
125                         }
126                     }
127                     if ((next == null) && (current instanceof Map)) {
128                         next = ((Map<?, ?>)current).get(p);
129                         if (next == null) {
130                             Iterator<Map.Entry> it = ((Map)current).entrySet().iterator();
131                             while (it.hasNext() && (next == null)) {
132                                 Map.Entry entry = it.next();
133                                 if ((entry.getKey() != null) && p.equals(entry.getKey().toString())) {
134                                     next = entry.getValue();
135                                 }
136                             }
137                         }
138                         
139                     }
140                 }
141             } else {
142                 try {
143                     BeanInfo beanInfo = Introspector.getBeanInfo(current.getClass());
144                     for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
145                         if (propertyDescriptor.getName().equals(p)) {
146                             Method readMethod = propertyDescriptor.getReadMethod();
147                             if (readMethod == null) {
148                                 return null;
149                             }
150                             try {
151                                 next = readMethod.invoke(current, EMPTY_ARRAY);
152                                 break;
153                             } catch (Exception ei) {
154                                 throw new BeanAccessException(path.substring(1, l), ei);
155                             }
156                         }
157                     }
158                 } catch (IntrospectionException ex) {
159                     throw new BeanAccessException(path.substring(1, l), new RuntimeException(ex));
160                 }
161             }
162 
163             if (next == null) {
164                 throw new BeanAccessException(path.substring(1, l), new IllegalArgumentException("Unknown attribute '" + p + "'"));
165             } else {
166                 current = next;
167             }
168             i = i + 1;
169         }
170         return current;
171     }
172 
173     /**
174      * This method set model parameters needed for preseting given object.
175      * It sets following model parameters:
176      * <ul>
177      *   <li>beans - list of {@link BeanDef} objects - if object is of {@link ConfigurableApplicationContext} type.</li>
178      *   <li>map - list of {@link BeanDef} objects - if object is of {@link Map} type.</li>
179      *   <li>collection - list of {@link BeanDef} objects - if object is of {@link Collection} type.</li>
180      *   <li>properties - list of {@link BeanDef} objects - object accessible properties.</li>
181      *   <li>methods - list of {@link MethodDescriptor} object - methods that are not used for properties' access.</li>
182      * </lu>
183      *
184      * @param object
185      * @param result
186      */
187     @SuppressWarnings("unchecked")
188     public static void prepare(Object object, Map<String, Object> result) {
189         Set<BeanDef> properties = new LinkedHashSet<BeanDef>();
190         Set<MethodDescriptor> methods = new LinkedHashSet<MethodDescriptor>();
191 
192         result.put("properties", properties);
193         result.put("methods", methods);
194 
195         if (object instanceof ConfigurableApplicationContext) {
196             Set<BeanDef> beans = new LinkedHashSet<BeanDef>();
197             ConfigurableListableBeanFactory factory = ((ConfigurableApplicationContext)object).getBeanFactory();
198             Set<String> names = new HashSet<String>();
199 
200             String[] beanNames = factory.getBeanDefinitionNames();
201             for (String beanName : beanNames) {
202                 AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition)factory.getBeanDefinition(beanName);
203                 String access = null;
204                 String value = null;
205                 String type = null;
206                 boolean followable = true;
207                 if (beanDefinition.isSingleton()) {
208                     access = BeanDef.RO;
209                     Object bean = factory.getBean(beanName);
210                     value = toString(bean);
211                     type = toTypeString(bean);
212                     followable = isFollowable(bean);
213                 } else {
214                     access = BeanDef.NO_ACCESS;
215                     value = "<no access - not a singleton bean>";
216                     type = beanDefinition.getBeanClassName();
217                     followable = false;
218                 }
219                 beans.add(new BeanDef(beanName, "", type, access, value, followable));
220                 names.add(beanName);
221             }
222 
223             beanNames = factory.getSingletonNames();
224             for (String beanName : beanNames) {
225                 if (!names.contains(beanName)) {
226                     String access = BeanDef.RO;
227                     Object bean = factory.getBean(beanName);
228                     String value = toString(bean);
229                     String type = toTypeString(bean);
230                     boolean followable = isFollowable(bean);
231                     beans.add(new BeanDef(beanName, "", type, access, value, followable));
232                 }
233             }
234 
235             result.put("beans", beans);
236         } else if (object instanceof Map) {
237             Set<BeanDef> elements = new LinkedHashSet<BeanDef>();
238             Map<Object, Object> map = (Map<Object, Object>)object;
239             for (Map.Entry<Object, Object> entry : map.entrySet()) {
240                 String key = toString(entry.getKey());
241                 Object val = entry.getValue();
242                 String value = toString(val);
243                 String type = toTypeString(val);
244                 boolean followable = isFollowable(val);
245                 BeanDef def = new BeanDef(key, "", type, BeanDef.RO, value, followable);
246                 elements.add(def);
247             }
248             result.put("map", elements);
249         } else if (object instanceof Collection) {
250             Set<BeanDef> elements = new LinkedHashSet<BeanDef>();
251             for (Object obj : (Collection<?>)object) {
252                 String value = toString(obj);
253                 String type = toTypeString(obj);
254                 boolean followable = isFollowable(obj);
255                 BeanDef def = new BeanDef(null, "", type, BeanDef.RO, value, followable);
256                 elements.add(def);
257             }
258             result.put("collection", elements);
259         } else if (object instanceof Object[]) {
260             Set<BeanDef> elements = new LinkedHashSet<BeanDef>();
261             for (Object obj : (Object[])object) {
262                 String value = toString(obj);
263                 String type = toTypeString(obj);
264                 boolean followable = isFollowable(obj);
265                 BeanDef def = new BeanDef(null, "", type, BeanDef.RO, value, followable);
266                 elements.add(def);
267             }
268             result.put("collection", elements);
269         }
270 
271         try {
272             BeanInfo beanInfo = Introspector.getBeanInfo(object.getClass());
273             for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
274                 String name = propertyDescriptor.getDisplayName();
275                 String access = getAccessType(propertyDescriptor);
276                 if (!BeanDef.WO.equals(access) && !BeanDef.WO_RAW.equals(access)) {
277                     Class<?> cls = propertyDescriptor.getPropertyType();
278                     String type = toTypeString(cls);
279                     String value = null;
280                     boolean followable = false;
281                     if (BeanDef.RW.equals(access) || BeanDef.RO.equals(access) || BeanDef.RW_RAW.equals(access) || BeanDef.RO_RAW.equals(access)) {
282                         Object val = getPropertyValue(object, propertyDescriptor);
283                         value = toString(val);
284                         followable = isFollowable(cls);
285                     } else {
286                         value = "";
287                     }
288                     BeanDef def = new BeanDef(name, propertyDescriptor.getShortDescription(), type, access, value, followable);
289                     properties.add(def);
290                 }
291             }
292 
293             for (MethodDescriptor methodDescriptor : getMethodDescriptors(beanInfo)) {
294                 methods.add(methodDescriptor);
295             }
296         } catch (IntrospectionException e) {
297             // TODO Auto-generated catch block
298             e.printStackTrace();
299         }
300     }
301 
302     /**
303      * Presents given object as a string.
304      *
305      * @param object object
306      * @return string representation of the object
307      */
308     public static String toString(Object object) {
309         if (object == null) {
310             return "&lt;null&gt;";
311         }
312         PropertyEditor propertyEditor = PropertyEditorManager.findEditor(object.getClass());
313         if (propertyEditor == null) {
314             return object.toString();
315         }
316         propertyEditor.setValue(object);
317         return propertyEditor.getAsText();
318 
319     }
320 
321     /**
322      * Presetns a class as a string
323      *
324      * @param type the class
325      * @return string representation of the class
326      */
327      public static String toTypeString(Class<?> type) {
328         if (type.isArray()) {
329             return "[" + type.getComponentType().getName() + "]";
330         } else {
331             return type.getName();
332         }
333     }
334 
335     /**result.toString()
336      * Presetns the object's class as a string
337      *
338      * @param object  the class
339      * @return string representation of the object's class
340      */
341     public static String toTypeString(Object object) {
342         if (object == null) {
343             return "&lt;null&gt;";
344         } else {
345             return toTypeString(object.getClass());
346         }
347     }
348 
349     /**
350      * Creates name of the bean from the given path.
351      *
352      * @param path the path
353      * @return name of the bean as last part of the path
354      */
355     protected static String createName(String path) {
356         if (path.length() == 0) {
357             return "Root Bean";
358         } else {
359             int i = path.lastIndexOf('/');
360             if (i > 0) {
361                 return path.substring(path.lastIndexOf("/") + 1);
362             } else {
363                 return path.substring(1);
364             }
365         }
366     }
367 
368     /**
369      * Creates path separated with &quot;.&quot;. If path element contains dot then it is
370      * going to be enclosed in &lt; and &gt; symbols.
371      * @param path the path
372      * @return string representation of the path
373      */
374     public static String createPath(String path) {
375         if (path.length() == 0) {
376             return "/";
377         }
378         return path;
379     }
380 
381     /**
382      * This method ensures that component path is or empty string or a path that starts and ends with &quot;/&quot;
383      * @param path component path
384      * @return updated path
385      */
386     public static String createResourcePath(String path) {
387         if ((path == null) || "/".equals(path)) {
388             return "";
389         }
390         if (!path.startsWith("/")) {
391             path = "/" + path;
392         }
393         if (path.endsWith("/")) {
394             return path.substring(0, path.length() - 1);
395         } else {
396             return path;
397         }
398     }
399 
400     /**
401      * Converts &quot;/&quot; delimited string into the array
402      * @param path &quot;/&quot; delimited
403      * @return an array of elements
404      */
405     public static String[] convertPath(String path) {
406         if ((path == null) || "".equals(path) || "/".equals(path)) {
407             return new String[0];
408         }
409 
410         int i = 0;
411         int l = 0;
412         if (path.startsWith("/")) {
413             i = path.indexOf('/', 1);
414             l = 1;
415         } else {
416             i = path.indexOf('/');
417         }
418         if (i >= 0) {
419             ArrayList<String> pathList = new ArrayList<String>();
420             while (i > 0) {
421                 pathList.add(path.substring(l, i));
422                 l = i + 1;
423                 i = path.indexOf('/', i+1);
424             }
425             pathList.add(path.substring(l));
426             String[] ps = new String[pathList.size()];
427             return pathList.toArray(ps);
428         } else {
429             return new String[]{path};
430         }
431     }
432 
433     /**
434      * Returns access type as in {@link BeanDef} for the given property
435      *
436      * @param property the property
437      * @return access type as in {@link BeanDef}
438      */
439     public static String getAccessType(PropertyDescriptor property) {
440         Method readMethod = property.getReadMethod();
441         Method writeMethod = property.getWriteMethod();
442         if ((readMethod != null) && (writeMethod != null)) {
443             if (PropertyEditorManager.findEditor(readMethod.getReturnType()) != null) {
444                 return BeanDef.RW;
445             } else {
446                 return BeanDef.RW_RAW;
447             }
448         } else if (readMethod != null) {
449             if (PropertyEditorManager.findEditor(readMethod.getReturnType()) != null) {
450                 return BeanDef.RO;
451             } else {
452                 return BeanDef.RO_RAW;
453             }
454         } else if (writeMethod != null) {
455             if (PropertyEditorManager.findEditor(writeMethod.getParameterTypes()[0]) != null) {
456                 return BeanDef.WO;
457             } else {
458                 return BeanDef.WO_RAW;
459             }
460         }
461         return BeanDef.NO_ACCESS;
462     }
463 
464     /**
465      * Returns property's value of the given bean
466      *
467      * @param bean the bean
468      * @param property the property
469      * @return dereferenced property
470      */
471     public static Object getPropertyValue(Object bean, PropertyDescriptor property) {
472         Method readMethod = property.getReadMethod();
473         if (readMethod == null) {
474             return null;
475         }
476         try {
477             return readMethod.invoke(bean, EMPTY_ARRAY);
478         } catch (Exception e) {
479             StringWriter res = new StringWriter();
480             res.append("Error on read method: " + readMethod.toString() + "\n");
481             e.printStackTrace(new PrintWriter(res));
482             return res.toString();
483         }
484     }
485 
486     /**
487      * Returns property's value as a string. It uses {@link #toString()} method.
488      *
489      * @param object the bean
490      * @param property the property
491      * @return property's value as a string
492      */
493     public static String getPropertyValueAsString(Object object, String propertyName) {
494         try {
495             BeanInfo beanInfo = Introspector.getBeanInfo(object.getClass());
496 
497             for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
498                 String name = propertyDescriptor.getDisplayName();
499                 if (name.equals(propertyName)) {
500                     return getPropertyValueAsString(object, propertyDescriptor);
501                 }
502             }
503         } catch (IntrospectionException e) {
504             // TODO Auto-generated catch block
505             e.printStackTrace();
506         }
507         return null;
508     }
509 
510     /**
511      * Returns property's value as a string. It uses {@link #toString()} method.
512      *
513      * @param bean the bean
514      * @param property the property
515      * @return property's value as a string
516      */
517     public static String getPropertyValueAsString(Object bean, PropertyDescriptor property) {
518         Method readMethod = property.getReadMethod();
519         if (readMethod == null) {
520             return "";
521         }
522         try {
523             Object value = readMethod.invoke(bean, EMPTY_ARRAY);
524             return toString(value);
525         } catch (Exception e) {
526             StringWriter res = new StringWriter();
527             res.append("Error on read method: " + readMethod.toString() + "\n");
528             e.printStackTrace(new PrintWriter(res));
529             return res.toString();
530         }
531     }
532 
533     /**
534      * This method returns collection of method descriptors for given beanInfo.
535      * It filters out properties (getter and setter method) as well as methods
536      * whose parameters do not have property editors
537      * @param beanInfo
538      * @return collection of methods
539      */
540     @SuppressWarnings("unchecked")
541     public static Collection<MethodDescriptor> getMethodDescriptors(BeanInfo beanInfo) {
542         Set<Method> attributes = new HashSet<Method>();
543         for (PropertyDescriptor property : beanInfo.getPropertyDescriptors()) {
544             Method readMethod = property.getReadMethod();
545             if (readMethod != null) {
546                 attributes.add(readMethod);
547             }
548             Method writeMethod = property.getWriteMethod();
549             if ((writeMethod != null) && (readMethod != null)) {
550                 attributes.add(writeMethod);
551             }
552         }
553 
554         ArrayList<MethodDescriptor> result = new ArrayList<MethodDescriptor>();
555 
556         for (MethodDescriptor methodDescriptor : beanInfo.getMethodDescriptors()) {
557             Method method = methodDescriptor.getMethod();
558             if (!attributes.contains(method)) {
559                 boolean ok = true;
560                 Class[] types = method.getParameterTypes();
561                 for (int j = 0; j < types.length; j++) {
562                     PropertyEditor propertyEditor = PropertyEditorManager.findEditor(types[j]);
563                     ok = ok && (propertyEditor != null);
564                 }
565                 if (ok) {
566                     result.add(methodDescriptor);
567                 }
568             }
569         }
570         return result;
571     }
572 
573     /**
574      * Returns <code>true</code> if given class is not {@link String} and not a primitive type.
575      * @param cls the class
576      * @return <code>true</code> if given class is not {@link String} and not a primitive type.
577      */
578     public static boolean isFollowable(Class<?> cls) {
579         return !String.class.equals(cls) && !cls.isPrimitive()/* && !cls.getClass().isArray()*/;
580     }
581 
582     /**
583      * Returns <code>true</code> if given object is not null and its class is not {@link String} and not a primitive type.
584      * @param object the object
585      * @return <code>true</code> if given object is not null and its class is not {@link String} and not a primitive type.
586      */
587     public static boolean isFollowable(Object object) {
588         return (object != null) && isFollowable(object.getClass());
589     }
590 
591     /**
592      * Returns stack trace of given throwable object as a string.
593      * @param t the throwable object
594      * @return stack trace of given throwable object as a string
595      */
596     public static String stackTrace(Throwable t) {
597         StringWriter res = new StringWriter();
598         PrintWriter out = new PrintWriter(res);
599         t.printStackTrace(out);
600         out.flush();
601         return res.toString();
602     }
603 }