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 }