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