Friday, February 7, 2014

Generics with Swing: one form for displaying and editing different DB tables


In previous post I described how to use one generic DAO component for manipulating different DB tables(entities). Next step - displaying and editing data in the same way: using only one form.
In fact, form count is more the one: one form used for displaying grid with table data and another form is used for editing: for CRUD (create, update, delete) operations.
As a result, at the end,  we will have ability to create different forms with related data using only one form class:
new TableView<Region>("Region", Region.class).display();
new TableView<Country>("Country", Country.class).display();
new TableView<Location>("Location", Location.class).display();

Application diagram:

Here is some screenshots of created application:
Grid form(TableView) for displaying content of entity - database table(in this case REGION table)

Form for creating/updating/deleting(TableEdit):

When we press Select button we see grid form(TableView) with button "Select" instead of "Insert", "Update", "Delete"


Application creating. 
Application based on previous post: Hibernate with generic DAO
So, you must have previous application to build, based on it, a new one - in fact, just add to previous application UI Swing forms. Steps for modifications:  

0. DTO
To show data we need to modify our DTO files for additional information:
For displaying grid in UI form we need:
 - names of fields - to be displayed in grid column headers
 - values of fields  - to be displayed in grid
For editing elements we also need:
 - field types - to knew which control have we have to display for editing
 - ability to create entity(dto object) based on entered by user list of values.
 - if field type is another entity, we need the ability to choose it value using UI form

For all tasks, described above, I created next interface:

package com.demien.hibgeneric.domain;

import com.demien.hibgeneric.swing.TableView;

public interface IDisplayable {
    Object[] getColumnValues();
    String[] getColumnNames();
    Class[] getColumnClasses();
    void restore(Object[] values);
    TableView<?> getSelectForm();
}
 
Of course, all DTOs which have to be displayed on UI have to implement this interface. Here is example :

package com.demien.hibgeneric.domain;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

import com.demien.hibgeneric.swing.TableView;

@Entity(name = "REGIONS")
public class Region implements Serializable, IDisplayable {

 private static final long serialVersionUID = 8268800253932817168L;
 private final static String COLUMN_NAMES[]={"Region Id", "Region Name"};
 
 @Id
 @Column(name = "REGION_ID")
 private Integer regionId;
 @Column(name = "REGION_NAME")
 private String regionName;

 public Region() {
 }

 public Integer getRegionId() {
  return regionId;
 }

 public void setRegionId(Integer regionId) {
  this.regionId = regionId;
 }

 public String getRegionName() {
  return regionName;
 }

 public void setRegionName(String regionName) {
  this.regionName = regionName;
 }

 @Override
 public String toString() {
  //return "[REGION_ID=" + getRegionId().toString() + " REGION_NAME="   + getRegionName() + "]";
   return "["+getRegionId().toString()+"] "+getRegionName();
 }
    @Override
    public String[] getColumnNames() {
        return COLUMN_NAMES;
    }

    @Override
    public Object[] getColumnValues() {
        Object[] result=new Object[COLUMN_NAMES.length];
        result[0]=getRegionId();
        result[1]=getRegionName();
        return result;
    }

    @Override
    public Class<?>[] getColumnClasses() {
        Class<?>[] result=new Class[COLUMN_NAMES.length];
        result[0]=Integer.class;
        result[1]=String.class;
        return result;
    }

    @Override
    public void restore(Object[] values) {
 
        if (values!=null) {
            setRegionId((Integer)values[0]);
            setRegionName((String)values[1]);
        }
    }

 @Override
 public TableView<?> getSelectForm() {
  return new TableView<Region>("Select region, please", Region.class);
 }
 

}


1. DAO modifications.
I decided to modify DAO class a little bit: if we have UI so, best practice is to show errors on it. Thus, I created exception DAOException and made every db calls inside DAO wrapped by TRY/CATCH blocks, with throwing DAO Exception. This change entailed changes of interface IGenericDAO, IGenericService, GenericServiceImpl: adding to methods signatures "throws DAOException" declaration.

DAOException:

package com.demien.hibgeneric.dao;

public class DAOException extends Exception {
 private static final long serialVersionUID = 5144264647681662293L;

 public DAOException(String message) {
  super(message);
 }
 
 public DAOException(String message, Exception e) {
  this(message+" "+e.getMessage());
 } 
}

Example of one method in GenericDAOImpl - wrapping db calls by TRY/CATCH:

 @Override
 public T save(T object) throws DAOException {
  LOGGER.trace("STARTED - save");
  try {
   Session session = sessionFactory.getCurrentSession();
   session.beginTransaction();
   session.save(object);
   session.getTransaction().commit();
  } catch (Exception e) {
   LOGGER.error(e);
   throw new DAOException("Exception in SAVE", e);
  }
  LOGGER.trace("FINISHED - save");
  return object;
 }

IGenericService modifications - declare throwing exceptions by methods:

package com.demien.hibgeneric.service;

import java.util.List;

import com.demien.hibgeneric.dao.DAOException;
import com.demien.hibgeneric.dao.IGenericDAO;

public interface IGenericService<T> extends IGenericDAO<T> {
  List<T> getAll() throws DAOException;
  void deleteAll() throws DAOException;
  Class<T> getElementClass();
}

GenericServiceImpl - declaring throwing exceptions in methods. Example of one method:

 @Override
 public List<T> getAll() throws DAOException {
  return query("from "+cl.getName(), null);
 }

2. Main form.
Main for is a regular swing form. The only interesting part here is the way of displaying grid forms by choosing  them on menu. Different forms are displayed using one class TableView:
new TableView<Region>("Region", Region.class).display();
new TableView<Country>("Country", Country.class).display();
new TableView<Location>("Location", Location.class).display();

 Code:
package com.demien.hibgeneric.swing;

import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;

import com.demien.hibgeneric.HibernateUtil;
import com.demien.hibgeneric.domain.Country;
import com.demien.hibgeneric.domain.Location;
import com.demien.hibgeneric.domain.Region;
import com.demien.hibgeneric.service.GenericServiceImpl;
import com.demien.hibgeneric.service.IGenericService;

public class MainView extends JFrame {

 public void display() {
        //JFrame frame = new JFrame("Test frame");
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
         
              
        JMenuBar menuBar = new JMenuBar();
         
        JMenu fileMenu = new JMenu("File");                 
        JMenu newMenu = new JMenu("New");
        JMenu dictMenu=new JMenu("Dictionary");
        fileMenu.add(newMenu);
         
        JMenuItem miTxtFile = new JMenuItem("Text file");
        newMenu.add(miTxtFile);
         
        JMenuItem miImgFile = new JMenuItem("Image file");
        newMenu.add(miImgFile);
         
        fileMenu.addSeparator();
         
        JMenuItem miExit = new JMenuItem("Exit");
        fileMenu.add(miExit);
        
        JMenuItem miDictRegion=new JMenuItem("Region");
        miDictRegion.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {          
             new TableView<Region>("Region", Region.class).display();
            }
        });
        dictMenu.add(miDictRegion);
        
        JMenuItem miDictCountry=new JMenuItem("Country");
        miDictCountry.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {   
             new TableView<Country>("Country", Country.class).display();
            }
        });
        dictMenu.add(miDictCountry);
        
        JMenuItem miDictLocation=new JMenuItem("Locations");
        miDictLocation.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {   
             new TableView<Location>("Location", Location.class).display();
            }
        });
        dictMenu.add(miDictLocation);
         
        miExit.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                System.exit(0);
            }
        });
         
        menuBar.add(fileMenu);
        menuBar.add(dictMenu);
                 
        this.setJMenuBar(menuBar);
         
        //frame.setPreferredSize(new Dimension(270, 225));
        GraphicsEnvironment env =GraphicsEnvironment.getLocalGraphicsEnvironment();
        this.setMaximizedBounds(env.getMaximumWindowBounds());
        this.setExtendedState(this.getExtendedState() | this.MAXIMIZED_BOTH);
        this.pack();
        this.setLocationRelativeTo(null);
        this.setVisible(true);
  
 }
}

3. GenericTableModel.
Working with Swing tables(JTable) is based on TableModel classes which have to extends AbstractTableModel, and implements correcponding methods.  In next code stripe, we are creating  Generic class for this purpose. Little later we will use it for every entity in our application. It use generic service for getting data for overridden functions.

package com.demien.hibgeneric.swing;

import java.util.List;

import javax.swing.table.AbstractTableModel;

import com.demien.hibgeneric.dao.DAOException;
import com.demien.hibgeneric.domain.IDisplayable;
import com.demien.hibgeneric.service.IGenericService;

public class GenericTableModel<T extends IDisplayable> extends AbstractTableModel {

 private static final long serialVersionUID = 1561915663812379605L;
 private IGenericService<T> service; 
 
 public GenericTableModel(IGenericService<T> service) {
  this.service=service;
 }
 
 public List<T> getElements() {
  try {
   return service.getAll();
  } catch (DAOException e) {
   DialogUtils.showErrorDialog(e.getMessage());
  }
  return null;
 }

 @Override
 public int getRowCount() {
  List<T> items=getElements();
  return items.size();
 }

 @Override
 public int getColumnCount() {
  Class<T> cl= service.getElementClass();
  try {
   T element=cl.newInstance();
   return element.getColumnNames().length;
  } catch (InstantiationException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IllegalAccessException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  // TODO Auto-generated method stub
  return -1;
 }

 @Override
 public Object getValueAt(int rowIndex, int columnIndex) {
  // TODO Auto-generated method stub
  List<T> items=getElements();
  T element=items.get(rowIndex);
  return element.getColumnValues()[columnIndex];
 }
 
    @Override
    public String getColumnName(int column)
    {
  Class<T> cl= service.getElementClass();
  try {
   T element=cl.newInstance();
   return element.getColumnNames()[column];
  } catch (InstantiationException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IllegalAccessException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  // TODO Auto-generated method stub
  return "";
    } 

}

4. TableView
It's one of the most important form. It will will display grid with our entities(db tables).
First of all, in this form we have to create corresponding service. It will be used in child form for editing.
Using this form will be in 2 ways: displaying table data, and using for selecting data in editing. In display() method we can see the difference of displaying in that two cases.

On pressing "action" buttons (Insert/Update/Delete)  by simple command :
new TableEdit<T>(caption, TableEdit.EDIT_MODE.INSERT, element, TableView.this).display();
appears EditForm.

Methods setSelectSource and setSelectIndex used by EditForm when it use TableView for select operation. Also Edit form use refreshTable() method to refresh data in grid when INSERT/UPDATE/DELETE operation was executed.
package com.demien.hibgeneric.swing;

import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;

import com.demien.hibgeneric.HibernateUtil;
import com.demien.hibgeneric.domain.IDisplayable;
import com.demien.hibgeneric.domain.Region;
import com.demien.hibgeneric.service.GenericServiceImpl;
import com.demien.hibgeneric.service.IGenericService;

public class TableView<T extends IDisplayable> extends JFrame {

 private static final long serialVersionUID = -4645755885782493419L;
 private IGenericService<T> service;
 private JTable table;
 private GenericTableModel<T> model;
 private final String caption;
 private TableEdit<?> selectSource;
 private Integer selectIndex;
 private T element;
 
 // UI Controls
    JButton btnInsert = new JButton("Insert");
    JButton btnUpdate = new JButton("Update");
    JButton btnDelete = new JButton("Delete");
    JButton btnSelect = new JButton("Select");


 public TableView(String caption, Class cl) {
  this.caption=caption;
  this.service=new GenericServiceImpl<T>(cl, HibernateUtil.getSessionFactory());
  this.model=new GenericTableModel<T>(service); 
  try {
   element=(T)service.getElementClass().newInstance();
  } catch (InstantiationException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IllegalAccessException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  createControls();
 }
 
 public void setSelectSource(TableEdit<?> selectSource) {
  this.selectSource=selectSource;
 }
 
 public void setSelectIndex(Integer selectIndex) {
  this.selectIndex=selectIndex;
 }

    public final void createControls() {
        //Container c = getContentPane();
        //---- buttons
        JPanel pnlButtons = new JPanel();

            pnlButtons.add(btnInsert);
            pnlButtons.add(btnUpdate);
            pnlButtons.add(btnDelete);
            
            btnInsert.addActionListener(new ActionListener() {
                
                @Override
                public void actionPerformed(ActionEvent e) {                    
                    new TableEdit<T>(caption, TableEdit.EDIT_MODE.INSERT, element, TableView.this).display();
                }
            });
            
            btnUpdate.addActionListener(new ActionListener() {
                
                @Override
                public void actionPerformed(ActionEvent e) {
                    int selectedRow = table.getSelectedRow();
                    Object[] values = new Object[model.getColumnCount()];
                    for (int i = 0; i < model.getColumnCount(); i++) {
                        values[i] = model.getValueAt(selectedRow, i);
                    }                    
                    element.restore(values);
                    new TableEdit<T>(caption, TableEdit.EDIT_MODE.UPDATE, element, TableView.this).display();
                }
            });
            
            btnDelete.addActionListener(new ActionListener() {
                
                @Override
                public void actionPerformed(ActionEvent e) {
                    int selectedRow = table.getSelectedRow();
                    Object[] values = new Object[model.getColumnCount()];
                    for (int i = 0; i < model.getColumnCount(); i++) {
                        values[i] = model.getValueAt(selectedRow, i);
                    }                    
                    new TableEdit<T>(caption, TableEdit.EDIT_MODE.DELETE, element, TableView.this).display();
                }
            });
            

            
            pnlButtons.add(btnSelect);



        // ------ table
        table = new JTable(model);
        JPanel pnlTable = new JPanel();

        //add the table to the frame
        //this.add(new JScrollPane(table));
        //this.add(table);
        pnlTable.add(new JScrollPane(table));
        
        this.getContentPane().setLayout(new BorderLayout());
        this.add(pnlButtons, BorderLayout.NORTH);
        //this.add(pnlButtons);
        this.add(pnlTable, BorderLayout.CENTER);
        
        this.setTitle(caption);
        this.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);        
        this.pack();
        this.setLocationRelativeTo( null );
    }
 

 
 public void display() {
  if (selectSource!=null) {
   btnInsert.setVisible(false);
   btnUpdate.setVisible(false);
   btnDelete.setVisible(false);
   btnSelect.setVisible(true);
            btnSelect.addActionListener(new ActionListener() {
                
                @Override
                public void actionPerformed(ActionEvent e) {
                    int selectedRow = table.getSelectedRow();
                    Object[] values = new Object[model.getColumnCount()];
                    for (int i = 0; i < model.getColumnCount(); i++) {
                        values[i] = model.getValueAt(selectedRow, i);
                    }
                    element.restore(values);
                    selectSource.processSelectResult(selectIndex, element);
                    TableView.this.setVisible(false);
                }
            });  
  } else {
   btnInsert.setVisible(true);
   btnUpdate.setVisible(true);
   btnDelete.setVisible(true);
   btnSelect.setVisible(false);
   
  }
  this.setVisible(true);
 }
 
 public void refreshTable() {
  model.fireTableDataChanged();
 }
 
 public IGenericService<T> getService(){
  return service;
 }
 
}

5. EditView -another very imprtant form. 
It appears then "Action" button(Insert/Update/Delete) is pressed in a TableView form. Main part of this form is createControls method, there we have to create in dynamic way controls for user input. Created controls based on fields type of current entity which is edited.  For every control we have to create Label field and Text field, and if type of current entity field is another entity(for instance field "Region" in entity "Country") - we have to create also Select Button - by pressing it user will be able to choose entity (for instance "Region") in showed selection form(TableView).

package com.demien.hibgeneric.swing;

import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;

import com.demien.hibgeneric.domain.IDisplayable;
import com.demien.hibgeneric.service.IGenericService;

public class TableEdit<T extends IDisplayable> extends JFrame {

 public static enum EDIT_MODE {
  INSERT, UPDATE, DELETE
 }

 private final EDIT_MODE mode;
 private final T element;
 private JLabel[] labels;
 private JTextField[] fields;
 private final String caption;
 private final Object[] selectResults;
 private final TableView<T> parent;

 public TableEdit(String caption, EDIT_MODE mode, T element,
   TableView<T> parent) {
  this.mode = mode;
  this.element = element;
  this.caption = caption;
  this.selectResults = new Object[element.getColumnNames().length];
  this.parent=parent;
  createControls();

 }

 public void createControls() {
  this.setTitle(caption);
  int fieldCount = element.getColumnNames().length;
  labels = new JLabel[fieldCount];
  fields = new JTextField[fieldCount];
  this.getContentPane().setLayout(new GridLayout(fieldCount + 1, 3));
  String[] columnNames = element.getColumnNames();
  Object[] columnValues = element.getColumnValues();
  final Class<?>[] columnClasses = element.getColumnClasses();
  for (int i = 0; i < element.getColumnNames().length; i++) {
   // 1 label
   labels[i] = new JLabel(columnNames[i]);
   this.add(labels[i]);
   // 2 textFiled
   if (columnValues[i] != null) {
    fields[i] = new JTextField(columnValues[i].toString());
   } else {
    fields[i] = new JTextField();
   }
   this.add(fields[i]);
   // 3 select button
   final int ifinal = i;
   final Class<?> clfinal=columnClasses[i];
   if (element.getColumnValues()[i] != null) {
    selectResults[i] = element.getColumnValues()[i];
   }
   JButton btn = new JButton("Select");
   if (columnClasses[i].equals(Integer.class)
     || columnClasses[i].equals(String.class)) {
    btn.setVisible(false);

   } else {
    btn.setVisible(true);
    btn.addActionListener(new ActionListener() {

     @Override
     public void actionPerformed(ActionEvent e) {
      Class<?> cl=clfinal;
      Object element=null;
      try {
       element = cl.newInstance();
      } catch (InstantiationException e1) {
       // TODO Auto-generated catch block
       e1.printStackTrace();
      } catch (IllegalAccessException e1) {
       // TODO Auto-generated catch block
       e1.printStackTrace();
      }
      if (element instanceof IDisplayable) {
       TableView<?> tableView=((IDisplayable)element).getSelectForm();
       tableView.setSelectIndex(ifinal);
       tableView.setSelectSource(TableEdit.this);
       tableView.display();
      } else {
       JOptionPane.showMessageDialog(TableEdit.this, "Class "+cl.getName()+" should implement IDisplayable interface.");
       //throw new Exception("Class "+cl.getName()+" should implement IDisplayable interface.");
      }
      /*TableView selector = new TableView<T>(caption, service,
        ifinal, TableEdit.this);
      selector.display();*/
     }
    });
   }
   this.add(btn);
  }

  JButton btnDo = new JButton(mode.toString());
  JButton btnCancel = new JButton("Cancel");

  btnDo.addActionListener(new ActionListener() {

   @Override
   public void actionPerformed(ActionEvent e) {
    processAction();
   }
  });

  btnCancel.addActionListener(new ActionListener() {

   @Override
   public void actionPerformed(ActionEvent e) {
    processCancel();
   }
  });

  this.add(btnDo);
  this.add(btnCancel);

  this.setTitle(mode.toString() + " : "
    + element.getClass().getSimpleName());
  this.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
  this.pack();
  this.setLocationRelativeTo(null);

 }

 public void display() {
  this.setVisible(true);
 }

 public void processSelectResult(int elementIndex, Object result) {
  selectResults[elementIndex] = result;
  fields[elementIndex].setText(result.toString());
 }

 private void processCancel() {
  this.setVisible(false);
  // this.dispose();
 }

 private void processAction() {
  Object[] values = new Object[fields.length];
  Class<?>[] classes = element.getColumnClasses();
  for (int i = 0; i < fields.length; i++) {
   // String
   if (classes[i].equals(String.class)) {
    values[i] = fields[i].getText();
   } else
   // Integer
   if (classes[i].equals(Integer.class)) {
    int intValue = Integer.parseInt(fields[i].getText());
    values[i] = new Integer(intValue);
   } else {
    values[i] = selectResults[i];
   }
  }
  // calling presenter
  element.restore(values);
  try {
  switch (mode) {
  case INSERT:
   parent.getService().save(element);
   break;
  case UPDATE:
   parent.getService().update(element);
   break;
  case DELETE:
   parent.getService().delete(element);
  }
  } catch (Exception e) {
   DialogUtils.showErrorDialog(this, e.getMessage());
  }
  // refresh parent form
  parent.refreshTable();
  // close form :
  processCancel();

 }

}

6. DialogUtils class
It's a simple class for showing dialog.
package com.demien.hibgeneric.swing;

import java.awt.Component;

import javax.swing.JOptionPane;

public class DialogUtils {
    public static void showErrorDialog(String message) {
     showErrorDialog(null, message);
    }
    public static void showErrorDialog(Component parentComponent, String message) {
     JOptionPane.showMessageDialog(parentComponent, message, "Error", JOptionPane.ERROR_MESSAGE);
    }
}

7. Main application
Before showing MainView I made a call of HibernateUtil to load Hibernate engine. Otherwise, Hibernate engine would be loaded after first showing of TableView form I it might cause a little delay.  

package com.demien.hibgeneric;

import com.demien.hibgeneric.swing.MainView;


public class App {
 public static void main(String[] args) {
  // just to initialize hibernate 
  HibernateUtil.getSessionFactory();
  
  // show main window
  new MainView().display();

 }
}

Source codes can be downloaded from here.