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 */
016package org.kuali.student.datadictionary.util;
017
018import java.beans.BeanInfo;
019import java.beans.IntrospectionException;
020import java.beans.Introspector;
021import java.beans.PropertyDescriptor;
022import java.lang.reflect.Method;
023import java.util.Date;
024import java.util.List;
025import java.util.Stack;
026import org.kuali.rice.core.api.uif.DataType;
027import org.kuali.rice.krad.datadictionary.AttributeDefinition;
028import org.kuali.rice.krad.datadictionary.CollectionDefinition;
029import org.kuali.rice.krad.datadictionary.ComplexAttributeDefinition;
030import org.kuali.rice.krad.datadictionary.DataDictionaryDefinitionBase;
031import org.kuali.rice.krad.datadictionary.DataObjectEntry;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035public 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}