001    package com.sptci;
002    
003    import java.beans.PropertyChangeEvent;
004    import java.beans.BeanInfo;
005    import java.beans.Introspector;
006    import java.beans.IntrospectionException;
007    import java.beans.PropertyDescriptor;
008    
009    import java.lang.reflect.Field;
010    import java.lang.reflect.Method;
011    import java.lang.reflect.Modifier;
012    
013    import java.text.NumberFormat;
014    import java.text.ParseException;
015    import java.util.HashMap;
016    import java.util.logging.Logger;
017    
018    /**
019     * An implementation of <code>PropertyChangeListener</code> used
020     * to synchronise changes made to properties in UI components to the
021     * {@link #bean} java bean.
022     *
023     * <p>Copyright 2006 Sans Pareil Technologies, Inc.</p>
024     * @author Rakesh Vidyadharan 2006-01-22
025     * @version $Id: PropertyChangeListener.java,v 1.3 2006/02/08 23:31:16 rakesh Exp $
026     */
027    public class PropertyChangeListener 
028      implements java.beans.PropertyChangeListener
029    {
030      /**
031       * The logger used to log errors/warnings.
032       */
033      private static final Logger logger =
034        Logger.getLogger( "com.sptci.PropertyChangeListener" );
035    
036      /**
037       * A <code>HashMap</code> that provides the mapping for primitives
038       * to their object wrappers.
039       */
040      protected static final HashMap<String, String> typeMapping;
041    
042      /**
043       * Initialise the {@link #typeMapping} map.
044       */
045      static
046      {
047        typeMapping = new HashMap<String, String>();
048        typeMapping.put( "boolean", "java.lang.Boolean" );
049        typeMapping.put( "byte", "java.lang.Byte" );
050        typeMapping.put( "char", "java.lang.Character" );
051        typeMapping.put( "double", "java.lang.Double" );
052        typeMapping.put( "float", "java.lang.Float" );
053        typeMapping.put( "int", "java.lang.Integer" );
054        typeMapping.put( "long", "java.lang.Long" );
055        typeMapping.put( "short", "java.lang.Short" );
056      }
057    
058      /**
059       * The java bean which is to be kept synchronised with updates
060       * made to UI components.
061       */
062      protected Object bean;
063    
064      /**
065       * A <code>HashMap</code> of all the properties defined in the {@link
066       * #bean}.
067       */
068      protected HashMap<String, PropertyDescriptor> properties;
069    
070      /**
071       * Default constructor.  Initialises {@link #properties}.
072       */
073      protected PropertyChangeListener() 
074      {
075        properties = new HashMap<String, PropertyDescriptor>();
076      }
077    
078      /**
079       * Create a new instance of the class using the specified java bean.
080       *
081       * @see #setBean
082       * @param bean The data object which is to be managed.
083       * @throws IntrospectionException If errors are encountered while
084       *   introspecting the {@link #bean} class.
085       */
086      public PropertyChangeListener( Object bean )
087        throws IntrospectionException
088      {
089        this();
090        setBean( bean );
091      }
092    
093      /**
094       * Implementation of the method defined in <code>
095       * PropertyChangeListener</code>.
096       *
097       * @see #modifyPrimitive
098       * @see #modifyObject
099       * @param event A <code>PropertyChangeEvent</code> object describing 
100       *   the event source and the property that has changed.
101       */
102      public void propertyChange( PropertyChangeEvent event )
103      {
104        PropertyDescriptor descriptor = 
105          properties.get( event.getPropertyName() );
106        if ( descriptor != null )
107        {
108          if ( descriptor.getWriteMethod() == null )
109          {
110            throw new RuntimeException( "No write method for property: " +
111                event.getPropertyName() + " in bean: " +
112                bean.getClass().getName() );
113          }
114    
115          if ( descriptor.getPropertyType().isPrimitive() )
116          {
117            modifyPrimitive( event, descriptor );
118          }
119          else
120          {
121            modifyObject( event, descriptor );
122          }
123        }
124        else
125        {
126          logger.warning( "No PropertyDescriptor for " +
127              event.getPropertyName() );
128        }
129      }
130    
131      /**
132       * Populate {@link #properties} by introspecting the {@link #bean}.
133       *
134       * @throws IntrospectionException If errors are encountered while
135       *   introspecting the {@link #bean} class.
136       */
137      protected void initProperties() throws IntrospectionException
138      {
139        properties = new HashMap<String, PropertyDescriptor>();
140        BeanInfo beanInfo = Introspector.getBeanInfo( bean.getClass() );
141        for ( PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors() )
142        {
143          properties.put( descriptor.getName(), descriptor );
144        }
145      }
146    
147      /**
148       * Modify a primitive property in {@link #bean}.
149       *
150       * @param event A <code>PropertyChangeEvent</code> object that
151       *   contains the property to change and the old and new values.
152       * @param descriptor A <code>PropertyDescriptor</code> object that
153       *   describes the property
154       * @throws RuntimeException If the appropriate mutator method
155       *   for the property cannot be accessed, or if the specified value
156       *   does not directly match a primitive type or is not a string.
157       */
158      protected void modifyPrimitive( PropertyChangeEvent event,
159          PropertyDescriptor descriptor ) throws RuntimeException
160      {
161        String type = descriptor.getPropertyType().getName();
162    
163        try
164        {
165          if ( event.getNewValue() instanceof java.lang.String )
166          {
167            descriptor.getWriteMethod().invoke( 
168                bean, objectFromString( event, descriptor ) );
169          }
170          else if ( ( type.equals( "boolean" ) && 
171                event.getOldValue() instanceof java.lang.Boolean ) || 
172              ( type.equals( "byte" ) && 
173                event.getOldValue() instanceof java.lang.Byte ) || 
174              ( type.equals( "char" ) && 
175                event.getOldValue() instanceof java.lang.Character ) || 
176              ( type.equals( "double" ) && 
177                event.getOldValue() instanceof java.lang.Double ) ||
178              ( type.equals( "float" ) && 
179                event.getOldValue() instanceof java.lang.Float ) ||
180              ( type.equals( "int" ) &&
181                event.getOldValue() instanceof java.lang.Integer ) ||
182              ( type.equals( "long" ) && 
183                event.getOldValue() instanceof java.lang.Long ) ||
184              ( type.equals( "short" ) &&
185                event.getOldValue() instanceof java.lang.Short ) )
186          {
187            descriptor.getWriteMethod().invoke( bean, event.getNewValue() );
188          }
189          else
190          {
191            throw new RuntimeException( 
192                "Unable to properly convert new value type " +
193                event.getNewValue().getClass().getName() + 
194                " to primitive: " + type );
195          }
196        }
197        catch ( IllegalAccessException iaex )
198        {
199          throw new RuntimeException( iaex.getMessage(), iaex );
200        }
201        catch ( java.lang.reflect.InvocationTargetException itex )
202        {
203          throw new RuntimeException( itex.getMessage(), itex );
204        }
205      }
206    
207      /**
208       * Convert the given String value to an Object wrapper of the
209       * appropriate kind.
210       *
211       * @param event A <code>PropertyChangeEvent</code> object that
212       *   contains the property to change and the old and new values.
213       * @param descriptor A <code>PropertyDescriptor</code> object that
214       *   describes the property
215       * @throws RuntimeException If errors are encountered while trying
216       *   to initialise the wrapper object.
217       */
218      protected Object objectFromString( PropertyChangeEvent event,
219          PropertyDescriptor descriptor ) throws RuntimeException
220      {
221        String type = descriptor.getPropertyType().getName();
222        String value = (String) event.getNewValue();
223        if ( value != null && value.length() > 0 )
224        {
225          try
226          {
227            Number number = NumberFormat.getInstance().parse( value );
228            value = number.toString();
229          }
230          catch ( ParseException pex ) {}
231        }
232    
233        try
234        {
235          Class cls = Class.forName( 
236              typeMapping.get( descriptor.getPropertyType().getName() ) );
237          Class[] args = new Class[]{ java.lang.String.class };
238          return ( cls.getConstructor( args ).newInstance( value ) );
239        }
240        catch ( Throwable t )
241        {
242          throw new RuntimeException( t.getMessage(), t );
243        }
244      }
245    
246      /**
247       * Modify an object property in {@link #bean}.
248       *
249       * @param event A <code>PropertyChangeEvent</code> object that
250       *   contains the property to change and the old and new values.
251       * @param descriptor A <code>PropertyDescriptor</code> object that
252       *   describes the property
253       * @throws RuntimeException If errors are encountered while trying
254       *   to invoke the mutator method.
255       */
256      protected void modifyObject( PropertyChangeEvent event,
257          PropertyDescriptor descriptor ) throws RuntimeException
258      {
259        if ( event.getNewValue() == null ||
260            event.getNewValue().getClass() == descriptor.getPropertyType() )
261        {
262          try
263          {
264            descriptor.getWriteMethod().invoke( bean, event.getNewValue() );
265          }
266          catch ( Throwable t )
267          {
268            throw new RuntimeException( t.getMessage(), t );
269          }
270        }
271        else
272        {
273          throw new RuntimeException( "Value specified for (" +
274              event.getPropertyName() + ") has type: " +
275              event.getNewValue().getClass().getName() + 
276              " while property (" + descriptor.getName() +
277              ") has type: " + descriptor.getPropertyType().getName() );
278        }
279      }
280      
281      /**
282       * Returns {@link #bean}.
283       *
284       * @return Object The value/reference of/to bean.
285       */
286      public final Object getBean()
287      {
288        return bean;
289      }
290      
291      /**
292       * Set {@link #bean}. Re-initialises {@link #properties}.
293       *
294       * @see #initProperties
295       * @param bean The value to set.
296       * @throws IntrospectionException If errors are encountered while
297       *   introspecting the {@link #bean} class.
298       */
299      public void setBean( Object bean ) throws IntrospectionException
300      {
301        boolean init = false;
302        if ( this.bean == null )
303        {
304          init = true;
305        }
306        else if ( ! this.bean.getClass().equals( bean.getClass() ) )
307        {
308          init = true;
309        }
310    
311        this.bean = bean;
312        if ( init ) initProperties();
313      }
314      
315      /**
316       * Returns a shallow-clone of {@link #properties}.
317       *
318       * @return HashMap The value/reference of/to properties.
319       */
320      public final HashMap<String, PropertyDescriptor> getProperties()
321      {
322        return ( (HashMap<String, PropertyDescriptor>) properties.clone() );
323      }
324    }