View Javadoc

1   /*
2   	Copyright (c) 2009 Olivier Chafik, All Rights Reserved
3   	
4   	This file is part of JNAerator (http://jnaerator.googlecode.com/).
5   	
6   	JNAerator is free software: you can redistribute it and/or modify
7   	it under the terms of the GNU General Public License as published by
8   	the Free Software Foundation, either version 3 of the License, or
9   	(at your option) any later version.
10  	
11  	JNAerator is distributed in the hope that it will be useful,
12  	but WITHOUT ANY WARRANTY; without even the implied warranty of
13  	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  	GNU General Public License for more details.
15  	
16  	You should have received a copy of the GNU General Public License
17  	along with JNAerator.  If not, see <http://www.gnu.org/licenses/>.
18  */
19  package com.ochafik.beans;
20  import java.awt.Color;
21  import java.awt.Component;
22  import java.awt.Font;
23  import java.awt.event.ActionEvent;
24  import java.awt.event.ActionListener;
25  import java.awt.event.FocusAdapter;
26  import java.awt.event.FocusEvent;
27  import java.beans.PropertyChangeListener;
28  import java.beans.PropertyChangeSupport;
29  import java.lang.reflect.Method;
30  import java.util.ArrayList;
31  import java.util.HashMap;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.TreeSet;
35  
36  import javax.swing.AbstractButton;
37  import javax.swing.BorderFactory;
38  import javax.swing.JCheckBox;
39  import javax.swing.JComponent;
40  import javax.swing.JScrollPane;
41  import javax.swing.JTextArea;
42  import javax.swing.JTextField;
43  import javax.swing.event.DocumentEvent;
44  import javax.swing.event.DocumentListener;
45  import javax.swing.text.JTextComponent;
46  
47  import com.ochafik.swing.FormUtils;
48  
49  
50  @SuppressWarnings("unchecked")
51  public class BeansController<M> {
52  	M model;
53  	Class<M> modelClass;
54  	
55  	public BeansController(Class<M> modelClass) {
56  		this.modelClass=modelClass;
57  	}
58  	
59  	Map<String,java.util.List<JComponent>> viewsByPropertyName=new HashMap<String,java.util.List<JComponent>>();
60  	Map<String,Class> propertiesTypes=new HashMap<String,Class>();
61  	Map<String,Object> oldValues=new HashMap<String,Object>();
62  	Map<String,Method> getterMethods=new HashMap<String,Method>();
63  	Map<String,Method> setterMethods=new HashMap<String,Method>();
64  	public PropertyChangeSupport getPropertyChangeSupport() { 
65  		return propertyChangeSupport;
66  	}
67  	PropertyChangeSupport propertyChangeSupport=new PropertyChangeSupport(this);
68  	static final Class getterArgs[]=new Class[0];
69  		
70  	public void addPropertyChangeListener(PropertyChangeListener listener) {
71          propertyChangeSupport.addPropertyChangeListener(listener);
72  	}
73  	public void addPropertyChangeListener(String propertyName,PropertyChangeListener listener) {
74          propertyChangeSupport.addPropertyChangeListener(propertyName,listener);
75  	}
76  	public JComponent createScrollableViewComponent(
77  			final String propertyName,
78  			String caption, 
79  			String title, 
80  			String tooltip, 
81  			boolean largeComponent
82  	)  {//, IllegalAccessException {
83  		JComponent c=createViewComponent(propertyName,caption,largeComponent);
84  		if (c==null) return c;
85  		if (title!=null) c.setBorder(BorderFactory.createTitledBorder(title));
86  		if (c instanceof JTextArea) {
87  			JTextArea ta=(JTextArea)c;
88              ta.setLineWrap(true);
89  			ta.setWrapStyleWord(true);
90  			JScrollPane jsp=new JScrollPane(ta);
91  			c=jsp;
92  		}
93  		if (title!=null) c.setBorder(BorderFactory.createTitledBorder(title));
94  		if (tooltip!=null) c.setToolTipText(tooltip);
95  		return c;
96  	}
97  	public static final boolean booleanTrue=true;//, booleanFalse=false;
98  	//private static Object booleanTrueObject,booleanFalseObject;
99  	//public static Class BooleanPrimitiveClass;
100 	/*static {
101 		try {
102 			Class clazz=BeansController.class;
103 			BooleanPrimitiveClass=clazz.getField("booleanTrue").getType();
104 		} catch (Exception ex) {
105 			ex.printStackTrace();
106 		}
107 	}*/	
108 	public JComponent createViewComponent(final String propertyName,String caption, boolean largeComponent) {//throws NoSuchMethodException {//, IllegalAccessException {
109         try {
110     		Class propertyType=getPropertyType(propertyName);
111     		JComponent jc;
112     		if (String.class.isAssignableFrom(propertyType)) {
113     			jc=largeComponent ? new JTextArea() : new JTextField();
114                 final JTextComponent jtc=(JTextComponent )jc;
115                 FormUtils.addUndoRedoSupport(jtc);
116                 jtc.addFocusListener(new FocusAdapter() { 
117                     @Override
118                     public void focusGained(FocusEvent arg0) {
119                         jtc.selectAll();
120                     }
121                     @Override
122                     public void focusLost(FocusEvent arg0) {
123                         // TODO Auto-generated method stub
124                         jtc.setSelectionStart(0);
125                         jtc.setSelectionEnd(0);
126                     }
127                 });
128                 
129     		} else if (isBoolean(propertyType)) {
130     			jc=caption==null ? new JCheckBox() : new JCheckBox(caption);
131     		} else if (isInteger(propertyType)) {
132                 jc=new JTextField();
133             } else {
134     			System.err.println("IMPLEMENTME! Don't know how to create a view component for model class "+propertyType.getName());
135     			jc=null;
136     		}
137     		attachViewComponent(jc,propertyName);
138     		return jc;
139         } catch (NoSuchMethodException ex) {
140             throw new RuntimeException("No such field in "+modelClass.getName()+" : "+propertyName);
141         }
142 	}
143     public void attachViewComponent(JComponent jc, final String propertyName) throws NoSuchMethodException {//, IllegalAccessException {
144 		Class propertyType=getPropertyType(propertyName);
145 		if (String.class.isAssignableFrom(propertyType)) {
146 			final JTextComponent c=(JTextComponent)jc;
147             c.getDocument().addDocumentListener( new DocumentListener() {
148                 public void changedUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); }
149 				public void insertUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); }
150 				public void removeUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); }
151 			});
152 		} else if (isBoolean(propertyType)) {
153 			final AbstractButton c=(AbstractButton)jc;
154 			c.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { 
155 				fireViewChange(c,propertyName,new Boolean(c.isSelected())); 
156 			}});
157 		} else if (isInteger(propertyType)){
158             final JTextComponent c=(JTextComponent)jc;
159             c.getDocument().addDocumentListener(new DocumentListener() {
160                 public void changedUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); }
161                 public void insertUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); }
162                 public void removeUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); }
163             });
164             
165         } else {
166 			System.err.println("IMPLEMENTME! Don't know how to create a view component for model class "+propertyType.getName());
167 		}
168 		if (jc!=null) {
169 			java.util.List<JComponent> views=viewsByPropertyName.get(propertyName);
170 			if (views==null) {
171 				 views=new ArrayList<JComponent>();
172 				 viewsByPropertyName.put(propertyName,views);
173 			}
174 			views.add(jc);
175 		}
176 		jc.setEnabled(model!=null);
177 	}
178 	Method getGetterMethod(String propertyName) throws NoSuchMethodException {//, IllegalAccessException {
179 		Method getterMethod=getterMethods.get(propertyName);
180 		if (getterMethod==null) {
181 			try {
182 				getterMethod=modelClass.getMethod(getGetterMethodName(propertyName),getterArgs);
183 			} catch (NoSuchMethodException ex) {
184 				getterMethod=modelClass.getMethod(getIsGetterMethodName(propertyName),getterArgs);
185 			}
186 			getterMethods.put(propertyName,getterMethod);
187 		}
188 		return getterMethod;
189 	}
190 	Class getPropertyType(String propertyName) throws NoSuchMethodException { //, IllegalAccessException {
191 		Class propertyType=propertiesTypes.get(propertyName);
192 		if (propertyType==null) {
193 			propertyType=getGetterMethod(propertyName).getReturnType();
194 			propertiesTypes.put(propertyName,propertyType);
195 		}
196 		return propertyType;
197 	}
198 	Method getSetterMethod(String propertyName) throws NoSuchMethodException { //, IllegalAccessException {
199 		Method setterMethod=setterMethods.get(propertyName);
200 		if (setterMethod==null) {
201 			Class propertyType=getPropertyType(propertyName);
202 			Class setterArgs[]=new Class[] { propertyType };
203 			setterMethod=modelClass.getMethod(getSetterMethodName(propertyName),setterArgs);
204 			setterMethods.put(propertyName,setterMethod);
205 		}
206 		return setterMethod;
207 	}
208 	boolean updatingModel=false;
209     public M getModel() { return model; }
210 	public void setModel(M model) {
211 		this.model=model;
212 		if (model!=null) {
213 			if (!modelClass.isAssignableFrom(model.getClass())) throw new ClassCastException(model.getClass().getName()+" not a subclass of "+modelClass.getName());
214 			
215 		}
216 		modelUpdated();
217 	}
218     boolean firingPropertyChange=false;
219     Set<String> propertiesBeingFired=new TreeSet<String>();
220 	public void fireViewChange(Component eventSource,String propertyName, Object newValue) {
221 		//System.out.println("FireViewChange : propertyName="+propertyName+", firingPropertyChange="+firingPropertyChange+", model="+model);
222         
223         /// Do not fire change events if somebody is just setting the model
224         if (updatingModel) return;
225         
226         if (propertiesBeingFired.contains(propertyName)) {
227             return;
228         } else {
229             propertiesBeingFired.add(propertyName);
230         }
231 		if (model!=null) {
232 			try {
233 				Class propertyType=getPropertyType(propertyName);
234 				Object oldValue=oldValues.get(propertyName);
235 				boolean validValue=true;
236                 if (String.class.isAssignableFrom(propertyType)) {
237                     getSetterMethod(propertyName).invoke(model,newValue);
238                     oldValues.put(propertyName,newValue);
239                     
240                     for (JComponent view : viewsByPropertyName.get(propertyName)) {
241                         if (view!=eventSource) {
242                             JTextComponent tc=(JTextComponent)view;
243                             //System.out.println("propertyName="+propertyName+" set at "+newValue
244                             tc.setText((String)newValue);
245                         }
246                     }
247 				} else if (isBoolean(propertyType)) {
248                     getSetterMethod(propertyName).invoke(model,newValue);
249                     oldValues.put(propertyName,newValue);
250                     
251                     for (JComponent view : viewsByPropertyName.get(propertyName)) {
252                         if (view!=eventSource) {
253                             AbstractButton cb=(AbstractButton)view;
254                             cb.setSelected(((Boolean)newValue).booleanValue());
255                         }
256                     }
257 				} else if (isInteger(propertyType)) {
258                     try {
259                         int intValue=Integer.parseInt(((String)newValue).trim());
260                         getSetterMethod(propertyName).invoke(model,new Integer(intValue));
261                         oldValues.put(propertyName,newValue);
262                         
263                         for (JComponent view : viewsByPropertyName.get(propertyName)) {
264                             view.setFont(view.getFont().deriveFont(Font.PLAIN));
265                             view.setForeground(Color.black);
266                             if (view!=eventSource) {
267                                 JTextComponent tc=(JTextComponent)view;
268                                 tc.setText(intValue+"");
269                             }
270     					}
271                     } catch (NumberFormatException ex) {
272                         validValue=false;
273                         if (eventSource instanceof Component) {
274                             Component eventSourceComponent=(Component)eventSource;
275                             eventSourceComponent.setFont(eventSourceComponent.getFont().deriveFont(Font.BOLD|Font.ITALIC));
276                             eventSourceComponent.setForeground(Color.red);
277                         }
278                     }
279 				}
280 				if (propertyChangeSupport!=null&&validValue) {
281                     propertyChangeSupport.firePropertyChange(
282         				propertyName,
283     					oldValue,
284     					newValue
285                     );
286                 }
287 				
288 			} catch (Exception ex) {
289 				ex.printStackTrace();
290 			}
291 		} 
292 		//firingPropertyChange=false;
293         propertiesBeingFired.remove(propertyName);
294 	}
295 	private static boolean isBoolean(Class propertyType) {
296         return Boolean.class.isAssignableFrom(propertyType) || propertyType.getName().equals("boolean");
297 	}	
298 	public void modelUpdated() {
299         if (updatingModel) return;
300         updatingModel=true;
301         boolean nullModel=model==null;
302 		//System.out.println("Model updated "+nullModel);
303 		for (String propertyName : propertiesTypes.keySet()) {
304 			try {
305 				Class propertyType=getPropertyType(propertyName);
306 				
307 				Object value=model==null ?
308 					null :
309 					getGetterMethod(propertyName).invoke(model);
310 				//System.out.println("\tValue("+propertyName+") = "+value);
311 				for (JComponent view : viewsByPropertyName.get(propertyName)) {
312 					if (String.class.isAssignableFrom(propertyType)) {
313 						JTextComponent tc=(JTextComponent)view;
314 						String svalue=(String)value;
315 						tc.setText(svalue==null ? "" : svalue);
316 						tc.setEnabled(!nullModel);
317 					} else if (isBoolean(propertyType)) {
318 						//TODO
319 						AbstractButton cb=(AbstractButton)view;
320 						Boolean bvalue=(Boolean)value;
321 						cb.setEnabled(!nullModel);
322 						cb.setSelected(bvalue!=null && bvalue.booleanValue());
323 					} else if (isInteger(propertyType)) {
324                         JTextComponent tc=(JTextComponent)view;
325                         Integer ivalue=(Integer)value;
326                         tc.setText(ivalue==null ? "" : ivalue.toString());
327                         tc.setEnabled(!nullModel);
328                     }
329 				}
330 			} catch (Exception ex) {
331                 System.err.println("Error while updating views for property '"+propertyName+"' of model "+modelClass.getName());
332 				ex.printStackTrace();
333 			}
334 		}
335         updatingModel=false;
336 	}
337     private static final boolean isInteger(Class c) {
338         return Integer.class.isAssignableFrom(c);
339     }
340 	final static String getGetterMethodName(String field) {
341 		return "get"+capitalizeFirstLetter(field);
342 	}
343 	final static String getIsGetterMethodName(String field) {
344 		return "is"+capitalizeFirstLetter(field);
345 	}
346 	final static String getSetterMethodName(String field) {
347 		return "set"+capitalizeFirstLetter(field);
348 	}
349 	final static String capitalizeFirstLetter(String s) {
350 		int sLength=s.length();
351 		if (sLength<=1) return s.toUpperCase();
352 		else {
353 			return s.substring(0,1).toUpperCase()+s.substring(1);
354 		}
355 	}	
356 }