This chapter shows how to use the framework to build a table editor. The HTML form is generated dynamically by a JSP. The framework still does the routine tasks such as form data handling, user error handling, etc. XML-based persistence was implemented for user data. A discussion about thread safety was included at the end of the chapter.
RowBean is a Java bean whose instances maintain the row data of a table. It has three standard properties (a boolean and two strings) that can be accessed using the get & set methods. The clone() method can be used to create copies of a bean object.
RowBean.java:
package com.devsphere.examples.mapping.dynamic;
/**
* Row bean
*/
public class RowBean implements java.io.Serializable, Cloneable {
private boolean selected;
private String text;
private String combo;
/**
* No-arg constructor
*/
public RowBean() {
}
/**
* Gets the selected property
*/
public boolean isSelected() {
return this.selected;
}
/**
* Sets the selected property
*/
public void setSelected(boolean value) {
this.selected = value;
}
/**
* Gets the text property
*/
public String getText() {
return this.text;
}
/**
* Sets the text property
*/
public void setText(String value) {
this.text = value;
}
/**
* Gets the combo property
*/
public String getCombo() {
return this.combo;
}
/**
* Sets the combo property
*/
public void setCombo(String value) {
this.combo = value;
}
/**
* Returns a clone of this bean object
*/
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
TableBean uses an array of RowBean instances to store the data of an entire table. The other two properties (command and fileName) are used to communicate with the processor component described in a later section. In addition to the access methods, the TableBean class implements several table operations: create table, clone table, count selected rows, append new row, delete selected rows.
TableBean.java:
package com.devsphere.examples.mapping.dynamic;
/**
* Table bean
*/
public class TableBean implements java.io.Serializable, Cloneable {
public static final int DEFAULT_ROW_COUNT = 3;
private String command;
private String fileName;
private RowBean rowArray[];
/**
* No-arg constructor
*/
public TableBean() {
}
/**
* Gets the command property
*/
public String getCommand() {
return this.command;
}
/**
* Sets the command property
*/
public void setCommand(String value) {
this.command = value;
}
/**
* Gets the fileName property
*/
public String getFileName() {
return this.fileName;
}
/**
* Sets the fileName property
*/
public void setFileName(String value) {
this.fileName = value;
}
/**
* Gets the rowArray property
*/
public RowBean[] getRowArray() {
return this.rowArray;
}
/**
* Sets the rowArray property
*/
public void setRowArray(RowBean values[]) {
this.rowArray = values;
}
/**
* Gets an element of the rowArray property
*/
public RowBean getRowArray(int index) {
return this.rowArray[index];
}
/**
* Sets an element of the rowArray property
*/
public void setRowArray(int index, RowBean value) {
this.rowArray[index] = value;
}
/**
* Returns a clone of this bean object
*/
public Object clone() {
TableBean tableClone = null;
try {
tableClone = (TableBean) super.clone();
if (tableClone.rowArray != null) {
tableClone.rowArray = (RowBean[]) tableClone.rowArray.clone();
for (int i = 0; i < tableClone.rowArray.length; i++)
tableClone.rowArray[i] = (RowBean) tableClone.rowArray[i].clone();
}
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return tableClone;
}
/**
* Returns the number of selected rows
*/
public int countSelectedRows() {
int count = 0;
for (int i = 0; i < rowArray.length; i++)
if (rowArray[i].isSelected())
count++;
return count;
}
/**
* Appends a new row at the end of the table
*/
public void appendNewRow() {
RowBean newRowArray[] = new RowBean[rowArray.length + 1];
for (int i = 0; i < rowArray.length; i++)
newRowArray[i] = rowArray[i];
newRowArray[rowArray.length] = new RowBean();
setRowArray(newRowArray);
}
/**
* Deletes the selected rows
*/
public void deleteSelectedRows() {
RowBean newRowArray[] = new RowBean[rowArray.length-countSelectedRows()];
for (int i = 0, j = 0; i < rowArray.length; i++)
if (!rowArray[i].isSelected())
newRowArray[j++] = rowArray[i];
setRowArray(newRowArray);
}
/**
* Creates a table having a default number of empty rows
*/
public static TableBean createEmptyTable() {
TableBean table = new TableBean();
RowBean rowArray[] = new RowBean[DEFAULT_ROW_COUNT];
for (int i = 0; i < rowArray.length; i++)
rowArray[i] = new RowBean();
table.setRowArray(rowArray);
return table;
}
}
The HTML form used to edit the table is built dynamically by a JSP. This is necessary only because the number of rows is variable.
DynamicForm.jsp is almost pure HTML. You don't have to code the insertion of the values of the form elements. The JSP handler invokes the JSP form and parses its content. The values of the table cells are extracted from a TableBean and inserted automatically within the HTML form.
Name |
Property type |
Element type |
|
|
|
command |
String |
submit[] |
fileName |
String |
text |
rowArray.length |
int |
hidden |
rowArray.index.selected |
boolean |
checkbox |
rowArray.index.text |
String |
text |
rowArray.index.combo |
String |
select |
The JSP handler disables the showing of the user error messages by removing from the error table the MappingError objects that contain these messages. Without this, a message would be shown for every cell that wasn't filled by the user. The showing of the application error messages isn't disabled. Not to affect the layout of the table, the message variables are relocated at the end of the document. This is possible by inserting <!-- VARIABLE NAME="[ERROR_MESSAGE.rowArray.index.propertyName]" --> comments.
The mapping utilities don't handle the I/O errors that may occur during the loading/saving of XML files. In order to report these errors to the user, the io_error variable defined in the HTML form (<!-- VARIABLE NAME="io_error" -->) is used by the JSP handler.
DynamicForm.jsp:
<%@ page language="java" %>
<HTML>
<HEAD><TITLE>DynamicForm</TITLE></HEAD>
<BODY>
<CENTER>
<H3>Dynamic Example</H3>
<FORM METHOD="POST">
<%
int count = 0;
try { count = Integer.parseInt(request.getParameter("count")); }
catch (Throwable t) { }
%>
<P>
<INPUT TYPE="TEXT" NAME="fileName" LENGTH="20">
<INPUT TYPE="SUBMIT" NAME="command" VALUE="Load">
<INPUT TYPE="SUBMIT" NAME="command" VALUE="Save">
<BR><!-- VARIABLE NAME="io_error" -->
<P>
<INPUT TYPE="SUBMIT" NAME="command" VALUE="Append New Row">
<INPUT TYPE="SUBMIT" NAME="command" VALUE="Delete Selected Rows">
<INPUT TYPE="SUBMIT" NAME="command" VALUE="Undo">
<P>
<INPUT TYPE="HIDDEN" NAME="rowArray.length" VALUE="<%=count%>">
<TABLE BORDER="1" CELLPADDING="3" CELLSPACING="1">
<TR>
<TH>Selected</TH>
<TH>Text</TH>
<TH>Combo</TH>
</TR>
<% for (int i = 0; i < count; i++) { %>
<TR>
<TD>
<CENTER>
<INPUT TYPE="CHECKBOX" NAME="rowArray.<%=i%>.selected">
</CENTER>
</TD>
<TD>
<INPUT TYPE="TEXT" NAME="rowArray.<%=i%>.text" LENGTH="30">
</TD>
<TD>
<SELECT NAME="rowArray.<%=i%>.combo" SIZE="1">
<OPTION VALUE=""></OPTION>
<OPTION VALUE="1">Item 1</OPTION>
<OPTION VALUE="2">Item 2</OPTION>
<OPTION VALUE="3">Item 3</OPTION>
</SELECT>
</TD>
</TR>
<% } %>
</TABLE>
</FORM>
<!-- VARIABLE NAME="[ERROR_MESSAGE]" -->
<!-- VARIABLE NAME="[ERROR_MESSAGE.fileName]" -->
<% for (int i = 0; i < count; i++) { %>
<!-- VARIABLE NAME="[ERROR_MESSAGE.rowArray.<%=i%>.selected]" -->
<!-- VARIABLE NAME="[ERROR_MESSAGE.rowArray.<%=i%>.text]" -->
<!-- VARIABLE NAME="[ERROR_MESSAGE.rowArray.<%=i%>.combo]" -->
<% } %>
</CENTER>
</BODY>
</HTML>
TableBeanResources is the single resource bundle of the application and contains the form's name, several application-specific error messages and the HTML wrappers of the error messages. The resource bundle is a .properties file.
TableBeanResources.properties:
[FORM_NAME]=DynamicForm.jsp
# Application-specific error messages
[MISSING_FILE_NAME]=You must input a file name
[FILE_NOT_FOUND]=File not found
[COULDNT_LOAD_FILE]=Couldn't load the file
[COULDNT_SAVE_FILE]=Couldn't save the file
DynamicHndl.jsp is not a typical handler. The HTML form is generated dynamically using a JSP described in a previous section of this chapter. The form data is mapped to TableBean instances that are passed to a processor declared as a session bean.
After loading the resource bundle, the handler builds a base URL by deleting its own name from the request URL. Next, it builds the path the processor uses to save the beans as XML files.
If the HTTP method isn't POST, the handler calls getCurrentState() of the processor, which returns a bean table with three empty rows at the first call.
If the HTTP method is POST, the form data is passed to FormUtils.formToBean() which fills the "cells" of a table bean. The user errors are removed from the returned Hahstable to disable the showing of the user error messages. Then, the table bean is passed to the executeCommand() method of the processor, which may append a new row, delete the selected rows, undo a change, save the data to an XML file or load an existent table from an XML file.
The JSP form is invoked using its URL (instead of <jsp:include> or <jsp:forward>) because the HTML form needs some processing. The new table bean returned by executeCommand() is passed to FormUtils.beanToForm(), which inserts the table's data into the HTML form. The processor returns any I/O error as a message stored in the command property of the table bean. If not null, this message is inserted into the HTML document.
The HTML form containing the table's data is send to the user using the send() method of the FormDocument instance.
DynamicHndl.jsp:
<%@ page language="java" %>
<%@ page import="com.devsphere.mapping.*, com.devsphere.logging.*" %>
<jsp:useBean id="dynamicProc" scope="session"
class="com.devsphere.examples.mapping.dynamic.DynamicProc"/>
<jsp:useBean id="tableBean" scope="request"
class="com.devsphere.examples.mapping.dynamic.TableBean"/>
<%
// Get the bean resources
java.util.ResourceBundle beanRes
= HandlerUtils.getBeanResources(tableBean.getClass());
// Construct the base URL
String baseURL = javax.servlet.http.HttpUtils.getRequestURL(
request).toString();
int slashIndex = baseURL.lastIndexOf('/');
baseURL = slashIndex != -1 ? baseURL.substring(0, slashIndex+1) : "";
// Construct the data path
String dataPath = request.getServletPath();
slashIndex = dataPath.lastIndexOf('/');
dataPath = slashIndex != -1 ? dataPath.substring(0, slashIndex+1) : "";
dataPath = application.getRealPath(dataPath + "data");
// Determine the HTTP method
boolean isPostMethod = request.getMethod().equals("POST");
// Create a logger that wraps the servlet context
ServletLogger logger = new ServletLogger(application);
// Declare local variables
com.devsphere.examples.mapping.dynamic.TableBean newTableBean = null;
java.util.Hashtable errorTable = null;
if (!isPostMethod) {
// Get the table of the current session
newTableBean = dynamicProc.getCurrentState();
} else {
// Wrap the form data
FormData formData = new ServletFormData(request);
// Form-to-bean mapping: request parameters are mapped to bean properties
errorTable = FormUtils.formToBean(formData, tableBean, logger);
// Ignore the user errors
errorTable = HandlerUtils.removeUserErrors(errorTable);
// Process the user command
newTableBean = dynamicProc.executeCommand(tableBean, dataPath, logger);
}
// Construct the form's URL
String formURL = baseURL + beanRes.getString("[FORM_NAME]").trim();
if (newTableBean.getRowArray() != null)
formURL += "?count=" + newTableBean.getRowArray().length;
// Get the form template
FormTemplate template = FormUtils.getTemplate(new java.net.URL(formURL));
// Get a new document
FormDocument document = template.getDocument();
// Bean-to-form mapping: bean properties are mapped to form elements
FormUtils.beanToForm(newTableBean, errorTable, document, logger);
// Show the possible I/O error
String ioError = newTableBean.getCommand();
if (ioError != null)
document.setValue("io_error", ioError);
// Send the form document
document.send(out);
%>
The application uses one processor instance per user session. This instance maintains the initial table, the current table and a stack of tables that is used to undo changes. The getCurrentState() method returns a clone of the current table. The executeCommand() method takes a bean table, performs some actions depending on the value of the command property and returns the new current table.
Before making an undoable change (adding/removing of rows), the executeCommand() method of DynamicProc pushes the current table on the stack. The undo command gets the last table from the stack and makes it the new current table. This undo mechanism works fine with forms, though it would be considered inefficient in a traditional GUI application based on events.
DynamicProc.java:
package com.devsphere.examples.mapping.dynamic;
import com.devsphere.logging.*;
import java.util.*;
/**
* Processor of table beans
*/
public class DynamicProc implements java.io.Serializable {
private Stack oldTables;
private TableBean initialState;
private TableBean currentState;
/**
* Creates the processor
*/
public DynamicProc() {
oldTables = new Stack();
initialState = TableBean.createEmptyTable();
currentState = TableBean.createEmptyTable();
}
/**
* Returns the current table bean
*/
public synchronized TableBean getCurrentState() {
return (TableBean) currentState.clone();
}
/**
* Processes a table bean and executes its command
*/
public synchronized TableBean executeCommand(
TableBean table, String dataPath, AbstractLogger logger) {
// Make a clone of the table
table = (TableBean) table.clone();
// Get the command
String command = table.getCommand();
// Clear the command property
table.setCommand(null);
// Execute the command
if (command != null)
if (command.startsWith("Load")) {
table = XMLArchiver.load(table, dataPath, logger);
if (table.getCommand() == null) {
// No error message.
// Reinitialize the processor
oldTables.removeAllElements();
initialState = (TableBean) table.clone();
}
} else if (command.startsWith("Save")) {
table = XMLArchiver.save(table, dataPath, logger);
} else if (command.startsWith("Undo")) {
// Get the last table from the stack
if (!oldTables.isEmpty())
table = (TableBean) oldTables.pop();
else
table = (TableBean) initialState.clone();
} else {
// Put the table on the stack
oldTables.push(table);
// Make a clone of the table
table = (TableBean) table.clone();
// Process the undoable command
if (command.startsWith("Append"))
table.appendNewRow();
else if (command.startsWith("Delete"))
table.deleteSelectedRows();
}
// Update the current state
currentState = table;
// Return the current state
return getCurrentState();
}
}
The save() and load() methods of XMLArchiver use XMLUtils.beanToXML() and XMLUtils.xmlToBean() to convert the bean table to XML and vice-versa. The file name is provided as a property of the table bean and any I/O error is reported via the command property. The values of these two properties shouldn't be stored in the XML file. Therefore, they are cleared before the table data is saved to the XML file. However, the fileName and command properties must participate to the mapping process in order to make the communication between the HTML form and the DynamicProc processor as easy as possible.
XMLArchiver.java:
package com.devsphere.examples.mapping.dynamic;
import com.devsphere.logging.*;
import com.devsphere.mapping.*;
import java.io.*;
import java.util.*;
/**
* Archiver of table beans
*/
public class XMLArchiver {
public static final Object FILE_LOCK = new Object();
private static final String HTML_MESSAGE_START = (String)
HandlerUtils.getResource("[HTML_MESSAGE_START]", "", TableBean.class);
private static final String HTML_MESSAGE_END = (String)
HandlerUtils.getResource("[HTML_MESSAGE_END]", "", TableBean.class);
private static final String HTML_MESSAGE_SEPARATOR = (String)
HandlerUtils.getResource("[HTML_MESSAGE_SEPARATOR]", "", TableBean.class);
private static ResourceBundle beanRes
= HandlerUtils.getBeanResources(TableBean.class);
/**
* Loads a table bean from an XML file
*/
public static TableBean load(TableBean tableBean, String dataPath,
AbstractLogger logger) {
String fileName = tableBean.getFileName();
if (fileName == null || fileName.length() == 0
|| fileName.indexOf('\\') != -1
|| fileName.indexOf('/') != -1) {
error(tableBean, "[MISSING_FILE_NAME]");
return tableBean;
}
if (!fileName.toLowerCase().endsWith(".xml")) {
fileName += ".xml";
tableBean.setFileName(fileName);
}
synchronized (FILE_LOCK) {
File file = new File(dataPath, fileName);
try {
FileInputStream in = new FileInputStream(file);
try {
Hashtable errorTable = null;
TableBean newTableBean = new TableBean();
errorTable = XMLUtils.xmlToBean(in, newTableBean, logger);
newTableBean.setFileName(fileName);
tableBean = newTableBean;
} finally {
in.close();
}
} catch (FileNotFoundException e) {
error(tableBean, "[FILE_NOT_FOUND]");
} catch (IOException e) {
error(tableBean, "[COULDNT_LOAD_FILE]");
logger.log(e);
}
}
return tableBean;
}
/**
* Saves a table bean to an XML file
*/
public static TableBean save(TableBean tableBean, String dataPath,
AbstractLogger logger) {
String fileName = tableBean.getFileName();
if (fileName == null || fileName.length() == 0) {
error(tableBean, "[MISSING_FILE_NAME]");
return tableBean;
}
if (!fileName.toLowerCase().endsWith(".xml"))
fileName += ".xml";
synchronized (FILE_LOCK) {
new File(dataPath).mkdirs();
File file = new File(dataPath, fileName);
try {
FileOutputStream out = new FileOutputStream(file);
try {
tableBean.setFileName(null);
XMLUtils.beanToXML(tableBean, null, out, logger);
} finally {
tableBean.setFileName(fileName);
out.close();
}
} catch (IOException e) {
error(tableBean, "[COULDNT_SAVE_FILE]");
logger.log(e);
}
}
return tableBean;
}
/**
* Signals an application-specific error
*/
private static void error(TableBean tableBean, String key) {
StringBuffer buf = new StringBuffer();
buf.append(HTML_MESSAGE_START);
try {
String message = beanRes.getString(key);
message = FormUtils.messageToHTML(message);
buf.append(message);
} catch (MissingResourceException e) {
buf.append(key);
}
buf.append(HTML_MESSAGE_END);
tableBean.setCommand(buf.toString());
}
}
All classes of the framework are thread safe. This section discusses the need to synchronize some of the code of this example application.
A user could perform multiple concurrent requests during the same session. The servlet engine may create a pool of threads running the code of the JSP handler. Therefore, the same processor instance may be accessed from concurrent threads. Making the processor's methods synchronized is necessary, but not enough.
DynamicHndl.jsp creates instances of TableBean, which aren't thread safe. Therefore each bean object must be used in a single thread at a given time. The JSP handler passes a table bean to the executeCommand() method of the processor. It is OK to access this bean object within executeCommand(), but the processor must not keep any reference to the table bean instance because it could be accessed from other threads. In other words, a bean object that was created by the JSP handler must not be stored to oldTables or currentState. To ensure this, executeCommand() creates a clone of the bean object.
The table beans created by the JSP handler are accessed in a single thread. Unlike them the table beans created internally by the processor may be accessed from multiple threads activated by requests of the same session. (The user could open multiple windows of the browser and make concurrent requests.) This is OK too because the methods of DynamicProc are declared synchronized, which means that their code cannot be executed concurrently. In other words, only one thread of a request may execute the methods of a processor instance at a given time.
Both getCurrentState() and executeCommand() methods return a bean object, which is used further by the JSP handler. To avoid the concurrent access to these objects, the processor's methods must not return bean objects used internally by them. In addition, the same object shouldn't be returned twice. The solution is to return newly created clones of the bean object and not use the returned clones internally. This way, the clone of the current table will be used further only by the handler invocation that gets it.
One last word about thread safety. Since exectuteCommand() is synchronized, a user can't access the XML files at the same time within two concurrent requests of the same session. This isn't enough in a multi-user environment. To avoid file conflicts, the XML documents should not be accessed concurrently by different sessions. For instance, a user shouldn't be able to load an XML file while another user saves the same file. The simplest solution is to a have a global file lock (XMLArchiver.FILE_LOCK) that ensures that a single file is accessed at a given time. A possible better approach would be to create a separate data directory for each user. In this case the users wouldn't be able to share the files, unless an exchange mechanism is implemented.
The FormUtils.getTemplate() method allows you to build FormTemplate instances from
- a File
- a URL
- an InputStream
- a Reader
- a String
A previous chapter describes the way bean objects are mapped to XML documents and vice-versa.
|