001    /**
002     * ========================================
003     * JFreeReport : a free Java report library
004     * ========================================
005     *
006     * Project Info:  http://reporting.pentaho.org/
007     *
008     * (C) Copyright 2000-2007, by Object Refinery Limited, Pentaho Corporation and Contributors.
009     *
010     * This library is free software; you can redistribute it and/or modify it under the terms
011     * of the GNU Lesser General Public License as published by the Free Software Foundation;
012     * either version 2.1 of the License, or (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
015     * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
016     * See the GNU Lesser General Public License for more details.
017     *
018     * You should have received a copy of the GNU Lesser General Public License along with this
019     * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
020     * Boston, MA 02111-1307, USA.
021     *
022     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
023     * in the United States and other countries.]
024     *
025     * ------------
026     * $Id: BeanUtility.java 3525 2007-10-16 11:43:48Z tmorgner $
027     * ------------
028     * (C) Copyright 2000-2005, by Object Refinery Limited.
029     * (C) Copyright 2005-2007, by Pentaho Corporation.
030     */
031    package org.jfree.report.util.beans;
032    
033    import java.beans.BeanInfo;
034    import java.beans.IndexedPropertyDescriptor;
035    import java.beans.IntrospectionException;
036    import java.beans.Introspector;
037    import java.beans.PropertyDescriptor;
038    import java.lang.reflect.Array;
039    import java.lang.reflect.Method;
040    import java.util.ArrayList;
041    import java.util.HashMap;
042    
043    /**
044     * The BeanUtility class enables access to bean properties using the reflection
045     * API.
046     *
047     * @author Thomas Morgner
048     */
049    public final class BeanUtility
050    {
051      /**
052       * A property specification parses a compound property name into segments
053       * and allows access to the next property.
054       */
055      private static class PropertySpecification
056      {
057        /** The raw value of the property name. */
058        private String raw;
059        /** The next direct property that should be accessed. */
060        private String name;
061        /** The index, if the named property points to an indexed property. */
062        private String index;
063    
064        /**
065         * Creates a new PropertySpecification object for the given property string.
066         *
067         * @param raw the property string, posssibly with index specifications.
068         */
069        private PropertySpecification (final String raw)
070        {
071          this.raw = raw;
072          this.name = getNormalizedName(raw);
073          this.index = getIndex(raw);
074        }
075    
076        /**
077         * Returns the name of the property without any index information.
078         *
079         * @param property the raw name
080         * @return the normalized name.
081         */
082        private String getNormalizedName (final String property)
083        {
084          final int idx = property.indexOf('[');
085          if (idx < 0)
086          {
087            return property;
088          }
089          return property.substring(0, idx);
090        }
091    
092        /**
093         * Extracts the first index from the given raw property.
094         *
095         * @param property the raw name
096         * @return the index as String.
097         */
098        private String getIndex (final String property)
099        {
100          final int idx = property.indexOf('[');
101          if (idx < 0)
102          {
103            return null;
104          }
105          final int end = property.indexOf(']', idx + 1);
106          if (end < 0)
107          {
108            return null;
109          }
110          return property.substring(idx + 1, end);
111        }
112    
113        public String getRaw ()
114        {
115          return raw;
116        }
117    
118        public String getName ()
119        {
120          return name;
121        }
122    
123        public String getIndex ()
124        {
125          return index;
126        }
127    
128        public String toString ()
129        {
130          final StringBuffer b = new StringBuffer("PropertySpecification={");
131          b.append("raw=");
132          b.append(raw);
133          b.append("}");
134          return b.toString();
135        }
136      }
137    
138      private BeanInfo beanInfo;
139      private Object bean;
140      private HashMap properties;
141    
142      private BeanUtility()
143      {
144      }
145    
146      public BeanUtility (final Object o)
147              throws IntrospectionException
148      {
149        bean = o;
150    
151        beanInfo = Introspector.getBeanInfo(o.getClass());
152        properties = new HashMap();
153        final PropertyDescriptor[] propertyDescriptors =
154                beanInfo.getPropertyDescriptors();
155        for (int i = 0; i < propertyDescriptors.length; i++)
156        {
157          properties.put(propertyDescriptors[i].getName(), propertyDescriptors[i]);
158        }
159      }
160    
161    
162    
163      public BeanUtility derive (final Object o)
164      {
165        if (o.getClass().equals(bean.getClass()) == false)
166        {
167          throw new IllegalArgumentException();
168        }
169        final BeanUtility bu = new BeanUtility();
170        bu.bean = o;
171        return bu;
172      }
173    
174      public PropertyDescriptor[] getPropertyInfos ()
175      {
176        return beanInfo.getPropertyDescriptors();
177      }
178    
179      public Object getProperty (final String name)
180              throws BeanException
181      {
182        return getPropertyForSpecification(new PropertySpecification(name));
183      }
184    
185      private Object getPropertyForSpecification (final PropertySpecification name)
186              throws BeanException
187      {
188        final PropertyDescriptor pd = (PropertyDescriptor) properties.get(name.getName());
189        if (pd == null)
190        {
191          throw new BeanException("No such property:" + name);
192        }
193    
194        if (pd instanceof IndexedPropertyDescriptor && name.getIndex() != null)
195        {
196          final IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd;
197          final Method readMethod = ipd.getIndexedReadMethod();
198          if (readMethod == null)
199          {
200            throw new BeanException("Property is not readable: " + name);
201          }
202          try
203          {
204            return readMethod.invoke(bean, new Object[]{new Integer(name.getIndex())});
205          }
206          catch (Exception e)
207          {
208            throw new BeanException("InvokationError", e);
209          }
210        }
211        else
212        {
213          final Method readMethod = pd.getReadMethod();
214          if (readMethod == null)
215          {
216            throw new BeanException("Property is not readable: " + name);
217          }
218          if (name.getIndex() != null)
219          {
220            // handle access to array-only properties ..
221            try
222            {
223              //System.out.println(readMethod);
224              final Object value = readMethod.invoke(bean, null);
225              // we have (possibly) an array.
226              if (value == null)
227              {
228                throw new IndexOutOfBoundsException("No such index, property is null");
229              }
230              if (value.getClass().isArray() == false)
231              {
232                throw new BeanException("The property contains no array.");
233              }
234              final int index = Integer.parseInt(name.getIndex());
235              return Array.get(value, index);
236            }
237            catch(BeanException be)
238            {
239              throw be;
240            }
241            catch(IndexOutOfBoundsException iob)
242            {
243              throw iob;
244            }
245            catch(Exception e)
246            {
247              throw new BeanException("Failed to read indexed property.");
248            }
249          }
250    
251          try
252          {
253            return readMethod.invoke(bean, null);
254          }
255          catch (Exception e)
256          {
257            throw new BeanException("InvokationError", e);
258          }
259        }
260      }
261    
262      public String getPropertyAsString (final String name)
263              throws BeanException
264      {
265        final PropertySpecification ps = new PropertySpecification(name);
266        final PropertyDescriptor pd = (PropertyDescriptor) properties.get(ps.getName());
267        if (pd == null)
268        {
269          throw new BeanException("No such property:" + name);
270        }
271        final Object o = getPropertyForSpecification(ps);
272        if (o == null)
273        {
274          return null;
275        }
276    
277        final ValueConverter vc =
278                ConverterRegistry.getInstance().getValueConverter(o.getClass());
279        if (vc == null)
280        {
281          throw new BeanException("Unable to handle property of type " + o.getClass()
282                  .getName());
283        }
284        return vc.toAttributeValue(o);
285      }
286    
287      public void setProperty (final String name, final Object o)
288              throws BeanException
289      {
290        if (name == null)
291        {
292          throw new NullPointerException("Name must not be null");
293        }
294        setProperty(new PropertySpecification(name), o);
295      }
296    
297      private void setProperty (final PropertySpecification name, final Object o)
298              throws BeanException
299      {
300        final PropertyDescriptor pd = (PropertyDescriptor) properties.get(name.getName());
301        if (pd == null)
302        {
303          throw new BeanException("No such property:" + name);
304        }
305    
306        if (pd instanceof IndexedPropertyDescriptor && name.getIndex() != null)
307        {
308          final IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd;
309          final Method writeMethod = ipd.getIndexedWriteMethod();
310          if (writeMethod != null)
311          {
312            try
313            {
314              writeMethod.invoke(bean, new Object[]{new Integer(name.getIndex()), o});
315            }
316            catch (Exception e)
317            {
318              throw new BeanException("InvokationError", e);
319            }
320            // we've done the job ...
321            return;
322          }
323        }
324    
325        final Method writeMethod = pd.getWriteMethod();
326        if (writeMethod == null)
327        {
328          throw new BeanException("Property is not writeable: " + name);
329        }
330    
331        if (name.getIndex() != null)
332        {
333          // this is a indexed access, but no indexWrite method was found ...
334          updateArrayProperty(pd, name, o);
335        }
336        else
337        {
338          try
339          {
340            writeMethod.invoke(bean, new Object[]{o});
341          }
342          catch (Exception e)
343          {
344            throw new BeanException("InvokationError", e);
345          }
346        }
347      }
348    
349      private void updateArrayProperty (final PropertyDescriptor pd,
350                                        final PropertySpecification name,
351                                        final Object o)
352              throws BeanException
353      {
354        final Method readMethod = pd.getReadMethod();
355        if (readMethod == null)
356        {
357          throw new BeanException("Property is not readable, cannot perform array update: " + name);
358        }
359        try
360        {
361          //System.out.println(readMethod);
362          final Object value = readMethod.invoke(bean, null);
363          // we have (possibly) an array.
364          final int index = Integer.parseInt(name.getIndex());
365          final Object array = validateArray(getPropertyType(pd), value, index);
366          Array.set(array, index, o);
367    
368          final Method writeMethod = pd.getWriteMethod();
369          writeMethod.invoke(bean, new Object[]{array});
370        }
371        catch(BeanException e)
372        {
373          throw e;
374        }
375        catch(Exception e)
376        {
377          e.printStackTrace();
378          throw new BeanException("Failed to read property, cannot perform array update: " + name);
379        }
380      }
381    
382      private Object validateArray (final Class propertyType,
383                                    final Object o, final int size)
384              throws BeanException
385      {
386    
387        if (propertyType.isArray() == false)
388        {
389          throw new BeanException("The property's value is no array.");
390        }
391    
392        if (o == null)
393        {
394          return Array.newInstance(propertyType.getComponentType(), size + 1);
395        }
396    
397        if (o.getClass().isArray() == false)
398        {
399          throw new BeanException("The property's value is no array.");
400        }
401    
402        final int length = Array.getLength(o);
403        if (length > size)
404        {
405          return o;
406        }
407        // we have to copy the array ..
408        final Object retval = Array.newInstance(o.getClass().getComponentType(), size + 1);
409        System.arraycopy(o, 0, retval, 0, length);
410        return o;
411      }
412    
413      public void setPropertyAsString (final String name, final String txt)
414              throws BeanException
415      {
416        if (name == null)
417        {
418          throw new NullPointerException("Name must not be null");
419        }
420        if (txt == null)
421        {
422          throw new NullPointerException("Text must not be null");
423        }
424        final PropertySpecification ps = new PropertySpecification(name);
425        final PropertyDescriptor pd = (PropertyDescriptor) properties.get(ps.getName());
426        if (pd == null)
427        {
428          throw new BeanException("No such property:" + name);
429        }
430    
431        setPropertyAsString(name, getPropertyType(pd), txt);
432      }
433    
434      public Class getPropertyType (final String name) throws BeanException
435      {
436        if (name == null)
437        {
438          throw new NullPointerException("Name must not be null");
439        }
440        final PropertySpecification ps = new PropertySpecification(name);
441        final PropertyDescriptor pd = (PropertyDescriptor) properties.get(ps.getName());
442        if (pd == null)
443        {
444          throw new BeanException("No such property:" + name);
445        }
446        return getPropertyType(pd);
447      }
448    
449      public static Class getPropertyType (final PropertyDescriptor pd)
450              throws BeanException
451      {
452        final Class typeFromDescriptor = pd.getPropertyType();
453        if (typeFromDescriptor != null)
454        {
455          return typeFromDescriptor;
456        }
457        if (pd instanceof IndexedPropertyDescriptor)
458        {
459          final IndexedPropertyDescriptor idx = (IndexedPropertyDescriptor) pd;
460          return idx.getIndexedPropertyType();
461        }
462        throw new BeanException("Unable to determine the property type.");
463      }
464    
465      public void setPropertyAsString (final String name, final Class type, final String txt)
466              throws BeanException
467      {
468        if (name == null)
469        {
470          throw new NullPointerException("Name must not be null");
471        }
472        if (type == null)
473        {
474          throw new NullPointerException("Type must not be null");
475        }
476        if (txt == null)
477        {
478          throw new NullPointerException("Text must not be null");
479        }
480        final PropertySpecification ps = new PropertySpecification(name);
481        final ValueConverter vc;
482        if (ps.getIndex() != null && type.isArray())
483        {
484          vc = ConverterRegistry.getInstance().getValueConverter(type.getComponentType());
485        }
486        else
487        {
488          vc = ConverterRegistry.getInstance().getValueConverter(type);
489        }
490        if (vc == null)
491        {
492          throw new BeanException
493                  ("Unable to handle '" + type + "' for property '" + name + "'");
494        }
495        final Object o = vc.toPropertyValue(txt);
496        setProperty(ps, o);
497      }
498    
499      public String[] getProperties ()
500              throws BeanException
501      {
502        final ArrayList propertyNames = new ArrayList();
503        final PropertyDescriptor[] pd = getPropertyInfos();
504        for (int i = 0; i < pd.length; i++)
505        {
506          final PropertyDescriptor property = pd[i];
507          if (property.isHidden())
508          {
509            continue;
510          }
511          if (property.getReadMethod() == null ||
512                  property.getWriteMethod() == null)
513          {
514            // it will make no sense to write a property now, that
515            // we can't read in later...
516            continue;
517          }
518          if (getPropertyType(property).isArray())
519          {
520            final int max = findMaximumIndex(property);
521            for (int idx = 0; idx < max; idx++)
522            {
523              propertyNames.add(property.getName() + "[" + idx + "]");
524            }
525          }
526          else
527          {
528            propertyNames.add(property.getName());
529          }
530        }
531        return (String[]) propertyNames.toArray(new String[propertyNames.size()]);
532      }
533    
534      private int findMaximumIndex (final PropertyDescriptor id)
535      {
536        try
537        {
538          final Object o = getPropertyForSpecification
539                  (new PropertySpecification(id.getName()));
540          return Array.getLength(o);
541        }
542        catch (Exception e)
543        {
544          // ignore, we run 'til we encounter an index out of bounds Ex.
545        }
546        return 0;
547      }
548    }