001    package org.kuali.student.common.assembly.transform;
002    
003    import java.beans.BeanInfo;
004    import java.beans.Introspector;
005    import java.beans.PropertyDescriptor;
006    import java.lang.reflect.Field;
007    import java.lang.reflect.ParameterizedType;
008    import java.lang.reflect.Type;
009    import java.sql.Time;
010    import java.sql.Timestamp;
011    import java.util.ArrayList;
012    import java.util.Collection;
013    import java.util.Date;
014    import java.util.HashMap;
015    import java.util.HashSet;
016    import java.util.Iterator;
017    import java.util.List;
018    import java.util.Map;
019    import java.util.Map.Entry;
020    import java.util.Set;
021    
022    import org.apache.log4j.Logger;
023    import org.kuali.student.common.assembly.data.Data;
024    import org.kuali.student.common.assembly.data.Data.DataType;
025    import org.kuali.student.common.assembly.data.Data.DataValue;
026    import org.kuali.student.common.assembly.data.Data.Key;
027    import org.kuali.student.common.assembly.data.Data.Property;
028    import org.kuali.student.common.assembly.data.Data.StringKey;
029    import org.kuali.student.common.assembly.data.Metadata;
030    
031    
032    public class DefaultDataBeanMapper implements DataBeanMapper {
033            public static DataBeanMapper INSTANCE = new DefaultDataBeanMapper();
034            final Logger LOG = Logger.getLogger(DefaultDataBeanMapper.class);       
035            
036            /* (non-Javadoc)
037             * @see org.kuali.student.core.assembly.data.DataBeanMapper#convertFromBean(java.lang.Object)
038             */
039            public Data convertFromBean(Object value, Metadata metadata) throws Exception{
040                     Data result = new Data();
041                    
042                    if (value != null) {
043                    BeanInfo info = Introspector.getBeanInfo(value.getClass());
044                    PropertyDescriptor[] properties = info.getPropertyDescriptors();
045                     
046                    for (PropertyDescriptor pd : properties) {
047                        String propKey = pd.getName();
048                        Object propValue = pd.getReadMethod().invoke(value, (Object[]) null);
049                        
050                        if ("attributes".equals(propKey)){
051                            setDataAttributes(result, propValue, metadata);
052                        } else {
053                                setDataValue(result, propKey, propValue, metadata);
054                        }
055                    }
056                    }       
057                    
058                    return result;
059            }
060            
061            /* (non-Javadoc)
062             * @see org.kuali.student.core.assembly.data.DataBeanMapper#convertFromData(org.kuali.student.core.assembly.data.Data, java.lang.Class)
063             * 
064             * FIXME: Probably a good idea to change "clazz" parameter to be "String objectType" to be consistent with convertFromBean method
065             */
066            public Object convertFromData(Data data, Class<?> clazz, Metadata metadata) throws Exception{
067                    Object result = null;
068                    
069                    result = clazz.newInstance();
070            BeanInfo info = Introspector.getBeanInfo(result.getClass());
071            PropertyDescriptor[] properties = info.getPropertyDescriptors();
072    
073            PropertyDescriptor attrProperty = null;
074            
075            Set<Key> staticProperties = new HashSet<Key>();
076                    for (PropertyDescriptor pd : properties) {
077                            if ("attributes".equals(pd.getName())){
078                                    //Dynamic attributes will be handled later
079                                    attrProperty = pd;
080                            } else {
081                        StringKey propKey = new StringKey(pd.getName());
082                        Object propValue = data.get(propKey);
083            
084                        //Process a nested object structure or list
085                        if (propValue instanceof Data){
086                            clazz.getFields();
087                            if(metadata!=null){
088                                    if(DataType.LIST.equals(metadata.getDataType())){
089                                            propValue = convertNestedData((Data)propValue, clazz.getDeclaredField(propKey.toString()),metadata.getProperties().get("*"));
090                                    }else{
091                                            propValue = convertNestedData((Data)propValue, clazz.getDeclaredField(propKey.toString()),metadata.getProperties().get(propKey.toString()));
092                                    }
093                            }
094                            else{
095                                    propValue = convertNestedData((Data)propValue, clazz.getDeclaredField(propKey.toString()),null);
096                            }
097                        }else if(metadata!=null&&propValue==null){
098                            Metadata fieldMetadata = metadata.getProperties().get(propKey.toString());
099                            if(fieldMetadata != null && fieldMetadata.getDefaultValue() != null){
100                                    propValue = fieldMetadata.getDefaultValue().get();      
101                            }
102                        }
103                        
104                            //Set the bean property
105                            if(pd.getWriteMethod() != null & propValue != null){
106                                    if(!(propValue instanceof List) && pd.getPropertyType().isAssignableFrom(List.class)){
107                                            ArrayList<Object> list = new ArrayList<Object>(1);
108                                            list.add(propValue);
109                                            pd.getWriteMethod().invoke(result, list);
110                                    }else{
111                                            pd.getWriteMethod().invoke(result, new Object[] {propValue});
112                                    }
113                        }
114                        
115                            //Hold onto the property so we know it is not dynamic
116                            staticProperties.add(propKey);
117                            }
118                    }
119                    
120                    //Any fields not processed above doesn't exist as properties for the bean and 
121                    //will be set as dynamic attributes.
122                    Set<Key> keySet = data.keySet();
123                    if (keySet != null && attrProperty != null){
124                            Map<String,String> attributes = new HashMap<String,String>();
125                for (Key k : keySet) {
126                                    String keyString = k.toString();
127                                    //Obtain the dynamic flag from the dictionary
128                                    if(metadata==null){
129                                            if (!staticProperties.contains(k) && data.get(k) != null && !keyString.startsWith("_run")){
130                                                    attributes.put((String)k.get(),data.get(k).toString());
131                                            }
132                                    }
133                                    else {
134                                        if ((! staticProperties.contains(k)) &&
135                                            (null != data.get(k)) &&
136                                            (! keyString.startsWith("_run")) &&
137                                            (null != metadata.getProperties().get(keyString)) &&
138                                            (metadata.getProperties().get(keyString).isDynamic()))
139                                        {
140                                            if (data.get(k) instanceof Data){
141                                                    attributes.put((String) k.get(), convertDataValueToStringValue((Data)data.get(k)));
142                                            } else {
143                                                    attributes.put((String) k.get(), data.get(k).toString());
144                                            }
145                                            }
146                                    }
147                            }
148                if (attrProperty.getWriteMethod() != null) {
149                    attrProperty.getWriteMethod().invoke(result, new Object[] {attributes});
150                }
151                    }
152    
153                    return result;
154            }
155            
156            
157            /**
158             * Processes a nested data map, it checks to see if the data should be converted to 
159             * a list structure or simply be processed as a nested complex object structure.
160             * 
161             * @param data
162             * @param propField
163             * @return
164             * @throws Exception
165             */
166            protected Object convertNestedData(Data data, Field propField, Metadata metadata) throws Exception{
167                    Object result = null;
168    
169                    Class<?> propClass = propField.getType();
170                    if ("java.util.List".equals(propClass.getName())){
171                            //Get the generic type for the list
172                            ParameterizedType propType = (ParameterizedType)propField.getGenericType();
173                            Type itemType = propType.getActualTypeArguments()[0];
174                            
175                            
176                            List<Object> resultList = new ArrayList<Object>();
177                            for(Iterator<Property> listIter = data.realPropertyIterator(); listIter.hasNext();){
178                                    Property listItem = listIter.next();
179                                    Object listItemValue = listItem.getValue();
180                                    if (listItemValue instanceof Data ){
181                                            Data listItemData = (Data)listItemValue;
182                                            Boolean isDeleted = listItemData.query("_runtimeData/deleted");
183                                            if (isDeleted == null || !isDeleted){
184                                                    if(metadata!=null){
185                                                            listItemValue = convertFromData((Data)listItemValue, (Class<?>)itemType, metadata.getProperties().get("*"));
186                                                    }else{
187                                                            listItemValue = convertFromData((Data)listItemValue, (Class<?>)itemType, null);
188                                                    }
189                                                    resultList.add(listItemValue);
190                                            }
191                                    } else {
192                                            resultList.add(listItemValue);
193                                    }
194                            }
195    
196                            result = resultList;
197                    } else {
198                            result = convertFromData(data, propClass,metadata);
199                    }
200                    
201                    return result;
202            }
203            
204            /**
205             * Used to set a simple property value into the data object. 
206             * @param data
207             * @param propertyKey
208             * @param value
209             * 
210             */
211            protected void setDataValue(Data data, String propertyKey, Object value, Metadata metadata) throws Exception{
212                    if (isPropertyValid(value)){
213                            if (value instanceof Boolean){
214                                    data.set(propertyKey, (Boolean)value);
215                            } else if (value instanceof Date){
216                                    data.set(propertyKey, (Date)value);
217                            } else if (value instanceof Integer){
218                                    data.set(propertyKey, (Integer)value);
219                            } else if (value instanceof Double){
220                                    data.set(propertyKey, (Double)value);
221                            } else if (value instanceof Float){
222                                    data.set(propertyKey, (Float)value);
223                            } else if (value instanceof Long) {
224                                    data.set(propertyKey, (Long)value);
225                            } else if (value instanceof Short){
226                                    data.set(propertyKey, (Short)value);                    
227                            } else if (value instanceof String){
228                                    data.set(propertyKey, (String)value);
229                            } else if (value instanceof Timestamp){
230                                    data.set(propertyKey, (Timestamp)value);
231                            } else if (value instanceof Time){
232                                    data.set(propertyKey, (Time)value);
233                            } else if (value instanceof Collection){
234                                    //TODO: Determine correct metadata to pass in getCollectionData() instead of null
235                                    data.set(propertyKey, getCollectionData(value, null));
236                            } else {
237                                    //TODO: Determine correct metadata to pass into convertFromBean() instead of null
238                                    Data dataValue = convertFromBean(value, null);
239                                    data.set(propertyKey, dataValue);
240                            }
241                    }
242                    
243            }
244            
245            /**
246             * This process the attributes map and sets the attribute key/value pairs into the Data map
247             * 
248             * @param data
249             * @param value
250             */
251            protected void setDataAttributes(Data data, Object value, Metadata metadata) {
252                    @SuppressWarnings("unchecked")
253                    Map<String, String> attributes = (Map<String, String>)value;
254                    
255                    for (Entry<String, String> entry:attributes.entrySet()){
256                    Metadata fieldMetadata = null;
257                    if (metadata != null) {
258                            fieldMetadata = metadata.getProperties().get(entry.getKey());
259                    } else {
260                            //FIXME: Fix this so this warning never happens !!
261                            LOG.warn("Metadata was null while processing property " + entry.getKey());
262                    }
263                    if (fieldMetadata != null && DataType.LIST.equals(fieldMetadata.getDataType())){
264                            data.set(entry.getKey(), convertStringToDataValue(entry.getValue()));
265                    } else if ("false".equals(entry.getValue())||"true".equals(entry.getValue())){
266                                    data.set(entry.getKey(), Boolean.valueOf(entry.getValue()));
267                            }else{
268                                    data.set(entry.getKey(), entry.getValue());
269                            }
270                    }
271            }
272    
273            /**
274             * Converts a list represented by a comma delimited string so to a DataValue
275             * 
276             * @param stringValue a comma separated list of values
277             * @return DataValue representation of stringValue
278             */
279            protected Data convertStringToDataValue(String stringValue) {
280                    Data data = null;
281                    
282                    if (stringValue != null){
283                            data = new Data();
284                            String[] stringValues = stringValue.split(",");
285                            for (String value:stringValues){
286                                    data.add(value);
287                            }
288                    }
289                                    
290                    return data;
291            }
292    
293            /**
294             * Converts a list represented by DataValue to a list of values as a comma delimited StringValue
295    
296             * @param dataValue DataValue representing a list object
297             * @return the list converted to a comma delimited StringValue
298             */
299            protected String convertDataValueToStringValue(Data data) {
300                    StringBuffer sbValue = new StringBuffer();
301                    
302                    Iterator<Property> propertyIterator = data.realPropertyIterator();
303                    while(propertyIterator.hasNext()){
304                            Property property = propertyIterator.next();
305                            String propertyValue = property.getValue();
306                            sbValue.append(",");
307                            sbValue.append(propertyValue);
308                    }
309                                    
310                    if (sbValue.toString().isEmpty()){
311                            return "";
312                    } else {
313                            return sbValue.toString().substring(1);
314                    }
315            }
316    
317            /**
318             * Used to set a list item value into the data object.
319             * 
320             * @param data
321             * @param value
322             */
323            protected void setDataListValue(Data data, Object value, Metadata metadata) throws Exception{
324                    if (isPropertyValid(value)){
325                            if (value instanceof Boolean){
326                                    data.add((Boolean)value);
327                            } else if (value instanceof Date){
328                                    data.add((Date)value);
329                            } else if (value instanceof Integer){
330                                    data.add((Integer)value);
331                            } else if (value instanceof Double){
332                                    data.add((Double)value);
333                            } else if (value instanceof Float){
334                                    data.add((Float)value);
335                            } else if (value instanceof Long) {
336                                    data.add((Long)value);
337                            } else if (value instanceof Short){
338                                    data.add((Short)value);                 
339                            } else if (value instanceof String){
340                                    data.add((String)value);
341                            } else if (value instanceof Timestamp){
342                                    data.add((Timestamp)value);
343                            } else if (value instanceof Time){
344                                    data.add((Time)value);
345                            } else if (value instanceof Collection){
346                                    //TODO: Find correct metadata to pass in 
347                                    data.add(getCollectionData(value, null));
348                            } else {
349                                    //TODO: Find correct metadata to pass in                                
350                                    Data dataValue = convertFromBean(value, null);
351                                    data.add(dataValue);                    
352                            }
353                    }
354            }
355            
356            /**
357             * Returns a data map object representing a collection
358             * 
359             * @param value
360             * @return
361             * @throws Exception
362             */
363            protected Data getCollectionData(Object value, Metadata metadata) throws Exception{
364                    Data result = new Data();
365                    
366                    if (value instanceof List){
367                            List<?> valueList = (List<?>)value;
368                            for (int i=0;i<valueList.size();i++){
369                                    Object itemValue = valueList.get(i); 
370                                    setDataListValue(result, itemValue, metadata);
371                            }
372                    } else {
373                            Collection<?> valueCollection = (Collection<?>)value;
374                            for (Object o:valueCollection){
375                                    setDataListValue(result, o, metadata);
376                            }
377                    }
378                    
379                    return result;
380            }
381            
382            protected boolean isPropertyValid(Object value){
383                    boolean isValid = false;
384                    
385                    if (value != null){
386                            Class<?> clazz = value.getClass();
387                            isValid = !(clazz.isArray() || clazz.isAnnotation() || value instanceof Class);
388                    }
389                    
390                    return isValid;
391            }
392    }