OpenSwing Framework - advanced graphics Swing components - server side integration with Spring

Tutorial

It is available a Flash based tutorial: it shows how to create an MDI application having editable grid and master/detail functionalities.

Moreover, in this page another tutorial is proposed: it has the purpose of explain the main features of OpenSwing framework.

Suppose to realize an MDI client-server application that provides three functionalities: list of departments, list of tasks and list of employees. The list of departments and the list of tasks are based on editable grid frames; the list of employees is based on a read-only grid frame, whose row selection will open a detail window. Detail window contains an editable grid too, showing the working days of the week.

The objective of this tutorial is to explain how to create a powerful two tier client-server MDI application in a few steps and how organize classes, classes dependencies, data retrieval and storage mechanisms, ORM features, synchronization between Form data loading and dependent grid data loading, alternatives to QueryUtil class usage, how creating a three tier client-server application.

The tutorial classes are included with OpenSwing distribution, into “src/demo10” folder.

See tutorial application.

 

Creating an MDI Frame

OpenSwing provides a base MDI frame, named org.openswing.swing.mdi.client.MDIFrame. To create such a frame, an MDI controller must be defined: in fact MDIFrame class requires an MDIController interface, that must be defined. In this controller are defined all graphical aspects of the MDI frame: title, supported languages, menu type (tree menu and/or menubar), menu content, login support, colors, internationalization settings, etc.

These are the steps to follow to create an MDI frame with 3 functionalities in the menu:

  • Create a ClientApplication class that implements MDIController; this class represent the main class of the application, i.e. it must define a static main method that can be invoked from the shell/from Java Web Start.
  • In the constructor of this class the ClientSettings object must be created. It is assumed that only one language is supported, so that the EnglishOnlyResourceFactory is used to define internationalization settings, including domains required inside the application, i.e. combo-box items.
  • After ClientSettings object creation, the MDIFrame class can be invoked. This class will call several methods on MDIController implementation, including “getApplicationFunctions()” method. Inside this method the list of application functionalities must be defined; this set of functionalities are defined as a TreeModel that can contains folders or callable functionalities nested in folders.
    A callable functionality has the form: ApplicationFunction(String nodeName, String functionId, String iconName, String methodName) where the “methodName” argument represents a method defined into ClientFacade implementation.

  • An implemementation of ClientFacade OpenSwing interface must be defined, named DemoClientFacade: it contains all methods callable from the application menu (from the tree or from the menubar). In this tutorial three methods will be defined: getEmployees, getDepts, getTasks.

 

Creating department and task grid frames

To create a window that contains a grid, two classes are required:

  • The frame class that extends org.openswing.swing.mdi.client.InternalFrame; this class will contain a GridControl.
  • The grid controller class that extends org.openswing.swing.table.client.GridController; all events fired by the grid control are listened by the grid controller, such as save for insert, save for update, delete rows, cells validation.

A grid control requires also an implementation of org.openswing.swing.table.java.GridDataLocator interface, used by the grid control to retrieve data to show into the grid. To simply the example, the grid controller will implements GridDataLocator too, i.e. it define a “loadData” method that gives back a list of value objects, one for each row in the grid.

All column dropped in to the grid control are mapped with a specific attribute of value object supported by the grid. If you are using NetBeans or JBuilder or JDeveloper, then you can visually create the grid frame by means of the UI designer of the IDE: you can drag 'n drop the GridControl object and the XXXColumn objects into the grid control and set their properties by the property editor of the UI designer, including the attribute name binded for each column, by selecting it from the combo-box of v.o. attribute names, automatically fetched from the value object previously setted as property in the GridControl. You have not to worry about table cell renderers or editors: these are automatically created by OpenSwing at run-time.

In "loadData" method of GridDataLocator implemetation class you have to define the business logic related to grid data retrieval; you can delegate data retrieval to another class or simply include that logic in this method:


  public Response loadData(
      int action,
      int startIndex,
      Map filteredColumns,
      ArrayList currentSortedColumns,
      ArrayList currentSortedVersusColumns,
      Class valueObjectType,
      Map otherGridParams) {

    try {
      String sql = "select TASKS.TASK_CODE,TASKS.DESCRIPTION,TASKS.STATUS from TASKS where TASKS.STATUS='E'";

      // mapping between attributes and database fields...
      Map attribute2dbField = new HashMap();
      attribute2dbField.put("taskCode","TASKS.TASK_CODE");
      attribute2dbField.put("description","TASKS.DESCRIPTION");
      attribute2dbField.put("status","TASKS.STATUS");

      return QueryUtil.getQuery(
        conn,
        sql,
        new ArrayList(), // list of values linked to "?" parameters in sql
        attribute2dbField,
        TaskVO.class, // v.o. to dinamically create for each row...
        "Y",
        "N",
        new GridParams(
          action,
          startIndex,
          filteredColumns,
          currentSortedColumns,
          currentSortedVersusColumns,
          new HashMap() // other params...
        ),
        50, // pagination size...
        true // log query...
      );
    }
    catch (Exception ex) {
      ex.printStackTrace();
      return new ErrorResponse(ex.getMessage());
    }

/*
    // an alternative way: you can define your own business logic to retrieve data and adding filtering/sorting conditions at hand...
    PreparedStatement stmt = null;
    try {
      String sql = "select TASKS.TASK_CODE,TASKS.DESCRIPTION from TASKS where TASKS.STATUS='E'";
      Vector vals = new Vector();
      if (filteredColumns.size()>0) {
        FilterWhereClause[] filter = (FilterWhereClause[])filteredColumns.get("taskCode");
        sql += " and TASKS.TASK_CODE "+ filter[0].getOperator()+"?";
        vals.add(filter[0].getValue());
        if (filter[1]!=null) {
          sql += " and TASKS.TASK_CODE "+ filter[1].getOperator()+"?";
          vals.add(filter[1].getValue());
        }
      }
      if (currentSortedColumns.size()>0) {
        sql += " ORDER BY TASKS.TASK_CODE "+currentSortedVersusColumns.get(0);
      }

      stmt = conn.prepareStatement(sql);
      for(int i=0;i<vals.size();i++)
        stmt.setObject(i+1,vals.get(i));

      ResultSet rset = stmt.executeQuery();


      ArrayList list = new ArrayList();
      TaskVO vo = null;
      while (rset.next()) {
        vo = new TaskVO();
        vo.setTaskCode(rset.getString(1));
        vo.setDescription(rset.getString(2));
        list.add(vo);
      }
      return new VOListResponse(list,false,list.size());
    }
    catch (SQLException ex) {
      ex.printStackTrace();
      return new ErrorResponse(ex.getMessage());
    }
    finally {
      try {
        stmt.close();
      }
      catch (SQLException ex1) {
      }
    }
*/
  }
  

As you can see from the example reported above, you could retrieve data for the grid by using QueryUtil class provided by OpenSwing: this class is able

  • to load a block of data from the result set, according to grid settings provided as loadData method arguments

    (pagination, filtering and sorting conditions)
  • to load all records from the result set, according to grid settings provided as loadData method arguments

    (filtering and sorting conditions)

A programmer has to define the SQL base query only and an HashMap of attributes to database field mapping: QueryUtil does the rest. The database fields mapping is related to all the fields reported in select clause of SQL base query. QueryUtil.getQuery method uses this mapping

  • to automatically translate a filtering/sorting condition expressed as an attribute to the related database field name
  • to automatically create a value object for each record read from the result set and to map each result set value to the corresponding value object attribute.

You are free to do not use QueryUtil utility class: you can use any other ORM mechanism or simply define a business logic directly based on ResultSet scrolling.

 

To insert in the database one or more rows created in the grid, you have to define the insertRecords method in the GridController implementation class, as described below:

  public Response insertRecords(int[] rowNumbers, ArrayList newValueObjects) throws Exception {
    // mapping between attributes and database fields...
    Map attribute2dbField = new HashMap();
    attribute2dbField.put("taskCode","TASK_CODE");
    attribute2dbField.put("description","DESCRIPTION");
    attribute2dbField.put("status","STATUS");

    Response res = QueryUtil.insertTable(conn,newValueObjects,"TASKS",attribute2dbField,"Y","N",true);
    if (res.isError())
      conn.rollback();
    else
      conn.commit();
    return res;

/*
    // an alternative way: you can define your own business logic to store data at hand...
    PreparedStatement stmt = null;
    try {
      stmt = conn.prepareStatement("insert into TASKS(TASK_CODE,DESCRIPTION,STATUS) values(?,?,?)");
      TaskVO vo = (TaskVO)newValueObjects.get(0);
      stmt.setString(1,vo.getTaskCode());
      stmt.setString(2,vo.getDescription());
      stmt.setString(3,"E");
      stmt.execute();
      return new VOListResponse(newValueObjects,false,newValueObjects.size());
    }
    catch (SQLException ex) {
      ex.printStackTrace();
      return new ErrorResponse(ex.getMessage());
    }
    finally {
      try {
        stmt.close();
        conn.commit();
      }
      catch (SQLException ex1) {
      }
    }
*/
  }

QueryUtil class is very useful to insert a set of records: you could invoke "insertRecords" method (or "insertRecord" method to insert one record). This method requires a list of value objects to insert and a mapping between v.o. attribute names and related database fields and finally, the table name: QueryUtil.insertRecords uses this info to dinamically compose a SQL instruction to insert a record for each value object of the list.

As alternative, you are free to use directly JDBC API to insert record through Statement/PrepareStatement objects.

 

To update one or more records in the database starting from a list of changed value objects, OpenSwing provides the QueryUtil.updateRecord utility method:

  public Response updateRecords(int[] rowNumbers,ArrayList oldPersistentObjects,ArrayList persistentObjects) throws Exception {
    // mapping between attributes and database fields...
    Map attribute2dbField = new HashMap();
    attribute2dbField.put("taskCode","TASK_CODE");
    attribute2dbField.put("description","DESCRIPTION");
    attribute2dbField.put("status","STATUS");

    HashSet pk = new HashSet();
    pk.add("taskCode");

    Response res = null;
    TaskVO oldVO = null;
    TaskVO newVO = null;
    for(int i=0;i<persistentObjects.size();i++) {
      oldVO = (TaskVO)oldPersistentObjects.get(i);
      newVO = (TaskVO)persistentObjects.get(i);
      res = QueryUtil.updateTable(conn,pk,oldVO,newVO,"TASKS",attribute2dbField,"Y","N",true);
      if (res.isError()) {
        conn.rollback();
        return res;
      }
    }
    conn.commit();
    return new VOListResponse(persistentObjects,false,persistentObjects.size());

/*
    // an alternative way: you can define your own business logic to store data at hand...
    PreparedStatement stmt = null;
    try {
      stmt = conn.prepareStatement("update TASKS set TASK_CODE=?,DESCRIPTION=? where TASK_CODE=?");
      TaskVO vo = null;
      for(int i=0;i<persistentObjects.size();i++) {
        vo = (TaskVO)persistentObjects.get(i);
        stmt.setString(1,vo.getTaskCode());
        stmt.setString(2,vo.getDescription());
        stmt.setString(3,vo.getTaskCode());
        stmt.execute();
      }
      return new VOListResponse(persistentObjects,false,persistentObjects.size());
    }
    catch (SQLException ex) {
      ex.printStackTrace();
      return new ErrorResponse(ex.getMessage());
    }
    finally {
      try {
        stmt.close();
        conn.commit();
      }
      catch (SQLException ex1) {
      }
    }
*/
  }

This powerful method is able to compose a SQL update instruction having in the SET clause the list of all changed fields (attributes) and in the WHERE clause the old values of the same fields (attributes): this is possible thanks to GridControl component that stores inside it the original copy of changed value objects, and returns them together with changed value objects to updateRecords of GridController class. In this way it is possible to check for concurrent access to the same record and avoid to update it from two clients in the same time and without requiring any "timestamp" info in the database.

As for data loading and data insertion, you are free to realize update operation in any other way, e.g. using directly JDBC API.

 

Creating employee grid + detail frames

The employees grid can be created in the same way described for depts and tasks.

The detail frame can be linked to the grid by means of doubleClick method in the grid controller: this method is automatically invoked by the grid control when the user double clicks the selected row in the grid.

The doubleClick method is used to invoke the detail frame controller.

To create a detail window for an employee, two classes are required:

  • The frame class that extends org.openswing.swing.mdi.client.InternalFrame; this class will contain a org.openswing.swing.form.client.Form object, i.e. a JPanel that contains graphics controls.
  • The form controller class that extends org.openswing.swing.form.client.FormController; all events fired by the Form are listened by the form controller, such as save for insert, save for update, delete rows.

All graphics controls within the Form panel are mapped with a specific attribute of value object supported by the Form.

The Form object can retrieve data to set into the graphics controls by means of “loadData” method defined into the form controller: this method has the purpose of retrieve a value object that will be stored inside the form data model.

  public Response loadData(Class valueObjectClass) {
    try {
      String sql =
        "select EMP.EMP_CODE,EMP.FIRST_NAME, EMP.LAST_NAME,EMP.DEPT_CODE,DEPT.DESCRIPTION,EMP.TASK_CODE,TASKS.DESCRIPTION,EMP.SEX,EMP.HIRE_DATE,EMP.SALARY,EMP.NOTE "+
        "from EMP,DEPT,TASKS where EMP.DEPT_CODE=DEPT.DEPT_CODE and EMP.TASK_CODE=TASKS.TASK_CODE and EMP.EMP_CODE='"+pk+"'";

      // mapping between attributes and database fields...
      Map attribute2dbField = new HashMap();
      attribute2dbField.put("empCode","EMP.EMP_CODE");
      attribute2dbField.put("firstName","EMP.FIRST_NAME");
      attribute2dbField.put("lastName","EMP.LAST_NAME");
      attribute2dbField.put("deptCode","EMP.DEPT_CODE");
      attribute2dbField.put("deptDescription","DEPT.DESCRIPTION");
      attribute2dbField.put("taskCode","EMP.TASK_CODE");
      attribute2dbField.put("taskDescription","TASKS.DESCRIPTION");
      attribute2dbField.put("sex","EMP.SEX");
      attribute2dbField.put("hireDate","EMP.HIRE_DATE");
      attribute2dbField.put("salary","EMP.SALARY");
      attribute2dbField.put("note","EMP.NOTE");

      return QueryUtil.getQuery(
        conn,
        sql,
        new ArrayList(), // list of values linked to "?" parameters in sql
        attribute2dbField,
        EmpVO.class, // v.o. to dinamically create for each row...
        "Y",
        "N",
        true // log query...
      );
    }
    catch (Exception ex) {
      ex.printStackTrace();
      return new ErrorResponse(ex.getMessage());
    }

/*
    // an alternative way: you can define your own business logic to retrieve data and adding filtering/sorting conditions at hand...
    Statement stmt = null;
    try {
      stmt = conn.createStatement();
      ResultSet rset = stmt.executeQuery(
        "select EMP.EMP_CODE,EMP.FIRST_NAME, EMP.LAST_NAME,EMP.DEPT_CODE,DEPT.DESCRIPTION,EMP.TASK_CODE,TASKS.DESCRIPTION,EMP.SEX,EMP.HIRE_DATE,EMP.SALARY,EMP.NOTE "+
        "from EMP,DEPT,TASKS where EMP.DEPT_CODE=DEPT.DEPT_CODE and EMP.TASK_CODE=TASKS.TASK_CODE and EMP.EMP_CODE='"+pk+"'"
      );
      if (rset.next()) {
        EmpVO vo = new EmpVO();
        vo.setEmpCode(rset.getString(1));
        vo.setFirstName(rset.getString(2));
        vo.setLastName(rset.getString(3));
        vo.setDeptCode(rset.getString(4));
        vo.setDeptDescription(rset.getString(5));
        vo.setTaskCode(rset.getString(6));
        vo.setTaskDescription(rset.getString(7));
        vo.setSex(rset.getString(8));
        vo.setHireDate(rset.getDate(9));
        vo.setSalary(rset.getBigDecimal(10));
        vo.setNote(rset.getString(11));
        return new VOResponse(vo);
      }
      else
        return new ErrorResponse("No data found.");
    }
    catch (SQLException ex) {
      ex.printStackTrace();
      return new ErrorResponse(ex.getMessage());
    }
    finally {
      try {
        stmt.close();
      }
      catch (SQLException ex1) {
      }
    }
*/
  }

As you can see from the example reported above, QueryUtil.getQuery method can be used to retrieve a single value object and returns it in a VOResponse object. You are free to define your own data retrieval strategy, without QueryUtil class, e.g. using JDBC API.

Note that you could create inner value objects inside a main value object: for example a main value object named EmpVO that does not directly contain deptCode, deptDescription, taskCode and taskescription attributes; instead it could contain two inner v.o. TaskVO and DeptVO, with set/getTaskVO() and set/getDeptVO() methods; in this case input controls added to the Form panel must have attributes like "deptVO.deptCode", "deptVO.deptDescription", "taskVO.taskCode", "taskVO.taskDescription", i.e. you are free to bind input controls (or grid columns too) to attributes of nested value object. This could be useful to reuse business logic already defined for Tasks and Depts and simplify employee data retrieval.

 

Alternatives to QueryUtil class usage

Inner value objects defined inside a main value object can be a powerful way to design the whole application, by mapping database entities (tables) and relations with value objects and value object relatations (expressed an nested v.o.).

By supporting nested value objects in Form and GridControl components, OpenSwing simplify ORM tools adoption such as Hibernate.

You are free to replace QueryUtil usage with another (real) ORM facility; OpenSwing provides some classes/packages that simplify ORM tool integration with OpenSwing:

  • org.openswing.swing.util.server.HibernateUtils - allows to include filtering/sorting and pagination conditions provided by a grid control with Session.createQuery() Hibernate method
  • org.openswing.swing.util.server.IBatisUtils - allows to include filtering/sorting and pagination conditions provided by a grid control with SqlMapClient.queryForList() iBatis method
  • org.openswing.springframework- contains a set of utility classes that allows to use Spring framework as server-side layer of you application (see Spring integration section).

 

Adding a grid control depending on detail form

To add a grid in the employee detail frame (working days of the week), you have to add the GridControl outside the Form panel: this panel allows to add input controls only. You are free to add the GridControl in any other panel: e.g. in another folder, in a split pane or simply in another panel.

There are two main issues to resolve:

  • load data in grid only the the Form is in read only mode and only after Form data loading has been completed (because Form's value object will contains data that is required to correctly load grid)
  • enable/disable grid buttons (edit, insert, delete, ...) according to Form mode: in insert mode (maybe in edit mode too) grid should not be changed, so buttons must be disabled in insert mode; when Form is in read only mode, grid buttons should be enabled. Moreover grid should be empty when deleting Form content or when switching it to insert mode.

To fix all issues described above, OpenSwing provides many callback methods in FormController class:

  • by overriding "loadDataCompleted" method, you are sure that Form data loading has been completed (Form and grid data loading are always performed in separated threads to do not block the application) ; you can insert in this method a call to GridControl.reloadData() method to force grid data loading; before calling this method you can extract from Form v.o. data that could be needed to the grid data retrieval: you can pass that data to GridControl.getOtherGridParams() Hashtable, used when retrieving data for grid.
  • you can force grid buttons abilitation in "loadDataCompleted" method and disable them in "modeChanged" callback method that is invoked by the Form each time Form mode is changed
  • you can empty the grid by invoking GridControl.clearData() method in "modeChanged" callback method when the Form mode is switched to INSERT or after correctly deleting record in "deleteRecord" callback method.

 

How creating a 3tier client-server application

Until now, in this tutorial has been described how to connect graphical components (Form and GridControl) directly to the data access layer, by creating a two-layered client-server application. You can easly create a 3-layered client-server application where graphical components does not directly call data access methods, instead these GUI remotely invoke a server-side layer which phisically perfoms data access to a database. Client to server comunication can be realized by using any comunication layer, such as HTTP. OpenSwing provides the ClientUtil.getData() utility method to invoke via HTTP a set of server-side classes, by contacting a Servlet (see "Utility classes used in server-side tier" section).

In this way you can develop Rich Internet Applications having a GUI based on Swing, by using OpenSwing client components and by connect ingthese components via HTTP to a server-side layer, such as Spring (see Spring integration section). In server-side layer you are free to use QueryUtil class or a more powerful ORM layer, such as Hibernate or iBatis.

Licence | Contacts | ©2007 Mauro Carniel SourceForge.net Logo