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    }