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