Viewing forms and generating code
Andy is a solution architect. He can be reached at [email protected].
Apache's Jakarta Struts is rapidly becoming the de facto standard MVC (Model-View-Controller) Framework for building Java web applications. For Struts applications, there is usually an Action class associated with each JavaServer Page (JSP). If the JSP is an input form, there is also an associated ActionForm class that handles input data, unless you are using the DynaActionForm and Validator framework new to Struts 1.1. But for small to medium projects, ActionForm classes may be simpler to use.
There are a number of freely available tools to automatically generate ActionForm classes; for example, the Easy Struts plug-in for IDEs such as Eclipse and Borland's JBuilder. There are even IDEs such as Struts Studio that are specifically built for developing Struts applications. Easy Struts (http://www.easystruts.sourceforge .net.com/) generates ActionForm/Action classes as well as JSPs. Easy Form (one of the Easy Struts wizards) requires users to define the name and type of input before generating the code for ActionForm/Action classes and JSP. Struts Studio (http://www.exadel.com/) generates ActionForm and Action classes, but the generated ActionForm classes do not have any getter/setter methods for passing form parameters. They have to be added manually. Still, they are good tools for building Struts applications.
When building web applications, I usually start with storyboardingthat is, designing the overall structure of the site and some web pages before coding the business logic. I want to be able to quickly put together some web pages, including forms, using the Struts tag libraries and see how they look. However, to view a Struts form, you must go through a number of steps:
- Define the mapping between form-beans (ActionForms) and actions (Actions) in the struts-config.xml file.
- Code the Action and ActionForm classes for the form.
- Compile the classes.
- Restart the web container or the Struts ActionServlet.
It occurred to me that what I'd really like to do is simply upload and view a Struts form coded using the Struts tag libraries without updating the Struts configuration file and restarting the web container. Moreover, I'd like for the tool that does this to generate the ActionForm class(es) from the form so that it/they can be used later for production. In this article, I present a tool that does just this. The complete source code for this tool is available electronically; see "Resource Center," page 5.
Design
The main requirement of this tool is to be able to upload a JSP form coded using the Struts tag libraries and display it without additional development work. The intention is to preview Struts forms during the development cycle instead of for use in a production environment.
To do this, you must first understand how Struts processes such a request:
- Struts checks the configuration for an Action mapping to an ActionForm.
- If an ActionForm associated with the Action is found, it checks to see if an instance of the ActionForm with the proper scope (request or session) is present.
- If a proper instance is found, it reuses that instance; otherwise, it instantiates a new one and saves it in the proper scope.
- Struts calls the ActionForm's reset() method to initialize or reset its properties and displays the JSP form.
- When the submit button is clicked, Struts populates the ActionForm properties with the input parameters using the ActionForm's setter methods.
- If input validation is set to True, it calls the ActionForm's validate() method and invokes the associated action.
To display a JSP form using the Struts framework, associated Action and ActionForm entries have to be configured in the Struts configuration file. Two approaches for achieving this come to mind:
- For each upload, reconfigure the Struts configuration file to add the new Action/ActionForm mapping, generate and compile the associated Action and ActionForm classes.
- Reuse the mapping of an existing Action/ActionForm, generate and compile a new ActionForm using the existing name configured in Struts. The Action class does not need to be changed.
The first approach requires that the Struts configuration file be updated for each upload. For Struts to reread the configuration file, the Struts ActionServlet must be restarted. Another issue with this approach is that after a while the Struts configuration file is cluttered with many unwanted entries.
The second approach is simpler because it only involves generating/compiling/reloading an ActionForm class from the uploaded JSP. Both the uploaded JSP form and generated ActionForm must be renamed to match a particular Action/ActionForm mapping in the Struts configuration file.
The Struts application built using the second approach is meant to be a personal Struts productivity tool only. It is a personal tool because it requires featuresautoloading of modified Java classes and autocompilation of modified JSPson the web container that are usually turned off in production environments. Another limitation is that for every upload, the resultant JSP and ActionForm class always use the same name. Race conditions result if multiple users are uploading JSP forms for preview at the same time. The intent is to provide a personal productivity tool to help you preview your JSP forms without reconfiguring Struts and restarting the web container. Since you are usually the only user of the application, the single-user limitation is not an issue.
Implementation
The overall organization of this tool is straightforward. The welcome page (index.jsp) redirects to the /pages/fileupload.jsp. It is a simple file upload form. The Action and ActionForm classes are called FileUploadAction and FileUploadForm, respectively. The FileUploadAction class does all the processing and performs the following tasks:
1. Uploads the file (JSP form) and saves it temporarily in a string.
2. Generates one or more ActionForm classes depending on whether there are multiple forms in the JSP and saves the class(es) as a string in the session context. Each class has getter/setter methods in addition to the reset() and validate() methods. The name of the ActionForm class generated is derived from the html:form tag's action attribute. For example, for action="UserInput.do", the ActionForm class name generated will be UserInputForm. It takes the part of the attribute value before the ".do" and appends "Form" to it.
3. Generates the UserPageForm class based on the content of the uploaded JSP. UserPage and UserPageForm are pre-configured entries in the Struts configuration file (Listing One) used in displaying the uploaded form. Even if there is more than one form in the uploaded JSP form, only one ActionForm (UserPageForm) will be created. getter/setter methods from the second form onwards are added to UserPageForm. The content is saved in /temp/ UserPageForm.java.
4. Compiles the /temp/UserPageForm.java file using the batch file /temp/buildform.bat.
5. Replaces all occurrences of the action references to "UserPage.do" and all references of ActionForm named XXXForm in the uploaded JSP form to "UserPageForm." UserPage.do and UserPageForm are the preconfigured action and ActionForm names. The modified content of the uploaded JSP form is saved in the file /pages/userpage.jsp.
6. Forwards to the /pages/viewsource.jsp.
The /pages/viewsource.jsp retrieves the string containing the generated ActionForm class(es) from the session context for display on the screen. It also provides a link to view the uploaded page. The ActionForm class(es) displayed by this page is different from the UserPageForm.java. The class(es) displayed are meant to be copied and pasted by users into ActionForm Java file(s) for use with the unmodified upload page in the production environment (after users add the validation logic). The UserPageForm.java generated and saved on the server is for use with the modified uploaded page for display purpose. Struts displays the page because it has the mapping configured in the struts- config.xml file.
When users click on the View Form link, the tool displays the uploaded JSP form. Clicking on the Submit button brings users back to the upload page.
Generating the ActionForm classes in steps 2 and 3 is done using the five classes in the Codegen package:
- PrintfFormat.java, a utility class from Sun Microsystems that implements the C-style sprintf functionality.
- FormElement.java, that records four pieces of informationID, Name, Value, and Multifor use by BeanGenerator to generate ActionForms. ID identifies the Struts HTML library tag that is to be processed. Name records the name of the input parameter for which getter/setter methods will be generated. Value contains the initial value of the parameter. Multi indicates whether the parameter is to be saved as a simple property or a property array.
- FormObject.java, used to accumulate FormElements in a Java vector as the FormParser parses the JSP form. The FormObject is used by the BeanGenerator to access the individual FormElements to generate code.
- FormParser.java, used to parse a JSP form written using a combination of Struts tag libraries and HTML into FormElements and saved in a FormObject. It handles Struts HTML tags including: text, password, checkbox, radio, file, button, text area, select, hidden, multibox, and form.
- BeanGenerator.java, uses a FormParser object to parse a JSP form into FormElements contained in a FormObject and generates ActionForm beans. It provides methods to generate ActionForm beans based on the original JSP form (step 2) and a UserPageForm bean (step 3). This class has a main(). It can be used as a command-line program to generate ActionForm beans and display them on the screen. You invoke it like this: java BeanGenerator file1 file2 ...
The FileUploadAction class's execute() method uses a BeanGenerator object to generate the UserPageForm bean used for JSP form preview. It also generates one or more ActionForm beans for the uploaded JSP for display by the viewsource.jsp depending on whether there are multiple forms on the one JSP page uploaded. It spawns off another process to execute the /temp/buildform.bat file to compile the UserPageForm.java file. The buildform.bat file is for Windows only. You need to write your own shell script to compile the ActionForm bean if you are running this application on Linux or other UNIX systems.
All the Action and ActionForm Java source files are put into the application package. The Constants.java file defines string constants such as global forwards to be shared among the Action and ActionForm files.
Examples
Listing One shows the struts-config.xml file that defines:
- Two form beans (ActionForm classes) for the application. The FileUploadForm is for uploading the JSP to the application. The UserPageForm is reserved for displaying the uploaded JSP.
- There are two actions: FileUploadAction is invoked with the FileUploadForm, which is passed to it when a request to the URI /beanGenerator/FileUpload is made by users. beanGenerator is the name of this app. No parameter validation is to be performed. The Action forwards to /pages/viewsource.jsp. UserPageAction is invoked with the UserPageForm that is passed to it when a request to the URI /beanGenerator/UserPage is made by users, and so. The ActionForm beans are of request scope.
- Only a base resource bundle has been defined for this application. The messages in the bundle are kept in the file application.properties. When a key is not located in the bundle, it displays the key instead of a Null string.
I used Exadel's Struts Studio Community Edition (http://www.exadel.com/) to build this application. The Struts Studio IDE allows viewing the struts configuration file either diagrammatically or as an XML file. Figure 1 shows the Struts Studio IDE with the Struts configuration displayed as a diagram.
Listing Two (MultiOrder.jsp) is the file uploaded for viewing. It contains six forms. Listing Three (/pages/userpage.jsp) is the modified uploaded page to map to the existing Struts configuration file. Listing Four (displayed by /pages/viewsource.jsp) shows the ActionForm classes generated for use with the original multiform.jsp for use in the production environment. Listing Five (UserPageForm.java) is the ActionForm class generated for use with the modified uploaded page (/pages/userpage.jsp) to display it and accept input. If the CheckOut form had any input widgets, all its getter/setter methods would have been added to the UserPageForm class. Figure 2 shows the display of this uploaded page.
A number of more complicated JSP forms have been included, together with the source code of this application, as examples and for testing the application. You should try them out and examine the code generated for the ActionForm beans as well as the modified uploaded page (/pages/userpage.jsp) and its ActionForm (/temp/UserPageForm.java) after each upload to fully understand how this application works.
Conclusion
The main limitation of the application is that it uses a fixed Action/ActionForm mapping for previewing uploaded JSP forms. Consequently, the tool can only be used by one user at a time. If more than one user is uploading a JSP form for preview, the application may be modifying the /pages/userform.jsp and /temp/UserPageForm.java files at the same time and causes errors when displaying the uploaded page.
This tool is written for Windows. To run it on nonWindows platforms, write your own shell script named "buildform.sh" for compiling the UserPageForm.java. The content of your shell script should be quite similar to that of the Windows buildform.bat file. The application (Constant.java's getBuildFileName() static method) detects the platform by looking at the platform-separator character. If the character is "/", it is assumed to be Windows and "buildform.bat" is invoked to compile the action form. Otherwise, "buildform.sh" is invoked.
DDJ
Listing One
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/ dtds/struts-config_1_1.dtd"> <struts-config> <data-sources/> <form-beans> <form-bean name="FileUploadForm" type="application.FileUploadForm"/> <form-bean name="UserPageForm" type="application.UserPageForm"/> </form-beans> <global-exceptions/> <global-forwards> <forward name="upload" path="/pages/fileupload.jsp"/> <forward name="view" path="/pages/userpage.jsp"/> </global-forwards> <action-mappings> <action name="FileUploadForm" path="/FileUpload" scope="request" type="application.FileUploadAction" validate="no"> <forward name="viewsource" path="/pages/viewsource.jsp"/> </action> <action name="UserPageForm" path="/UserPage" scope="request" type="application.UserPageAction" validate="no"/> </action-mappings> <controller/> <message-resources null="false" parameter="application"/> </struts-config>
Listing Two
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %> <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %> <html:html locale="true"> <head> <title>Shopping Cart Content</title> </head> <body> <h3>Shopping Cart Content</h3> <table border="1" align="center"> <tr bgcolor="#808080"> <th>Item Id</th><th>Item Description</th><th>Unit Cost</th> <th>Quantity</th><th>Total Cost</th> < % for (int i = 0; i < 5; i++) { %> <tr> <td><%= i %></td> <td>Widget <%= i %></td> <td><%= 10.95 + i * 10 %></td> <td> <html:form action="Order.do"> <html:hidden property="itemId" value="Id<%= i %>" /> <html:text property="quantity" value="1" /> <html:submit value="Update Quantity" /> </html:form> </td> <td><%= 10.95 + i * 10 %></td> </tr> <% } %> </table> <html:form action="CheckOut.do"> <center> <html:submit value="Checkout" /> </center> </html:form> </body> </html:html>
Listing Three
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %> <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %> <html:html locale="true"> <head> <title>Shopping Cart Content</title> </head> <body> <h3>Shopping Cart Content</h3> <table border="1" align="center"> <tr bgcolor="#808080"> <th>Item Id</th><th>Item Description</th><th>Unit Cost</th> <th>Quantity</th><th>Total Cost</th> <% for (int i = 0; i < 5; i++) { %> <tr> <td><%= i %></td> <td>Widget <%= i %></td> <td><%= 10.95 + i * 10 %></td> <td> <html:form action="UserPage.do"> <html:hidden property="itemId" value="Id<%= i %>" /> <html:text property="quantity" value="1" /> <html:submit value="Update Quantity" /> </html:form> </td> <td><%= 10.95 + i * 10 %></td> </tr> <% } %> </table> <html:form action="UserPage.do"> <center> <html:submit value="Checkout" /> </center> </html:form> </body> </html:html>
Listing Four
// ActionForm class: OrderForm.java // Generated by beanGenerator on: Sun Aug 10 12:29:14 EST 2003 import javax.servlet.http.HttpServletRequest; import org.apache.struts.action.ActionError; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionMapping; import org.apache.struts.upload.FormFile; public class OrderForm extends ActionForm { private String itemId = ""; public String getItemId() { return itemId; } public void setItemId(String itemId) { this.itemId = itemId; } private String quantity = "1"; public String getQuantity() { return quantity; } public void setQuantity(String quantity) { this.quantity = quantity; } public void reset(ActionMapping actionMapping, HttpServletRequest request) { // TODO: Write method body quantity = "1"; itemId = ""; } public ActionErrors validate(ActionMapping actionMapping, HttpServletRequest request) { // TODO: Write method body throw new UnsupportedOperationException("Method not implemented"); } } // ActionForm class: CheckOutForm.java // Generated by beanGenerator on: Sun Aug 10 12:29:14 EST 2003 import javax.servlet.http.HttpServletRequest; import org.apache.struts.action.ActionError; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionMapping; import org.apache.struts.upload.FormFile; public class CheckOutForm extends ActionForm { public void reset(ActionMapping actionMapping, HttpServletRequest request) { // TODO: Write method body } public ActionErrors validate(ActionMapping actionMapping, HttpServletRequest request) { // TODO: Write method body throw new UnsupportedOperationException("Method not implemented"); } }
Listing Five
package application; // ActionForm class: UserPageForm.java // Generated by beanGenerator on: Sun Aug 10 12:29:14 EST 2003 import javax.servlet.http.HttpServletRequest; import org.apache.struts.action.ActionError; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionMapping; import org.apache.struts.upload.FormFile; public class UserPageForm extends ActionForm { private String itemId = ""; public String getItemId() { return itemId; } public void setItemId(String itemId) { this.itemId = itemId; } private String quantity = "1"; public String getQuantity() { return quantity; } public void setQuantity(String quantity) { this.quantity = quantity; } public void reset(ActionMapping actionMapping, HttpServletRequest request) { // TODO: Write method body quantity = "1"; itemId = ""; } public ActionErrors validate(ActionMapping actionMapping, HttpServletRequest request) { // TODO: Write method body throw new UnsupportedOperationException("Method not implemented"); } }