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.
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:
To create a window that contains a grid, two classes are required:
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
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 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.
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:
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.
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:
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:
To fix all issues described above, OpenSwing provides many callback methods in FormController class:
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 or Apache Cayenne or JPA layer.