View Javadoc

1   /**
2    * Copyright 2005-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  // begin Kuali Foundation modification
17  package org.kuali.rice.kns.web.struts.form.pojo;
18  
19  import org.apache.commons.beanutils.MappedPropertyDescriptor;
20  import org.apache.commons.beanutils.NestedNullException;
21  import org.apache.commons.beanutils.PropertyUtils;
22  import org.apache.commons.beanutils.PropertyUtilsBean;
23  import org.apache.commons.collections.FastHashMap;
24  import org.apache.log4j.Logger;
25  import org.kuali.rice.core.web.format.Formatter;
26  import org.kuali.rice.krad.util.ObjectUtils;
27  
28  import java.beans.IntrospectionException;
29  import java.beans.PropertyDescriptor;
30  import java.lang.reflect.InvocationTargetException;
31  import java.lang.reflect.Method;
32  import java.util.ArrayList;
33  import java.util.Collection;
34  import java.util.HashMap;
35  import java.util.List;
36  import java.util.Map;
37  
38  /**
39   * begin Kuali Foundation modification
40   * This class is used to access the properties of a Pojo bean.
41   * deleted author tag
42   * end Kuali Foundation modification
43   */
44  // Kuali Foundation modification: class originally SLPropertyUtilsBean
45  public class PojoPropertyUtilsBean extends PropertyUtilsBean {
46  
47      public static final Logger LOG = Logger.getLogger(PojoPropertyUtilsBean.class.getName());
48  
49  	// begin Kuali Foundation modification
50      public PojoPropertyUtilsBean() {
51          super();
52      }
53      // end Kuali Foundation modification
54  
55      public Object getProperty(Object bean, String key) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
56          // begin Kuali Foundation modification
57          if (!(bean instanceof PojoForm))
58              return super.getProperty(bean, key);
59  
60          PojoForm form = (PojoForm) bean;
61          Map unconvertedValues = form.getUnconvertedValues();
62  
63          if (unconvertedValues.containsKey(key))
64              return unconvertedValues.get(key);
65  
66          Object val = getNestedProperty(bean, key);
67          Class type = (val!=null)?val.getClass():null;
68          if ( type == null ) {
69              try {
70                  type = getPropertyType(bean, key);
71              } catch ( Exception ex ) {
72                  type = String.class;
73                  LOG.warn( "Unable to get property type for Class: " + bean.getClass().getName() + "/Property: " + key );
74              }
75          }
76          return (Formatter.isSupportedType(type) ? form.formatValue(val, key, type) : val);
77          // end Kuali Foundation modification
78      }
79  
80      	// begin Kuali Foundation modification
81      private Map<String,List<Method>> cache = new HashMap<String, List<Method>>();
82      private static Map<String,Method> readMethodCache = new HashMap<String, Method>();
83      private IntrospectionException introspectionException = new IntrospectionException( "" );
84  
85      public Object fastGetNestedProperty(Object obj, String propertyName) throws IntrospectionException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
86          //logger.debug("entering fastGetNestedProperty");
87  
88          List<Method> methods = (List<Method>) cache.get(propertyName + obj.getClass().getName());
89          if (methods == null) {
90              methods = new ArrayList<Method>();
91              Object currentObj = obj;
92              Class<?> currentObjClass = currentObj.getClass();
93  
94              for (String currentPropertyName : propertyName.split("\\.") ) {
95                  String cacheKey = currentObjClass.getName() + currentPropertyName;
96                  Method readMethod = readMethodCache.get( cacheKey );
97                  if ( readMethod == null ) {
98                  	synchronized (readMethodCache) {
99  	                    // if the read method was resolved to an error, repeat the exception
100 	                    // rather than performing the reflection calls below
101 	                    if ( readMethodCache.containsKey(cacheKey) ) {
102 	                        throw introspectionException;
103 	                    }
104 	                    try {
105 	                        try {
106 	                            readMethod = currentObjClass.getMethod("get" + currentPropertyName.substring(0, 1).toUpperCase() + currentPropertyName.substring(1), (Class[])null);
107 	                        } catch (NoSuchMethodException e) {
108 	                            readMethod = currentObjClass.getMethod("is" + currentPropertyName.substring(0, 1).toUpperCase() + currentPropertyName.substring(1), (Class[])null);
109 	                        }
110 	                    } catch ( NoSuchMethodException ex ) {
111 	                        // cache failures to prevent re-checking of the parameter
112 	                        readMethodCache.put( cacheKey, null );
113 	                        throw introspectionException;
114 	                    }
115 	                    readMethodCache.put(cacheKey, readMethod );
116 					}
117                 }
118                 methods.add(readMethod);
119                 currentObj = readMethod.invoke(currentObj, (Object[])null);
120                 currentObjClass = currentObj.getClass();
121             }
122             synchronized (cache) {
123                 cache.put(propertyName + obj.getClass().getName(), methods);
124 			}
125         }
126 
127         for ( Method method : methods ) {
128             obj = method.invoke(obj, (Object[])null);
129         }
130 
131         //logger.debug("exiting fastGetNestedProperty");
132 
133         return obj;
134     }
135 	// end Kuali Foundation modification
136 
137     /**
138      * begin Kuali Foundation modification
139      * removed comments and @<no space>since javadoc attribute
140      * end Kuali Foundation modification
141      * @see org.apache.commons.beanutils.PropertyUtilsBean#getNestedProperty(java.lang.Object, java.lang.String)
142      */
143     public Object getNestedProperty(Object arg0, String arg1) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
144 		// begin Kuali Foundation modification
145         try {
146             try {
147                 return fastGetNestedProperty(arg0, arg1);
148             }
149             catch (Exception e) {
150                 return super.getNestedProperty(arg0, arg1);
151             }
152         }
153         catch (NestedNullException e) {
154             return getUnreachableNestedProperty(arg0, arg1);
155         }
156         catch (InvocationTargetException e1) {
157             return getUnreachableNestedProperty(arg0, arg1);
158         }
159         // removed commented code
160         // end Kuali Foundation modification
161     }
162 
163     // begin Kuali Foundation modification
164     /**
165      * helper method makes sure we don't return "" for collections
166      */
167     private Object getUnreachableNestedProperty(Object arg0, String arg1) {
168         try {
169             PropertyDescriptor propertyDescriptor  = getPropertyDescriptor(arg0, arg1);
170             if (propertyDescriptor == null || Collection.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
171                 return null;
172             }
173         } catch (IllegalAccessException e) {
174             // ignore
175         } catch (InvocationTargetException e) {
176             // ignore
177         } catch (NoSuchMethodException e) {
178             // ignore
179         }
180 
181         return "";
182     }
183     // end Kuali Foundation modification
184 
185 
186     // begin Kuali Foundation modification 
187     /**
188      * begin Kuali Foundation modification
189      * Set the value of the (possibly nested) property of the specified name, for the specified bean, with no type conversions.
190      *
191      * @param bean Bean whose property is to be modified
192      * @param name Possibly nested name of the property to be modified
193      * @param value Value to which the property is to be set
194      *
195      * @exception IllegalAccessException if the caller does not have access to the property accessor method
196      * @exception IllegalArgumentException if <code>bean</code> or <code>name</code> is null
197      * @exception IllegalArgumentException if a nested reference to a property returns null
198      * @exception InvocationTargetException if the property accessor method throws an exception
199      * @exception NoSuchMethodException if an accessor method for this propety cannot be found
200      * end Kuali Foundation modification
201      */
202     public void setNestedProperty(Object bean, String name, Object value) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
203 
204         if (bean == null) {
205         	if (LOG.isDebugEnabled()) LOG.debug("No bean specified, name = " + name + ", value = " + value);
206         	return;
207         }
208         if (name == null) {
209             throw new IllegalArgumentException("No name specified");
210         }
211 
212         Object propBean = null;
213         int indexOfINDEXED_DELIM = -1;
214         int indexOfMAPPED_DELIM = -1;
215         while (true) {
216             int delim = name.indexOf(PropertyUtils.NESTED_DELIM);
217             if (delim < 0) {
218                 break;
219             }
220             String next = name.substring(0, delim);
221             indexOfINDEXED_DELIM = next.indexOf(PropertyUtils.INDEXED_DELIM);
222             indexOfMAPPED_DELIM = next.indexOf(PropertyUtils.MAPPED_DELIM);
223             if (bean instanceof Map) {
224                 propBean = ((Map) bean).get(next);
225             }
226             else if (indexOfMAPPED_DELIM >= 0) {
227                 propBean = getMappedProperty(bean, next);
228             }
229             else if (indexOfINDEXED_DELIM >= 0) {
230                 propBean = getIndexedProperty(bean, next);
231             }
232             else {
233                 propBean = getSimpleProperty(bean, next);
234             }
235             if (ObjectUtils.isNull(propBean)) {
236                 Class propertyType = getPropertyType(bean, next);
237                 if (propertyType != null) {
238                 	Object newInstance = ObjectUtils.createNewObjectFromClass(propertyType);
239                     setSimpleProperty(bean, next, newInstance);
240                     propBean = getSimpleProperty(bean, next);
241                 }
242             }
243             bean = propBean;
244             name = name.substring(delim + 1);
245         }
246 
247         indexOfINDEXED_DELIM = name.indexOf(PropertyUtils.INDEXED_DELIM);
248         indexOfMAPPED_DELIM = name.indexOf(PropertyUtils.MAPPED_DELIM);
249 
250         if (bean instanceof Map) {
251             // check to see if the class has a standard property
252             PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
253             if (descriptor == null) {
254                 // no - then put the value into the map
255                 ((Map) bean).put(name, value);
256             }
257             else {
258                 // yes - use that instead
259                 setSimpleProperty(bean, name, value);
260             }
261         }
262         else if (indexOfMAPPED_DELIM >= 0) {
263             setMappedProperty(bean, name, value);
264         }
265         else if (indexOfINDEXED_DELIM >= 0) {
266             setIndexedProperty(bean, name, value);
267         }
268         else {
269             setSimpleProperty(bean, name, value);
270         }
271     }
272     // end Kuali Foundation modification
273 
274 	// begin Kuali Foundation modification
275     /**
276      * <p>
277      * Retrieve the property descriptor for the specified property of the specified bean, or return <code>null</code> if there is
278      * no such descriptor. This method resolves indexed and nested property references in the same manner as other methods in this
279      * class, except that if the last (or only) name element is indexed, the descriptor for the last resolved property itself is
280      * returned.
281      * </p>
282      *
283      * <p>
284      * <strong>FIXME </strong>- Does not work with DynaBeans.
285      * </p>
286      *
287      * @param bean Bean for which a property descriptor is requested
288      * @param name Possibly indexed and/or nested name of the property for which a property descriptor is requested
289      *
290      * @exception IllegalAccessException if the caller does not have access to the property accessor method
291      * @exception IllegalArgumentException if <code>bean</code> or <code>name</code> is null
292      * @exception IllegalArgumentException if a nested reference to a property returns null
293      * @exception InvocationTargetException if the property accessor method throws an exception
294      * @exception NoSuchMethodException if an accessor method for this propety cannot be found
295      */
296     public PropertyDescriptor getPropertyDescriptor(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
297         if (bean == null) {
298         	if (LOG.isDebugEnabled()) LOG.debug("No bean specified, name = " + name);
299         	return null;
300         }
301         if (name == null) {
302             throw new IllegalArgumentException("No name specified");
303         }
304         try {
305             // Resolve nested references
306             Object propBean = null;
307             while (true) {
308                 int delim = findNextNestedIndex(name);
309                 //int delim = name.indexOf(PropertyUtils.NESTED_DELIM);
310                 if (delim < 0) {
311                     break;
312                 }
313                 String next = name.substring(0, delim);
314                 int indexOfINDEXED_DELIM = next.indexOf(PropertyUtils.INDEXED_DELIM);
315                 int indexOfMAPPED_DELIM = next.indexOf(PropertyUtils.MAPPED_DELIM);
316                 if (indexOfMAPPED_DELIM >= 0 && (indexOfINDEXED_DELIM < 0 || indexOfMAPPED_DELIM < indexOfINDEXED_DELIM)) {
317                     propBean = getMappedProperty(bean, next);
318                 }
319                 else {
320                     if (indexOfINDEXED_DELIM >= 0) {
321                         propBean = getIndexedProperty(bean, next);
322                     }
323                     else {
324                         propBean = getSimpleProperty(bean, next);
325                     }
326                 }
327                 if (ObjectUtils.isNull(propBean)) {
328                     Class propertyType = getPropertyType(bean, next);
329                     if (propertyType != null) {
330                     	Object newInstance = ObjectUtils.createNewObjectFromClass(propertyType);
331                         setSimpleProperty(bean, next, newInstance);
332                         propBean = getSimpleProperty(bean, next);
333                     }
334                 }
335                 bean = propBean;
336                 name = name.substring(delim + 1);
337             }
338     
339             // Remove any subscript from the final name value
340             int left = name.indexOf(PropertyUtils.INDEXED_DELIM);
341             if (left >= 0) {
342                 name = name.substring(0, left);
343             }
344             left = name.indexOf(PropertyUtils.MAPPED_DELIM);
345             if (left >= 0) {
346                 name = name.substring(0, left);
347             }
348     
349             // Look up and return this property from our cache
350             // creating and adding it to the cache if not found.
351             if ((bean == null) || (name == null)) {
352                 return (null);
353             }
354     
355             PropertyDescriptor descriptors[] = getPropertyDescriptors(bean);
356             if (descriptors != null) {
357     
358                 for (int i = 0; i < descriptors.length; i++) {
359                     if (name.equals(descriptors[i].getName()))
360                         return (descriptors[i]);
361                 }
362             }
363     
364             PropertyDescriptor result = null;
365             FastHashMap mappedDescriptors = getMappedPropertyDescriptors(bean);
366             if (mappedDescriptors == null) {
367                 mappedDescriptors = new FastHashMap();
368                 mappedDescriptors.setFast(true);
369             }
370             result = (PropertyDescriptor) mappedDescriptors.get(name);
371             if (result == null) {
372                 // not found, try to create it
373                 try {
374                     result = new MappedPropertyDescriptor(name, bean.getClass());
375                 }
376                 catch (IntrospectionException ie) {
377                 }
378                 if (result != null) {
379                     mappedDescriptors.put(name, result);
380                 }
381             }
382     
383             return result;
384         } catch ( RuntimeException ex ) {
385             LOG.error( "Unable to get property descriptor for " + bean.getClass().getName() + " . " + name
386                     + "\n" + ex.getClass().getName() + ": " + ex.getMessage() );
387             throw ex;
388         }
389     }
390     // end Kuali Foundation modification
391 
392     private int findNextNestedIndex(String expression)
393     {
394         // walk back from the end to the start
395         // and find the first index that
396         int bracketCount = 0;
397         for (int i=0, size=expression.length(); i<size ; i++) {
398             char at = expression.charAt(i);
399             switch (at) {
400                 case PropertyUtils.NESTED_DELIM:
401                     if (bracketCount < 1) {
402                         return i;
403                     }
404                     break;
405 
406                 case PropertyUtils.MAPPED_DELIM:
407                 case PropertyUtils.INDEXED_DELIM:
408                     // not bothered which
409                     ++bracketCount;
410                     break;
411 
412                 case PropertyUtils.MAPPED_DELIM2:
413                 case PropertyUtils.INDEXED_DELIM2:
414                     // not bothered which
415                     --bracketCount;
416                     break;
417             }
418         }
419         // can't find any
420         return -1;
421     }
422 
423     /**
424      * Set the value of the specified simple property of the specified bean,
425      * with no type conversions.
426      *
427      * @param bean Bean whose property is to be modified
428      * @param name Name of the property to be modified
429      * @param value Value to which the property should be set
430      *
431      * @exception IllegalAccessException if the caller does not have
432      *  access to the property accessor method
433      * @exception IllegalArgumentException if <code>bean</code> or
434      *  <code>name</code> is null
435      * @exception IllegalArgumentException if the property name is
436      *  nested or indexed
437      * @exception InvocationTargetException if the property accessor method
438      *  throws an exception
439      * @exception NoSuchMethodException if an accessor method for this
440      *  propety cannot be found
441      */
442     public void setSimpleProperty(Object bean,
443                                          String name, Object value)
444             throws IllegalAccessException, InvocationTargetException,
445             NoSuchMethodException {
446 
447         if (bean == null) {
448         	if (LOG.isDebugEnabled()) LOG.debug("No bean specified, name = " + name + ", value = " + value);
449         	return;
450         }
451         if (name == null) {
452             throw new IllegalArgumentException("No name specified");
453         }
454 
455         // Validate the syntax of the property name
456         if (name.indexOf(PropertyUtils.NESTED_DELIM) >= 0) {
457             throw new IllegalArgumentException
458                     ("Nested property names are not allowed");
459         } else if (name.indexOf(PropertyUtils.INDEXED_DELIM) >= 0) {
460             throw new IllegalArgumentException
461                     ("Indexed property names are not allowed");
462         } else if (name.indexOf(PropertyUtils.MAPPED_DELIM) >= 0) {
463             throw new IllegalArgumentException
464                     ("Mapped property names are not allowed");
465         }
466 
467         // Retrieve the property setter method for the specified property
468         PropertyDescriptor descriptor =
469                 getPropertyDescriptor(bean, name);
470         if (descriptor == null) {
471             throw new NoSuchMethodException("Unknown property '" +
472                     name + "'");
473         }
474         Method writeMethod = getWriteMethod(descriptor);
475         if (writeMethod == null) {
476             //throw new NoSuchMethodException("Property '" + name + "' has no setter method");
477         	LOG.warn("Bean: " + bean.getClass().getName() + ", Property '" + name + "' has no setter method");
478         	return;
479         }
480 
481         // Call the property setter method
482         Object values[] = new Object[1];
483         values[0] = value;
484         if (LOG.isDebugEnabled()) {
485             String valueClassName =
486                 value == null ? "<null>" : value.getClass().getName();
487             LOG.debug("setSimpleProperty: Invoking method " + writeMethod
488                       + " with value " + value + " (class " + valueClassName + ")");
489         }
490         
491         
492         invokeMethod(writeMethod, bean, values);
493 
494     }
495     
496     /** This just catches and wraps IllegalArgumentException. */
497     private Object invokeMethod(
498                         Method method, 
499                         Object bean, 
500                         Object[] values) 
501                             throws
502                                 IllegalAccessException,
503                                 InvocationTargetException {
504         try {
505             
506             return method.invoke(bean, values);
507         
508         } catch (IllegalArgumentException e) {
509             
510             LOG.error("Method invocation failed.", e);
511             throw new IllegalArgumentException(
512                 "Cannot invoke " + method.getDeclaringClass().getName() + "." 
513                 + method.getName() + " - " + e.getMessage());
514             
515         }
516     }
517 
518 }