/* Copyright (c) 2009-2011 Olivier Chafik, All Rights Reserved This file is part of JNAerator (http://jnaerator.googlecode.com/). JNAerator is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. JNAerator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with JNAerator. If not, see <http://www.gnu.org/licenses/>. */ package com.ochafik.beans; import java.awt.Color; import java.awt.Component; import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.TreeSet; import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.JTextComponent; import com.ochafik.swing.FormUtils; @SuppressWarnings("unchecked") public class BeansController<M> { M model; Class<M> modelClass; public BeansController(Class<M> modelClass) { this.modelClass=modelClass; } Map<String,java.util.List<JComponent>> viewsByPropertyName=new HashMap<String,java.util.List<JComponent>>(); Map<String,Class> propertiesTypes=new HashMap<String,Class>(); Map<String,Object> oldValues=new HashMap<String,Object>(); Map<String,Method> getterMethods=new HashMap<String,Method>(); Map<String,Method> setterMethods=new HashMap<String,Method>(); public PropertyChangeSupport getPropertyChangeSupport() { return propertyChangeSupport; } PropertyChangeSupport propertyChangeSupport=new PropertyChangeSupport(this); static final Class getterArgs[]=new Class[0]; public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } public void addPropertyChangeListener(String propertyName,PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(propertyName,listener); } public JComponent createScrollableViewComponent( final String propertyName, String caption, String title, String tooltip, boolean largeComponent ) {//, IllegalAccessException { JComponent c=createViewComponent(propertyName,caption,largeComponent); if (c==null) return c; if (title!=null) c.setBorder(BorderFactory.createTitledBorder(title)); if (c instanceof JTextArea) { JTextArea ta=(JTextArea)c; ta.setLineWrap(true); ta.setWrapStyleWord(true); JScrollPane jsp=new JScrollPane(ta); c=jsp; } if (title!=null) c.setBorder(BorderFactory.createTitledBorder(title)); if (tooltip!=null) c.setToolTipText(tooltip); return c; } public static final boolean booleanTrue=true;//, booleanFalse=false; //private static Object booleanTrueObject,booleanFalseObject; //public static Class BooleanPrimitiveClass; /*static { try { Class clazz=BeansController.class; BooleanPrimitiveClass=clazz.getField("booleanTrue").getType(); } catch (Exception ex) { ex.printStackTrace(); } }*/ public JComponent createViewComponent(final String propertyName,String caption, boolean largeComponent) {//throws NoSuchMethodException {//, IllegalAccessException { try { Class propertyType=getPropertyType(propertyName); JComponent jc; if (String.class.isAssignableFrom(propertyType)) { jc=largeComponent ? new JTextArea() : new JTextField(); final JTextComponent jtc=(JTextComponent )jc; FormUtils.addUndoRedoSupport(jtc); jtc.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent arg0) { jtc.selectAll(); } @Override public void focusLost(FocusEvent arg0) { // TODO Auto-generated method stub jtc.setSelectionStart(0); jtc.setSelectionEnd(0); } }); } else if (isBoolean(propertyType)) { jc=caption==null ? new JCheckBox() : new JCheckBox(caption); } else if (isInteger(propertyType)) { jc=new JTextField(); } else { System.err.println("IMPLEMENTME! Don't know how to create a view component for model class "+propertyType.getName()); jc=null; } attachViewComponent(jc,propertyName); return jc; } catch (NoSuchMethodException ex) { throw new RuntimeException("No such field in "+modelClass.getName()+" : "+propertyName); } } public void attachViewComponent(JComponent jc, final String propertyName) throws NoSuchMethodException {//, IllegalAccessException { Class propertyType=getPropertyType(propertyName); if (String.class.isAssignableFrom(propertyType)) { final JTextComponent c=(JTextComponent)jc; c.getDocument().addDocumentListener( new DocumentListener() { public void changedUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); } public void insertUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); } public void removeUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); } }); } else if (isBoolean(propertyType)) { final AbstractButton c=(AbstractButton)jc; c.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { fireViewChange(c,propertyName,new Boolean(c.isSelected())); }}); } else if (isInteger(propertyType)){ final JTextComponent c=(JTextComponent)jc; c.getDocument().addDocumentListener(new DocumentListener() { public void changedUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); } public void insertUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); } public void removeUpdate(DocumentEvent e) { fireViewChange(c,propertyName,c.getText()); } }); } else { System.err.println("IMPLEMENTME! Don't know how to create a view component for model class "+propertyType.getName()); } if (jc!=null) { java.util.List<JComponent> views=viewsByPropertyName.get(propertyName); if (views==null) { views=new ArrayList<JComponent>(); viewsByPropertyName.put(propertyName,views); } views.add(jc); } jc.setEnabled(model!=null); } Method getGetterMethod(String propertyName) throws NoSuchMethodException {//, IllegalAccessException { Method getterMethod=getterMethods.get(propertyName); if (getterMethod==null) { try { getterMethod=modelClass.getMethod(getGetterMethodName(propertyName),getterArgs); } catch (NoSuchMethodException ex) { getterMethod=modelClass.getMethod(getIsGetterMethodName(propertyName),getterArgs); } getterMethods.put(propertyName,getterMethod); } return getterMethod; } Class getPropertyType(String propertyName) throws NoSuchMethodException { //, IllegalAccessException { Class propertyType=propertiesTypes.get(propertyName); if (propertyType==null) { propertyType=getGetterMethod(propertyName).getReturnType(); propertiesTypes.put(propertyName,propertyType); } return propertyType; } Method getSetterMethod(String propertyName) throws NoSuchMethodException { //, IllegalAccessException { Method setterMethod=setterMethods.get(propertyName); if (setterMethod==null) { Class propertyType=getPropertyType(propertyName); Class setterArgs[]=new Class[] { propertyType }; setterMethod=modelClass.getMethod(getSetterMethodName(propertyName),setterArgs); setterMethods.put(propertyName,setterMethod); } return setterMethod; } boolean updatingModel=false; public M getModel() { return model; } public void setModel(M model) { this.model=model; if (model!=null) { if (!modelClass.isAssignableFrom(model.getClass())) throw new ClassCastException(model.getClass().getName()+" not a subclass of "+modelClass.getName()); } modelUpdated(); } boolean firingPropertyChange=false; Set<String> propertiesBeingFired=new TreeSet<String>(); public void fireViewChange(Component eventSource,String propertyName, Object newValue) { //System.out.println("FireViewChange : propertyName="+propertyName+", firingPropertyChange="+firingPropertyChange+", model="+model); /// Do not fire change events if somebody is just setting the model if (updatingModel) return; if (propertiesBeingFired.contains(propertyName)) { return; } else { propertiesBeingFired.add(propertyName); } if (model!=null) { try { Class propertyType=getPropertyType(propertyName); Object oldValue=oldValues.get(propertyName); boolean validValue=true; if (String.class.isAssignableFrom(propertyType)) { getSetterMethod(propertyName).invoke(model,newValue); oldValues.put(propertyName,newValue); for (JComponent view : viewsByPropertyName.get(propertyName)) { if (view!=eventSource) { JTextComponent tc=(JTextComponent)view; //System.out.println("propertyName="+propertyName+" set at "+newValue tc.setText((String)newValue); } } } else if (isBoolean(propertyType)) { getSetterMethod(propertyName).invoke(model,newValue); oldValues.put(propertyName,newValue); for (JComponent view : viewsByPropertyName.get(propertyName)) { if (view!=eventSource) { AbstractButton cb=(AbstractButton)view; cb.setSelected(((Boolean)newValue).booleanValue()); } } } else if (isInteger(propertyType)) { try { int intValue=Integer.parseInt(((String)newValue).trim()); getSetterMethod(propertyName).invoke(model,new Integer(intValue)); oldValues.put(propertyName,newValue); for (JComponent view : viewsByPropertyName.get(propertyName)) { view.setFont(view.getFont().deriveFont(Font.PLAIN)); view.setForeground(Color.black); if (view!=eventSource) { JTextComponent tc=(JTextComponent)view; tc.setText(intValue+""); } } } catch (NumberFormatException ex) { validValue=false; if (eventSource instanceof Component) { Component eventSourceComponent=(Component)eventSource; eventSourceComponent.setFont(eventSourceComponent.getFont().deriveFont(Font.BOLD|Font.ITALIC)); eventSourceComponent.setForeground(Color.red); } } } if (propertyChangeSupport!=null&&validValue) { propertyChangeSupport.firePropertyChange( propertyName, oldValue, newValue ); } } catch (Exception ex) { ex.printStackTrace(); } } //firingPropertyChange=false; propertiesBeingFired.remove(propertyName); } private static boolean isBoolean(Class propertyType) { return Boolean.class.isAssignableFrom(propertyType) || propertyType.getName().equals("boolean"); } public void modelUpdated() { if (updatingModel) return; updatingModel=true; boolean nullModel=model==null; //System.out.println("Model updated "+nullModel); for (String propertyName : propertiesTypes.keySet()) { try { Class propertyType=getPropertyType(propertyName); Object value=model==null ? null : getGetterMethod(propertyName).invoke(model); //System.out.println("\tValue("+propertyName+") = "+value); for (JComponent view : viewsByPropertyName.get(propertyName)) { if (String.class.isAssignableFrom(propertyType)) { JTextComponent tc=(JTextComponent)view; String svalue=(String)value; tc.setText(svalue==null ? "" : svalue); tc.setEnabled(!nullModel); } else if (isBoolean(propertyType)) { //TODO AbstractButton cb=(AbstractButton)view; Boolean bvalue=(Boolean)value; cb.setEnabled(!nullModel); cb.setSelected(bvalue!=null && bvalue.booleanValue()); } else if (isInteger(propertyType)) { JTextComponent tc=(JTextComponent)view; Integer ivalue=(Integer)value; tc.setText(ivalue==null ? "" : ivalue.toString()); tc.setEnabled(!nullModel); } } } catch (Exception ex) { System.err.println("Error while updating views for property '"+propertyName+"' of model "+modelClass.getName()); ex.printStackTrace(); } } updatingModel=false; } private static final boolean isInteger(Class c) { return Integer.class.isAssignableFrom(c); } final static String getGetterMethodName(String field) { return "get"+capitalizeFirstLetter(field); } final static String getIsGetterMethodName(String field) { return "is"+capitalizeFirstLetter(field); } final static String getSetterMethodName(String field) { return "set"+capitalizeFirstLetter(field); } final static String capitalizeFirstLetter(String s) { int sLength=s.length(); if (sLength<=1) return s.toUpperCase(); else { return s.substring(0,1).toUpperCase()+s.substring(1); } } }