001 /*
002 Copyright (c) 2009 Olivier Chafik, All Rights Reserved
003
004 This file is part of JNAerator (http://jnaerator.googlecode.com/).
005
006 JNAerator is free software: you can redistribute it and/or modify
007 it under the terms of the GNU General Public License as published by
008 the Free Software Foundation, either version 3 of the License, or
009 (at your option) any later version.
010
011 JNAerator is distributed in the hope that it will be useful,
012 but WITHOUT ANY WARRANTY; without even the implied warranty of
013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 GNU General Public License for more details.
015
016 You should have received a copy of the GNU General Public License
017 along with JNAerator. If not, see <http://www.gnu.org/licenses/>.
018 */
019 package com.ochafik.beans;
020 import java.awt.Color;
021 import java.awt.Component;
022 import java.awt.Font;
023 import java.awt.event.ActionEvent;
024 import java.awt.event.ActionListener;
025 import java.awt.event.FocusAdapter;
026 import java.awt.event.FocusEvent;
027 import java.beans.PropertyChangeListener;
028 import java.beans.PropertyChangeSupport;
029 import java.lang.reflect.Method;
030 import java.util.ArrayList;
031 import java.util.HashMap;
032 import java.util.Map;
033 import java.util.Set;
034 import java.util.TreeSet;
035
036 import javax.swing.AbstractButton;
037 import javax.swing.BorderFactory;
038 import javax.swing.JCheckBox;
039 import javax.swing.JComponent;
040 import javax.swing.JScrollPane;
041 import javax.swing.JTextArea;
042 import javax.swing.JTextField;
043 import javax.swing.event.DocumentEvent;
044 import javax.swing.event.DocumentListener;
045 import javax.swing.text.JTextComponent;
046
047 import com.ochafik.swing.FormUtils;
048
049
050 @SuppressWarnings("unchecked")
051 public class BeansController<M> {
052 M model;
053 Class<M> modelClass;
054
055 public BeansController(Class<M> modelClass) {
056 this.modelClass=modelClass;
057 }
058
059 Map<String,java.util.List<JComponent>> viewsByPropertyName=new HashMap<String,java.util.List<JComponent>>();
060 Map<String,Class> propertiesTypes=new HashMap<String,Class>();
061 Map<String,Object> oldValues=new HashMap<String,Object>();
062 Map<String,Method> getterMethods=new HashMap<String,Method>();
063 Map<String,Method> setterMethods=new HashMap<String,Method>();
064 public PropertyChangeSupport getPropertyChangeSupport() {
065 return propertyChangeSupport;
066 }
067 PropertyChangeSupport propertyChangeSupport=new PropertyChangeSupport(this);
068 static final Class getterArgs[]=new Class[0];
069
070 public void addPropertyChangeListener(PropertyChangeListener listener) {
071 propertyChangeSupport.addPropertyChangeListener(listener);
072 }
073 public void addPropertyChangeListener(String propertyName,PropertyChangeListener listener) {
074 propertyChangeSupport.addPropertyChangeListener(propertyName,listener);
075 }
076 public JComponent createScrollableViewComponent(
077 final String propertyName,
078 String caption,
079 String title,
080 String tooltip,
081 boolean largeComponent
082 ) {//, IllegalAccessException {
083 JComponent c=createViewComponent(propertyName,caption,largeComponent);
084 if (c==null) return c;
085 if (title!=null) c.setBorder(BorderFactory.createTitledBorder(title));
086 if (c instanceof JTextArea) {
087 JTextArea ta=(JTextArea)c;
088 ta.setLineWrap(true);
089 ta.setWrapStyleWord(true);
090 JScrollPane jsp=new JScrollPane(ta);
091 c=jsp;
092 }
093 if (title!=null) c.setBorder(BorderFactory.createTitledBorder(title));
094 if (tooltip!=null) c.setToolTipText(tooltip);
095 return c;
096 }
097 public static final boolean booleanTrue=true;//, booleanFalse=false;
098 //private static Object booleanTrueObject,booleanFalseObject;
099 //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 }