001    package biz.hammurapi.util;
002    
003    import java.lang.reflect.Field;
004    import java.lang.reflect.Method;
005    import java.lang.reflect.Modifier;
006    import java.util.Arrays;
007    import java.util.Collection;
008    import java.util.HashMap;
009    import java.util.Iterator;
010    import java.util.LinkedList;
011    import java.util.Map;
012    
013    /**
014     * Wraps bean into visitable. Children are inferred from: <UL> <LI>getXXX methods which <UL><LI>Are declared in classes which belong to one of root packages or its sub-packages 
015     * </LI><LI>Have return type either collection or class (including arrays) belonging to one of root packages or its subpackages.</LI></UL>
016     * <LI>Public fields of type belonging to one of root packages or subpackages (including arrays) or of collection type.</LI></UL>
017     * @author Pavel Vlasov
018     */
019    public class BeanVisitable implements Visitable {
020            
021            private Object bean;
022            private String[] rootPackages;
023            private Map trace;
024            private Map parentMap;
025            private Integer identity;
026            
027            public BeanVisitable(Object bean) {
028                    this(bean, packageName(bean.getClass()));
029            }
030    
031            private static String packageName(Class clazz) {
032                    int idx = clazz.getName().lastIndexOf(".");
033                    return idx==-1 ? "" : clazz.getName().substring(0, idx);
034            }
035    
036            /**
037             * @param bean Bean to visit
038             * @param rootPackage Package for child classes to visit.
039             */
040            public BeanVisitable(Object bean, String rootPackage) {
041                    this(bean, new String[] {rootPackage});
042            }
043            
044            /**
045             * @param bean Bean to visit
046             * @param rootPackages Packages for child classes to visit.
047             */
048            public BeanVisitable(Object bean, String[] rootPackages) {
049                    this(bean, rootPackages, new HashMap(), new HashMap());
050            }
051            
052            /**
053             * This constructor is used by BeanVisitable itself to wrap children into visitable.
054             * @param bean Bean to visit
055             * @param rootPackages Package for child classes to visit.
056             */
057            protected BeanVisitable(Object bean, String[] rootPackages, Map trace, Map parentMap) {
058                    this.bean = bean;
059                    this.rootPackages = rootPackages;
060                    this.trace = trace;
061                    this.parentMap = parentMap;
062                    this.identity = new Integer(System.identityHashCode(bean));
063            }
064            
065            protected boolean inTheRightPackage(Class clazz) {
066                    String name = clazz.getName();
067                    for (int i=0; i<rootPackages.length; ++i) {
068                            if (clazz.isArray()) {
069                                    if (name.startsWith("[L"+rootPackages[i]+".")) {
070                                            return true;
071                                    }                               
072                            } else {
073                                    if (name.startsWith(rootPackages[i]+".")) {
074                                            return true;
075                                    }
076                            }
077                    }
078                    return false;
079            }
080            
081            public boolean accept(Visitor visitor) {
082                    if (trace.containsKey(identity)) {
083                            return false;
084                    }
085                    trace.put(identity, bean);
086                    if (visitor.visit(bean)) {
087                        Class beanClass=bean.getClass();
088                            final Object[] args = new Object[] {};
089                        Method[] methods = beanClass.getMethods();
090                        for (int i=0; i<methods.length; i++) {
091                            // getXXX() methods. Object.getClass() is not included.
092                            Method method = methods[i];
093                                    if (!(void.class.equals(method.getReturnType()))
094                                                    && (inTheRightPackage(method.getReturnType()) || Collection.class.isAssignableFrom(method.getReturnType()))
095                                                    && inTheRightPackage(method.getDeclaringClass())
096                                                    && Modifier.isPublic(method.getModifiers())
097                                            && !Modifier.isAbstract(method.getModifiers())
098                                            && !(method.getDeclaringClass().equals(Object.class)) 
099                                            && !Modifier.isStatic(method.getModifiers()) 
100                                            && method.getName().startsWith("get") 
101                                            && method.getParameterTypes().length==0) {
102                                    try {
103                                                    Object value = method.invoke(bean, args);
104                                                    if (value instanceof Collection) {
105                                                            Iterator it = ((Collection) value).iterator();
106                                                            while (it.hasNext()) {
107                                                                    wrap(it.next()).accept(visitor);
108                                                            }
109                                                    } else if (value.getClass().isArray()) {
110                                                            Iterator it = (Arrays.asList((Object[]) value)).iterator();
111                                                            while (it.hasNext()) {
112                                                                    wrap(it.next()).accept(visitor);
113                                                            }                                                       
114                                                    } else {
115                                                            wrap(value).accept(visitor);
116                                                    }                                               
117                                            } catch (Exception e) {
118                                            handleAccessError(method, e);
119                                            }
120                            }                                                                                               
121                        }
122                                        
123                        Field[] fields = beanClass.getFields();
124                        for (int i=0; i<fields.length; i++) {
125                            Field field = fields[i];
126                                    if (!Modifier.isStatic(field.getModifiers()) 
127                                                    && Modifier.isPublic(field.getModifiers())
128                                                    && inTheRightPackage(field.getDeclaringClass())
129                                                    && (inTheRightPackage(field.getType()) || Collection.class.isAssignableFrom(field.getType()))) {
130                                    try {
131                                                    Object value = field.get(bean);
132                                                    if (value instanceof Collection) {
133                                                            Iterator it = ((Collection) value).iterator();
134                                                            while (it.hasNext()) {
135                                                                    wrap(it.next()).accept(visitor);
136                                                            }
137                                                    } else if (value.getClass().isArray()) {
138                                                            Iterator it = (Arrays.asList((Object[]) value)).iterator();
139                                                            while (it.hasNext()) {
140                                                                    wrap(it.next()).accept(visitor);
141                                                            }                                                       
142                                                    } else {
143                                                            wrap(value).accept(visitor);
144                                                    }                                               
145                                            } catch (Exception e) {
146                                            handleAccessError(fields[i], e);
147                                            }
148                            }
149                        }
150                        
151                        if (visitor instanceof PoliteVisitor) {
152                            ((PoliteVisitor) visitor).leave(bean);
153                        }
154                    }
155                    return false;
156            }
157    
158            /**
159             * Prints stack trace to System.err. Override if necessary
160             * @param field
161             * @param e
162             */
163            protected void handleAccessError(Field field, Exception e) {
164                    System.err.println("Error accessing field "+field);
165                    e.printStackTrace();            
166            }
167    
168            /**
169             * Prints stack trace to System.err. Override if necessary
170             * @param method
171             * @param e
172             */
173            protected void handleAccessError(Method method, Exception e) {
174                    System.err.println("Error accessing method "+method);
175                    e.printStackTrace();            
176            }
177            
178            /**
179             * Wraps child into Visitable and updates path. 
180             * If child is already instance of Visitable it is returned as is and path is not
181             * updated.
182             * @param child
183             * @return
184             */
185            protected Visitable wrap(Object child) {
186                    if (child instanceof Visitable) {
187                            return (Visitable) child;
188                    }
189                    
190                    BeanVisitable ret = new BeanVisitable(child, rootPackages, trace, parentMap);
191                    parentMap.put(ret.identity, identity);
192                    return ret;
193            }
194            
195            /**
196             * @return Path from given object to the root of the model, the given object included.
197             */
198            public Object[] getPath(Object obj) {
199                    LinkedList path = new LinkedList();
200                    fillPath(path, obj);
201                    return path.toArray();          
202            }
203    
204            private void fillPath(LinkedList path, Object obj) {
205                    path.addFirst(obj);
206                    Object parentKey = parentMap.get(new Integer(System.identityHashCode(obj)));
207                    if (parentKey!=null) {
208                            Object parent = trace.get(parentKey);
209                            if (parent!=null) {
210                                    fillPath(path, parent);
211                            }
212                    }
213                    
214            }
215            
216            /**
217             * @return System hash code of underlying bean
218             */
219            public Integer getIdentity() {
220                    return identity;
221            }
222    
223    }
224