001    /*
002     * Copyright 2011 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 1.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl1.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.student.datadictionary.util;
017    
018    import java.beans.BeanInfo;
019    import java.beans.IntrospectionException;
020    import java.beans.Introspector;
021    import java.beans.PropertyDescriptor;
022    import java.lang.reflect.Method;
023    import java.util.Date;
024    import java.util.List;
025    import java.util.Stack;
026    import org.kuali.rice.core.api.uif.DataType;
027    import org.kuali.rice.krad.datadictionary.AttributeDefinition;
028    import org.kuali.rice.krad.datadictionary.CollectionDefinition;
029    import org.kuali.rice.krad.datadictionary.ComplexAttributeDefinition;
030    import org.kuali.rice.krad.datadictionary.DataDictionaryDefinitionBase;
031    import org.kuali.rice.krad.datadictionary.DataObjectEntry;
032    
033    public class Bean2DictionaryConverter {
034    
035        private Class<?> clazz;
036        private Stack<DataDictionaryDefinitionBase> parentFields;
037        private Stack<Class<?>> parentClasses;
038    
039        public Bean2DictionaryConverter(Class<?> clazz, Stack<DataDictionaryDefinitionBase> parentFields, Stack<Class<?>> parentClasses) {
040            this.clazz = clazz;
041            this.parentFields = parentFields;
042            this.parentClasses = parentClasses;
043        }
044    
045        public DataObjectEntry convert() {
046            DataObjectEntry ode = new DataObjectEntry();
047            ode.setDataObjectClass(clazz);
048            addFields("", ode);
049            return ode;
050        }
051    
052        public void addFields(String debuggingContext, DataObjectEntry ode) {
053            BeanInfo beanInfo;
054            try {
055                beanInfo = Introspector.getBeanInfo(clazz);
056            } catch (IntrospectionException ex) {
057                throw new RuntimeException(ex);
058            }
059            for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
060                if (Class.class.equals(pd.getPropertyType())) {
061                    continue;
062                }
063                if ("_futureElements".equals(pd.getName())) {
064                    continue;
065                }
066                if ("attributes".equals(pd.getName())) {
067                    continue;
068                }
069                String name = calcName(pd.getName());
070                Class<?> actualClass = calcActualClass(clazz, pd);
071                DataType dt = calcDataType(debuggingContext + "." + clazz.getSimpleName() + "." + name, actualClass);
072                DataDictionaryDefinitionBase dddef = calcDataDictionaryDefinition(pd, dt);
073                if (dddef instanceof AttributeDefinition) {
074                    AttributeDefinition ad = (AttributeDefinition) dddef;
075                    ode.getAttributes().add(ad);
076                } else if (dddef instanceof ComplexAttributeDefinition) {
077                    ComplexAttributeDefinition cad = (ComplexAttributeDefinition) dddef;
078                    ode.getComplexAttributes().add(cad);
079                    if (!parentClasses.contains(clazz)) {
080                        parentFields.push(dddef);
081                        parentClasses.push(clazz);
082                        Bean2DictionaryConverter subConverter = new Bean2DictionaryConverter(actualClass, parentFields, parentClasses);
083                        subConverter.addFields(debuggingContext + "." + clazz.getSimpleName() + name, ode);
084                        parentFields.pop();
085                        parentClasses.pop();
086                    }
087                } else if (dddef instanceof CollectionDefinition) {
088                    CollectionDefinition cd = (CollectionDefinition) dddef;
089                    ode.getCollections().add(cd);
090                    // TODO: handle collections of primitives
091                    // DataType == null means it is a complex
092    //                TODO: add back in this logic once they fix the jira about collectoin definition not working right                
093    //                if (dt == null) {
094    //                    if (!parentClasses.contains(clazz)) {
095    //                        parentFields.push(dddef);
096    //                        parentClasses.push(clazz);
097    //                        Bean2DictionaryConverter subConverter = new Bean2DictionaryConverter(actualClass, parentFields, parentClasses);
098    //                        subConverter.addFields(debuggingContext + "." + clazz.getSimpleName() + name, ode);
099    //                        parentFields.pop();
100    //                        parentClasses.pop();
101    //                    }
102    //                }
103                }
104                if (dddef instanceof ComplexAttributeDefinition || dddef instanceof CollectionDefinition) {
105                }
106            }
107        }
108    
109        private DataDictionaryDefinitionBase calcDataDictionaryDefinition(PropertyDescriptor pd, DataType dataType) {
110            Class<?> pt = pd.getPropertyType();
111            if (List.class.equals(pt)) {
112                if (dataType != null) {
113                    System.out.println("WARNING: Can't handle lists of primitives just yet: " + calcName(pd.getName()));
114                }
115                CollectionDefinition cd = new CollectionDefinition();
116                cd.setName(calcName(pd.getName()));
117    //            cd.setDataObjectClass(pt);
118                return cd;
119            }
120            if (dataType != null) {
121                AttributeDefinition ad = new AttributeDefinition();
122                ad.setName(calcName(pd.getName()));
123                ad.setDataType(dataType);
124                return ad;
125            }
126            ComplexAttributeDefinition cad = new ComplexAttributeDefinition();
127            cad.setName(calcName(pd.getName()));
128    //        cad.setDataObjectEntry(pt);
129            return cad;
130        }
131    
132        private String calcName(String leafName) {
133            StringBuilder bldr = new StringBuilder();
134            if (!parentFields.isEmpty()) {
135                DataDictionaryDefinitionBase parent = parentFields.peek();
136                if (parent instanceof ComplexAttributeDefinition) {
137                    ComplexAttributeDefinition cad = (ComplexAttributeDefinition) parent;
138                    bldr.append(cad.getName());
139                    bldr.append(".");
140                } else if (parent instanceof CollectionDefinition) {
141                    CollectionDefinition cad = (CollectionDefinition) parent;
142                    bldr.append(cad.getName());
143                    bldr.append(".");
144                }
145            }
146            bldr.append(initLower(leafName));
147            return bldr.toString();
148        }
149    
150        private String initLower(String name) {
151            return name.substring(0, 1).toLowerCase() + name.substring(1);
152        }
153    
154        public static Class<?> calcActualClass(Class<?> clazz, PropertyDescriptor pd) {
155            Class<?> pt = null;
156            // there is a bug in the BeanInfo impl see workaround below
157            // pd.getPropertyType gets the interface not the info object
158            // for example: 
159            // info has...
160            // @Override
161            // ExpenditureInfo getExpenditure ();
162            // 
163            // and interface has
164            // Expenditure getExpenditure ();
165            // then pd.getPropertyType () returns Expenditure not ExpenditureInfo
166            // so...
167            // we use the work around if it gets an interface
168            pt = pd.getPropertyType();
169            if (pt.isInterface()) {
170                if (pd.getReadMethod() != null) {
171                    pt = workAround(clazz, pd.getReadMethod().getName());
172                }
173    //            else {
174    //                throw new NullPointerException (clazz.getName() + "." + pd.getName() + " has no corresponding read method");
175    //            }
176            }
177            
178            if (List.class.equals(pt)) {
179                pt = ComplexSubstructuresHelper.getActualClassFromList(clazz, pd.getName());
180            }
181            return pt;
182        }
183    
184        private static Class<?> workAround(Class<?> currentTargetClass, String methodName) {
185            Method method = findMethodImplFirst(currentTargetClass, methodName);
186            return method.getReturnType();
187        }
188    
189        /**
190         * Got this code from:
191         * http://raulraja.com/2009/09/12/java-beans-introspector-odd-behavio/
192         * 
193         * workaround for introspector odd behavior with javabeans that implement interfaces with comaptible return types
194         * but instrospection is unable to find the right accessors
195         *
196         * @param currentTargetClass the class being evaluated
197         * @param methodName                 the method name we are looking for
198         * @param argTypes             the arg types for the method name
199         * @return a method if found
200         */
201        private static Method findMethodImplFirst(Class<?> currentTargetClass, String methodName, Class<?>... argTypes) {
202            Method method = null;
203            if (currentTargetClass != null && methodName != null) {
204                try {
205                    method = currentTargetClass.getMethod(methodName, argTypes);
206                } catch (Throwable t) {
207                    // nothing we can do but continue
208                }
209                //Is the method in one of our parent classes
210                if (method == null) {
211                    Class<?> superclass = currentTargetClass.getSuperclass();
212                    if (!superclass.equals(Object.class)) {
213                        method = findMethodImplFirst(superclass, methodName, argTypes);
214                    }
215                }
216            }
217            return method;
218        }
219    
220        public static DataType calcDataType(String context, Class<?> pt) {
221            if (int.class.equals(pt) || Integer.class.equals(pt)) {
222                return DataType.INTEGER;
223            } else if (long.class.equals(pt) || Long.class.equals(pt)) {
224                return DataType.LONG;
225            } else if (double.class.equals(pt) || Double.class.equals(pt)) {
226                return DataType.DOUBLE;
227            } else if (float.class.equals(pt) || Float.class.equals(pt)) {
228                return DataType.FLOAT;
229            } else if (boolean.class.equals(pt) || Boolean.class.equals(pt)) {
230                return DataType.BOOLEAN;
231            } else if (Date.class.equals(pt)) {
232                return DataType.DATE;
233            } else if (String.class.equals(pt)) {
234                return DataType.STRING;
235            } else if (List.class.equals(pt)) {
236                throw new RuntimeException("Found list can't have a list of lists, List<List<?>> in " + context);
237            } else if (Enum.class.isAssignableFrom(pt)) {
238                return DataType.STRING;
239            } else if (Object.class.equals(pt)) {
240                return DataType.STRING;
241            } else if (pt.getName().startsWith("org.kuali.student.")) {
242                return null;
243            } else {
244                throw new RuntimeException("Found unknown/unhandled type of object in bean " + pt.getName() + " in " + context);
245            }
246        }
247    }