001    package com.sptci.echo2;
002    
003    import java.beans.PropertyChangeEvent;
004    import java.io.Serializable;
005    import java.lang.reflect.Field;
006    import java.lang.reflect.Method;
007    import java.lang.reflect.Modifier;
008    
009    import java.util.ArrayList;
010    import java.util.List;
011    import java.util.Map;
012    import java.util.HashMap;
013    import java.util.Iterator;
014    import java.util.logging.Logger;
015    
016    import nextapp.echo2.app.ListBox;
017    import nextapp.echo2.app.SelectField;
018    import nextapp.echo2.app.button.ToggleButton;
019    import nextapp.echo2.app.list.DefaultListModel;
020    
021    import com.sptci.ReflectionUtility;
022    
023    /**
024     * The <code>PropertyChangeListener</code> for change events fired
025     * by java beans to update <code>Echo2</code> UI container classes
026     * (represented by {@link #bean}).
027     *
028     * <p>Copyright 2006 Sans Pareil Technologies, Inc.</p>
029     * @author Rakesh Vidyadharan 2006-01-25
030     * @version $Id: ViewPropertyChangeListener.java,v 1.2 2006/02/15 00:48:26 rakesh Exp $
031     */
032    public class ViewPropertyChangeListener extends PropertyChangeListener 
033      implements Serializable
034    {
035      /**
036       * The logger used to log errors/warnings to.
037       */
038      private static transient final Logger logger =
039        Logger.getLogger( "com.sptci.echo2.ViewPropertyChangeListener" );
040    
041      /**
042       * A map of all the <code>Field</code>s in the {@link #bean}.
043       */
044      private transient HashMap<String, Field> fields;
045    
046      /**
047       * A map of the appropriate methods to be invoked for the elements
048       * in {@link #fields}.
049       */
050      private transient HashMap<Field, Method> methods;
051    
052      /**
053       * Create a new instance of the listener for the specified object.
054       *
055       * @see #setBean
056       * @param bean The object whose fields are to be updated by
057       *   change events.
058       */
059      public ViewPropertyChangeListener( Object bean )
060      {
061        super();
062        setBean( bean );
063      }
064    
065      /**
066       * Implementation of the event handling method.
067       *
068       * @see #parseName
069       * @see #processList
070       * @see #processMap
071       * @param event A <code>PropertyChangeEvent</code> object describing
072       * the event source and the property that has changed.
073       * @throws BindingException If errors are encountered while
074       *   attempting to modify the property in {@link #bean}.
075       */
076      public void propertyChange( PropertyChangeEvent event )
077        throws BindingException
078      {
079        try
080        {
081          String name = parseName( event.getPropertyName() );
082          Field field = fields.get( name );
083          if ( field == null ) 
084          {
085            throw new BindingException( "No field in bean " + 
086                bean.getClass() + " with name " + name );
087          }
088    
089          Object object = field.get( bean );
090          Method method = methods.get( field );
091          if ( method == null )
092          {
093            processList( object, event, field );
094          }
095          else
096          {
097            if ( object instanceof java.util.Map )
098            {
099              processMap( (Map) object, event );
100            }
101            else
102            {
103              method.invoke( object, event.getNewValue() );
104            }
105          }
106        }
107        catch ( Throwable t )
108        {
109          throw new BindingException( t.getMessage(), t );
110        }
111      }
112    
113      /**
114       * Introspect the fields of {@link #bean} and populate the
115       * {@link #fields} and {@link #methods} fields.
116       *
117       * @throws BindingException If errors are encountered while
118       *   introspecting the {@link #bean}.
119       */
120      private void introspect() throws BindingException
121      {
122        if ( bean == null ) return;
123        fields = new HashMap<String, Field>();
124        methods = new HashMap<Field, Method>();
125    
126        try
127        {
128          for ( Field field : bean.getClass().getDeclaredFields() ) 
129          {
130            fields.put( field.getName(), field );
131    
132            if ( ! Modifier.isPublic( field.getModifiers() ) )
133            {
134              field.setAccessible( true );
135            }
136    
137            Object object = field.get( bean );
138            if ( object instanceof  nextapp.echo2.app.text.TextComponent )
139            {
140              Method method = ReflectionUtility.fetchMethod( field, 
141                  "setText", new Class[] { java.lang.String.class } );
142              methods.put( field, method );
143            }
144            else if ( object instanceof  
145                nextapp.echo2.app.button.ToggleButton )
146            {
147              Method method = ReflectionUtility.fetchMethod( field, 
148                  "setSelected", new Class[] { java.lang.String.class } );
149              methods.put( field, method );
150            }
151            else if ( object instanceof java.util.Map )
152            {
153              Method method = ReflectionUtility.fetchMethod( field, 
154                  "get", new Class[] { java.lang.Object.class } );
155              methods.put( field, method );
156            }
157            else 
158            { 
159              logger.fine( "No method for object type: " + field.getType() ); 
160            }
161          }
162        }
163        catch ( Throwable t )
164        {
165          throw new BindingException( t );
166        }
167      }
168    
169      /**
170       * Update <code>AbstractList</code> UI components.
171       *
172       * @param object The UI component that is to be updated.
173       * @param event A <code>PropertyChangeEvent</code> object describing
174       *   the event source and the property that has changed.
175       * @param field The field that represents the UI component that is
176       *   to be updated.
177       * @throws BindingException If errors are encountered while
178       *   attempting to modify the property in {@link #bean}.
179       */
180      protected void processList( Object object, PropertyChangeEvent event,
181          Field field ) throws BindingException
182      {
183        if ( object instanceof nextapp.echo2.app.SelectField )
184        {
185          updateSelectField( (SelectField) object, event );
186        }
187        else if ( object instanceof nextapp.echo2.app.ListBox )
188        {
189          updateListBox( (ListBox) object, event );
190        }
191        else
192        {
193          throw new BindingException( "No method found for field " + field );
194        }
195      }
196    
197      /**
198       * Process a <code>SelectField</code> UI component.
199       *
200       * @param selectField The UI component that is to be updated.
201       * @param event A <code>PropertyChangeEvent</code> object describing
202       * the event source and the property that has changed.
203       */
204      protected void updateSelectField( SelectField selectField,
205          PropertyChangeEvent event )
206      {
207        DefaultListModel model = new DefaultListModel();
208        selectField.setSelectedIndex( 0 );
209        int count = 0;
210        for ( ListItem item : (List<ListItem>) event.getNewValue() )
211        {
212          model.add( count, item.getValue() );
213          if ( item.isSelected() )
214          {
215            selectField.setSelectedIndex( count );
216          }
217          ++count;
218        }
219    
220        selectField.setModel( model );
221      }
222    
223      /**
224       * Update a <code>ListBox</code> UI component.
225       *
226       * @param listBox The ListBox UI component that is to be updated.
227       * @param event A <code>PropertyChangeEvent</code> object describing
228       *   the event source and the property that has changed.
229       */
230      protected void updateListBox( ListBox listBox, 
231          PropertyChangeEvent event )
232      {
233        DefaultListModel model = new DefaultListModel();
234        int count = 0;
235        ArrayList<Integer> indices = new ArrayList<Integer>();
236    
237        for ( ListItem item : (List<ListItem>) event.getNewValue() )
238        {
239          model.add( count, item.getValue() );
240          if ( item.isSelected() )
241          {
242            indices.add( count );
243          }
244          ++count;
245        }
246    
247        listBox.setModel( model );
248    
249        int[] selectedIndices = new int[ indices.size() ];
250        count = 0;
251        for ( int i : indices )
252        {
253          selectedIndices[ count++ ] = i;
254        }
255    
256        listBox.setSelectedIndices( selectedIndices );
257      }
258    
259      /**
260       * Update <code>ToggleButton</code> UI components.
261       *
262       * @param map The map that contains the name and selected state
263       *   of the toggle button.
264       */
265      protected void processMap( Map map, PropertyChangeEvent event )
266      {
267        for ( Iterator iterator = map.entrySet().iterator();
268            iterator.hasNext(); )
269        {
270          Map.Entry entry = (Map.Entry) iterator.next();
271          Map newValue = (Map) event.getNewValue();
272          String key = (String) entry.getKey();
273    
274          if ( key != null && key.indexOf( ">" ) != -1 )
275          {
276            key = key.substring( key.indexOf( ">" ) + 1 );
277          }
278    
279          if ( newValue.containsKey( key ) )
280          {
281            if ( entry.getValue() instanceof 
282                nextapp.echo2.app.button.ToggleButton )
283            {
284              ToggleButton button = (ToggleButton) entry.getValue();
285              button.setSelected( 
286                  ( (Boolean) newValue.get( key ) ).booleanValue() );
287            }
288          }
289          else 
290          { 
291            logger.info( newValue.toString() ); 
292            logger.info( "No key " + key + " found in map" );
293          }
294        }
295      }
296      
297      /**
298       * Set {@link #bean}.
299       *
300       * @see #introspect
301       * @param bean The value to set.
302       */
303      public void setBean( Object bean )
304      {
305        boolean introspect = false;
306        if ( this.bean == null )
307        {
308          introspect = true;
309        }
310        else if ( ! this.bean.getClass().equals( bean.getClass() ) )
311        {
312          introspect = true;
313        }
314    
315        this.bean = bean;
316        if ( introspect ) introspect();
317      }
318    }
319