Struts Tutorial
Struts Tutorial
The application you are going to create mimics entering an employee into a database. The user will be required to enter an employee's name and age. Concepts introduced in Lesson I: Setting up your environment Data Transfer Object ActionForm web.xml struts-config.xml ApplicationResources.properties BeanUtils Tag usage The steps (see left menu) in this lesson will walk you through building all of the necessary components for this small application. If you want to download the complete application you can do so. (You should be able to plop this war file into your application server webapps directory and it should work fine). Download rr_lesson_1 application.war Begin lesson now by clicking START.
Copy .jar files from struts into rr_lesson_1 application: Next copy the following .jar files from {StrutsDirectory}/contrib/struts-el/lib into rr_lesson_1/WEB-INF/lib directory: commons-beanutils.jar commons-collections.jar commons-digester.jar commons-logging.jar jstl.jar standard.jar struts-el.jar struts.jar (Note we are using the tld files and jars in the contributed struts-el directory since this will help us to use the standard JSTL tags whenever possible).
return age; } }
Note: we do not have to return the EmployeeDTO back. You might want to return a boolean or some other variable instead that indicates success. Failure should throw an Exception and our insertEmployee method in real life would probably handle some sort of DAOException if it were thrown.
Create EmployeeForm:
package net.reumann; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionMapping; public class EmployeeForm extends ActionForm { private String name; private String age; public void setName(String name) { this.name = name; } public void setAge(String age) { this.age = age; } public String getName() { return name; } public String getAge() { return age; } }
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>application</param-name> <param-value>ApplicationResources</param-value> </init-param> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>3</param-value> </init-param> <init-param> <param-name>detail</param-name> <param-value>3</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- Action Servlet Mapping --> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>/do/*</url-pattern> </servlet-mapping> <!-- The Welcome File List --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>
<!-- tag libs --> <taglib> <taglib-uri>struts/bean-el</taglib-uri> <taglib-location>/WEB-INF/struts-bean-el.tld</taglib-location> </taglib> <taglib> <taglib-uri>struts/html-el</taglib-uri> <taglib-location>/WEB-INF/struts-html-el.tld</taglib-location> </taglib> <taglib> <taglib-uri>struts/logic-el</taglib-uri> <taglib-location>/WEB-INF/struts-logic-el.tld</taglib-location> </taglib> <taglib> <taglib-uri>jstl/c</taglib-uri> <taglib-location>/WEB-INF/c.tld</taglib-location> </taglib> </web-app>
Note: Any URI call to /do/* will send the request to this ActionServlet. (You don't have to use /do/*, you can use whatever you want- ie. /*.action, /*.do, etc). Also note that all of the tags we will use are defined in this file as well.
</form-beans> <!-- Action Mapping Definitions --> <action-mappings> <action path="/setupEmployeeForm" forward="/employeeForm.jsp"/> <action path="/insertEmployee" type="net.reumann.InsertEmployeeAction" name="employeeForm" scope="request" validate="false" > <forward name="success" path="/confirmation.jsp"/> </action> </action-mappings> <!-- message resources --> <message-resources parameter="ApplicationResources" null="false" /> </struts-config>
Some points about the struts-config.xml file: Notice we defined a form-bean definition for the EmployeeForm (ActionForm) that we created in Step 6. The name property was called "employeeForm." Now look at our the action-mapping for the path "/insertEmployee" in the struts-config file. When a user submits a form (or link) with the path /do/insertEmployee the request will be sent to the ActionServlet which we defined in the web.xml. Eventually a RequestProcessor object called from the ActionServlet will find this mapping and send the request to the type of Action class that we define in this mapping. (You'll learn about the Action class next). This Action class object is defined where you see type="net.reumann.InsertEmployeeAction". Our request will go to this class and when it exists you will see it try to get the 'forward' mapping with the name "success" and will forward to the page defined in our mapping (/confirmation.jsp). The last thing to note is the defintion of the message-resources. The ApplicationReources value refers to a properties file (ApplicationResources.properties) that we are going to add to our classes directory. This file will hold key/value pairs that will save us from having to hard code information directly in our JSPs.
Notice our action mapping in the struts-config file: <action path="/insertEmployee" type="net.reumann.InsertEmployeeAction" name="employeeForm" scope="request" > <forward name="success" path="/confirmation.jsp"/> </action> The path attribute will correspond to the action we will define for our JSP form. When the form is submitted with this action path our request will make it to InsertEmployeeAction. When it exits the InsertEmployeeAction it will forward to confirmation.jsp by looking up the foward name "success." Create InsertEmployeeAction: package net.reumann; import org.apache.struts.action.Action; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionForm; import org.apache.commons.beanutils.BeanUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public final class InsertEmployeeAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { EmployeeService service = new EmployeeService(); EmployeeForm employeeForm = (EmployeeForm) form; EmployeeDTO employeeDTO = new EmployeeDTO(); BeanUtils.copyProperties( employeeDTO, employeeForm ); service.insertEmployee( employeeDTO ); request.setAttribute("employee",employeeDTO); return (mapping.findForward("success")); }
The Action class has one method to worry about "execute(..)." When we submit our JSP page the behind the scenes Struts stuff (Action Servlet and RequestProcessor) will find this Action class that we associated with the /insertEmployee action in the struts-config.xml file and the execute method will be called. Since the /insertEmployee action uses the EmployeeForm the InsertEmployeeAction will have this form bean of type ActionForm, so we first cast this into type EmployeeForm. Now we want to get the information in our EmployeeForm into the EmployeeDTO so we can pass that off to our Model layer. We created an instance of EmployeeDTO and then, like magic, we can use BeanUtils (from the Jakarta commons package) to transfer the data from EmployeeForm into the EmployeeDTO with one easy line: BeanUtils.copyProperties( employeeDTO, employeeForm ). This is a HUGE time saver, for if you are not going to use BeanUtils (or PropertyUtils) than you would normally build a helper class to do all the get and set stuff and all the data type conversions (a real pain, especially for large beans). After copyProperties() the now populated EmployeeDTO is passed off to the service object and the insert would be done. (We'll learn more in a later lesson about dealing with Exceptions that your model/business objects may throw). After the insert is done the EmployeeDTO is stuck into request scope so we could use the employee information for display purposes. The last thing to notice is that we need to return an ActionForward object which will tell our behind the scenes controller where we should forward to when the execute method is completed. In this case I want to forward to whatever I defined as "success" in my strutsconfig mapping for this action. Calling mapping.findFoward("success") will return an ActionForward object based on looking up the foward pararmeter in our mapping that matches "success" (in this case /confirmation.jsp). (Note: Rather than hard-code the word "success" here you should create a Constants class/interface that has these commonly used String variables).
In this application, this is the first page the user will see. Notice the taglibs needed are defined at the top. The first html:rewrite tag will prepend our webapp context (in this case rr_lesson_1) to /rr.css. This is a useful tag since it enables us to not have to hard code your webapp name in your code. Next the bean:message tag is used to look up the key 'title' in the ApplicationResources.properties file and write that to the page. Then notice the use of the html:link page. The html:link tag is useful since it will write out an <a href..> tag using the correct path to your root directory added to the beginning of your link. It also appends a sessionID in case the user has cookies disabled. Also notice that the link is calling /do/setupEmployeeForm. This will actually forward us through the controller servlet (since it's mapped to /do/*). The controller knows where to forward to when the link is clicked based on the mapping in the struts-config file:
<action path="/setupEmployeeForm" forward="/employeeForm.jsp"/> Clicking on the link will thus forward us to the employeeForm.jsp page. (Note: Even better than using html:link page="" is using html:link forward="". Using html:link forward="someForward" you can set up the actual /do/whatever mapping that "someForward" will call in the strutsConfig file ).
Here you see the use of html:form tag. The form tag will end up displaying: <form name="employeeForm" method="post" action="/rr_lesson_1/do/insertEmployee"> The name of the form- "employeeForm" comes from the name of the formBean that we associated in the mapping for /insertEmployee in our struts-config.xml file (name="employeeForm"). All you have to worry about is using the correct action when you use the <html:form action=""/> tag and the rest is handled by Struts. Using focus="name" will
write some javascript to the page to have our form begin with focus on the name field (name in this case refers to the "name" field in the actual EmployeeForm bean). Struts makes extenstive use of form field tags like <html:text ..> In our example Struts took care of putting the EmployeeForm we defined in our struts-config.xml mapping into request scope and the html:text tags will look for the fields mentioned as property values in the EmployeeForm and will set them up as the name values for our input buttons. If there are any values associated with the fields in the form it will also display that value. If our EmployeeForm had an initial age value of "33" then <html:text property="age"/> would end up displaying in the source as <input type="text" name="age" value="33">.
LESSON II - Introduction
Just like in Lesson 1, the application you will create mimics entering an employee into a database. The user will be required to enter an employee's name, age, and department. Concepts introduced in Lesson II: Validation (in ActionForm) ErrorMessages ActionMessages Pre-poluation of form Dealing with Errors thrown in Actions html:select and html:options tag In case you are starting with this lesson and skipping Lesson I, remember this Lesson does assume you understand the concepts of Lesson I. If you have already completed Lesson I you can simply make a copy of rr_lesson_1 in your webapps directory and rename it rr_lesson_2. As you procede through this lesson simply modify any existing code so that it matches what is presented in this lesson. (Obviously you will have to add any new components as well). Of course you can always just copy and paste from this lesson or, if you prefer, you can download the entire rr_lesson_2 application: Download rr_lesson_2 application.war Begin lesson now by clicking START.
LESSON II - 1 - Setup
These Lessons assume you are using Tomcat 4.1 and have downloaded Struts 1.1-rc2 or greater. Directory structure under tomcat should look like:
webapps | rr_lesson_2 | --- WEB-INF | |--- classes | | | | |--- lib | --- net | -- reumann
Copy .tld files from struts into rr_lesson_2 application: In the struts/contrib/struts-el/lib you should find the following files which you need to add to your rr_lesson_3/WEB-INF directory. c.tld struts-bean-el.tld struts-html-el.tld struts-logic-el.tld
Copy .jar files from struts into rr_lesson_2 application: In the same struts/contrib/struts-el/lib directory, copy the following jar files to the rr_lesson_3/WEB-INF/lib directory. commons-beanutils.jar commons-collections.jar commons-digester.jar commons-logging.jar jstl.jar standard.jar struts-el.jar struts.jar
(Note we are using the tld files and jars in the contributed struts-el directory since this will force us to use the standard JSTL tags whenever possible).
<web-app> <display-name>Struts rr lesson 2</display-name> <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>application</param-name> <param-value>ApplicationResources</param-value> </init-param> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>3</param-value> </init-param> <init-param> <param-name>detail</param-name> <param-value>3</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- Action Servlet Mapping --> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>/do/*</url-pattern> </servlet-mapping> <!-- The Welcome File List --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- tag libs --> <taglib> <taglib-uri>struts/bean-el</taglib-uri>
<taglib-location>/WEB-INF/struts-bean-el.tld</taglib-location> </taglib> <taglib> <taglib-uri>struts/html-el</taglib-uri> <taglib-location>/WEB-INF/struts-html-el.tld</taglib-location> </taglib> <taglib> <taglib-uri>struts/logic-el</taglib-uri> <taglib-location>/WEB-INF/struts-logic-el.tld</taglib-location> </taglib> <taglib> <taglib-uri>jstl/c</taglib-uri> <taglib-location>/WEB-INF/c.tld</taglib-location> </taglib> </web-app>
<action path="/setUpEmployeeForm" type="net.reumann.SetUpEmployeeAction" name="employeeForm" scope="request" validate="false" > <forward name="continue" path="/employeeForm.jsp"/> </action> <action path="/insertEmployee" type="net.reumann.InsertEmployeeAction" name="employeeForm" scope="request" validate="true" input="/employeeForm.jsp" > <forward name="success" path="/confirmation.jsp"/> </action> </action-mappings> <!-- message resources --> <message-resources parameter="ApplicationResources" null="false" /> </struts-config>
Comments: First thing to notice is the addition of a global-forward. Global forwards allow your entire application to share the same forward. In our case since we defined one for "error," any time one of our actions comes up with an error we can have our mapping findForward("error") and be forwarded to an error page. If we didn't use a global forward but wanted to always forward to this error page in case of an error, then all of our action mappings would have to declare this forward. The action mapping: action path="/setupEmployeeForm" calls SetupEmployeeAction which will be used to populate some information that our employeeForm.jsp will need. The action mapping: action path="/insertEmployee" has validate="true" and also has input="/employeeForm.jsp." Setting validate equal to true will make sure the validate method that we will write in our ActionForm is called. The input attribute will also be necessary so
that if validation returns any errors our application will know what page (or possibly an action) to return to. In this case if validating returns any errors the user is returned back to the same employeeForm.jsp page.
} public ActionErrors validate( ActionMapping mapping, HttpServletRequest request ) { ActionErrors errors = new ActionErrors(); if ( getName() == null || getName().length() < 1 ) { errors.add("name",new ActionError("error.name.required")); } if ( getAge() == null || getAge().length() < 1 ) { errors.add("age",new ActionError("error.age.required")); } else { try { Integer.parseInt( getAge() ); } catch( NumberFormatException ne ) { errors.add("age",new ActionError("error.age.integer")); } } return errors; } }
The EmployeeForm contains a validate method which it inherits from ActionForm. By setting validate to true in the struts-config file we can have our form elements validated here before we get to the Action class. In the case above we are validating to make sure that the user enters a name and an age and that the age is an integer. The ActionErrors object holds a Map of all the ActionError objects that we add to it. Using some Struts tags we can then easilly display these messages nicely on a JSP page (in the case of validation errors the display is usually back on the same JSP form that the user tried to submit). Looking more closely at adding an ActionError to our ActionErrors object, if we want to add to add an error message if our name does not validate how we want we can do: new ActionError("error.name.required") Later when we create our ApplicationResources.properties you will see the line: error.name.required=Name is required. What happens is our ActionError will look up error.name.required in this ApplicationRescources.properties file and display the appropriate message when we using
error/message tags on our JSP pages. Note: Before you create validate methods in your ActionForms make sure to first check out the next Lesson. There is a much cleaner (and easier) way to handle your form validation which is described in the next lesson.
Our JSP page is going to display a list of departments that the user can select from. Create a DepartmentBean to hold the department information: id and description. Create DepartmentBean:
package net.reumann; public class DepartmentBean { private int id; private String description; public DepartmentBean() { } public DepartmentBean( int id, String description ) { this.id = id; this.description = description; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
//since we aren't dealing with the model layer, we'll mimic it here ArrayList list = new ArrayList(3); list.add( new DepartmentBean( 1, "Accounting")); list.add( new DepartmentBean( 2, "IT")); list.add( new DepartmentBean( 3, "Shipping")); return list; } //this wouldn't be in this service class, but would be in some other business class/DAO private void doInsert( EmployeeDTO employee ) throws DatabaseException { //to test an Exception thrown uncomment line below //throw new DatabaseException(); } }
Create SetUpEmployeeAction:
package net.reumann;
import org.apache.struts.action.Action; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionForm; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.Collection; public final class SetUpEmployeeAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { EmployeeService service = new EmployeeService(); Collection departments = service.getDepartments(); HttpSession session = request.getSession(); session.setAttribute( "departments", departments ); EmployeeForm employeeForm = (EmployeeForm)form; employeeForm.setDepartment("2"); return (mapping.findForward("continue")); } }
Notice we have put a Collection of DepartmentBeans into session scope and we also set the department value of our ActionForm to "2". NOTE: It is not really a good idea to hard code your forwards as String literals like I have been doing. It's better to define these as constants in some constants Interface. If ever you decide to change the names that you use for these forwards in your struts-config.xml file you then only need to change the forward definitions in one other class.
import org.apache.struts.action.*; import org.apache.commons.beanutils.BeanUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public final class InsertEmployeeAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { EmployeeService service = new EmployeeService(); EmployeeForm employeeForm = (EmployeeForm) form; EmployeeDTO employeeDTO = new EmployeeDTO(); BeanUtils.copyProperties( employeeDTO, employeeForm ); try { service.insertEmployee( employeeDTO ); ActionMessages messages = new ActionMessages(); ActionMessage message = new ActionMessage("message.employee.insert.success",employeeDTO.getName() ); messages.add( ActionMessages.GLOBAL_MESSAGE, message ); saveMessages( request, messages ); request.setAttribute("employee",employeeDTO); return (mapping.findForward("success")); } catch( DatabaseException de ) { ActionErrors errors = new ActionErrors(); ActionError error = new ActionError("error.employee.databaseException"); errors.add( ActionErrors.GLOBAL_ERROR, error ); saveErrors( request, errors ); return (mapping.findForward("error")); } } }
The InsertEmployeeAction is responsible for calling our EmployeeService class and passing it the EmployeeDTO to perform the insert. First we use BeanUtils to copy the properties from the EmployeeForm to our EmployeeDTO. Since insertEmployee() can throw a
DatabaseException we have to handle that. *** In the next lesson you will see how Exception handling is greatly improved by using declarative Exception handling, which eliminates the need of try/catch blocks and redundant error handling code. Notice we use the same type of ActionErrors set up as we did in the EmployeeForm, the only difference being we are adding the error with the key ActionErrors.GLOBAL_ERROR which is a generic way you can add error messages to display at the top of a results page. If a DatabaseException is not thrown we are assuming the insert was successful and we set up an ActionMessage and add it to an ActionMessages instance which we will use on our confirmation.jsp page.
color: black; background-color: #F1FFD2; font-size: 13pt; } #success { color: green; font-weight: bold; } #error { color: red; font-weight: bold; } #errorsHeader { color: red; font-weight: bold; font-size: 14pt; }
<logic:messagesPresent> <span id="errorsHeader"><bean:message key="errors.validation.header"/></span> <html:messages id="error"> <li><c:out value="${error}"/></li> </html:messages> <hr> </logic:messagesPresent> <html:form action="insertEmployee" focus="name"> <table> <tr> <td >Name:</td> <td><html:text property="name"/></td> </tr> <tr> <td>Age:</td> <td><html:text property="age"/></td> </tr> <tr> <td>Department:</td> <td> <html:select name="employeeForm" property="department"> <html:options collection="departments" property="id" labelProperty="description"/> </html:select> </td> </tr> </table> <html:submit><bean:message key="button.submit"/></html:submit> </html:form> </body> </html>
Few new things introduced here. First notice how we are displaying error messages (ActionError messages we created). The logic:messagesPresent tag checks to see if any ActionMessages are in scope (remember ActionErrors is a subclass of ActionMessages). The html:messages tag then will loop through all messages in scope. Setting the id="error" just gives us a handle to use for our JSTL c:out tag to display the message in the current iteration of our loop. Note: You'll see some Struts developers simply use the <html:errors/> tag to display error messages. This does work well and the tag will also look in your resources file for an
errors.header and errors.footer definition and use them as a header and footer for your error message display. You'll also have to add some HTML code such as <LI> or <BR> before all of your validation errors definitions in the resources file. Personally I'd rather have a bit more code as above and keep the formatting of HTML done by style sheets vs having HTML formating code in your resources file. The Department form field provides a Select box and we simply use the struts html:select and html:options tags to generate our options. The html:select tag will set the initial selection to whatever the department property is set to in our EmployeeForm (remember we set it to "2" in our SetUpEmployeeAction). The select tag will also populate the department property in our EmployeeForm with whatever option the user selects. html:options will loop through whatever collection we provide. In this case we had put the Collection departments into request scope in our EmployeeSetUpAction. The property="id" portion will set the value of the option tag for that iteration to the id property in the current DepartmentBean and the labelValue will set what the user sees- in this case the 'description' field in the current DepartmentBean. Just view the source code at runtime and you'll see what gets generated from this JSP based on the select and options tags. Finally, to test how the validation works try not entering a name and/or age. (Also try entering a non-number for age).
Here we will display the ActionMessages created in our InsertEmployeeAction. It works the same way as it does on the employeeForm.jsp except we added message="true" to the logic:messagesPresent portion. We need to add message="true" so that the tag will look for ActionMessages and not actionErrors. If this was going to be a generic confirmation page used for other processes, we would remove the code specific to Employee such as the title and the NAME and AGE displays. Then we could use this page to display whatever message we wanted by just setting up an appropriate ActionMessage before forwarding to this page.
This page is really simple. Here we just use the <html:messages> tag to loop through and display the error messages that are in the ActionErorrs object in scope. I didn't bother to use <logic:messagesPresent> construct since I'm not using headers or anything that could be
displayed even if an ActionError was not present. This ends Lesson II. I hope you found it helpful. Please send any comments or suggestions to [email protected].
This application mimics the business requirement of inserting and updating an employee. The flow of this example doesn't mimic what you would really have set up in real life, since in real life you'd probably choose from a list of employees to update. This example, since it doesn't do any real business logic, simply lets you update the employee you just pretended to insert. This Lesson is a lot like Lesson II in functionality except the flow is a bit different. After an insert of an Employee you are forwarded to the filled out employeeForm and you could resubmit it as an update. The confirmation page that you saw in Lesson II now is displayed only after you do an update. Download rr_lesson_3.war Begin lesson now by clicking START.
-- reumann
Add The Appropriate Struts Files: In the struts/contrib/struts-el/lib you should find the following files which you need to add to your rr_lesson_3/WEB-INF directory: c.tld struts-bean-el.tld struts-html-el.tld struts-logic-el.tld
From the same struts/contrib/struts-el/lib copy the following jar files to your rr_lesson_3/WEB-INF/lib directory: commons-beanutils.jar commons-collections.jar commons-digester.jar commons-logging.jar commons-validator.jar jstl.jar standard.jar struts-el.jar struts.jar
(Note we are using the tld files and jars in the contributed struts-el directory since this will force us to use the standard JSTL tags whenever possible). In the main struts/lib directory you'll find: validation-rules.xml commons-validator.jar Both of these files need to also be added to your rr_lesson_3/WEB-INF/lib directory. Create web.xml: Finally, we need to make sure we have a web.xml file in our rr_lesson_3/WEB-INF/
directory. The web.xml looks exactly the same as it did in the other two lessons except that we changed the display name to rr lesson 3.
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <display-name>Struts rr lesson 3</display-name> <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>application</param-name> <param-value>ApplicationResources</param-value> </init-param> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>3</param-value> </init-param> <init-param> <param-name>detail</param-name> <param-value>3</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- Action Servlet Mapping --> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>/do/*</url-pattern> </servlet-mapping> <!-- The Welcome File List -->
<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- tag libs --> <taglib> <taglib-uri>struts/bean-el</taglib-uri> <taglib-location>/WEB-INF/struts-bean-el.tld</taglib-location> </taglib> <taglib> <taglib-uri>struts/html-el</taglib-uri> <taglib-location>/WEB-INF/struts-html-el.tld</taglib-location> </taglib> <taglib> <taglib-uri>struts/logic-el</taglib-uri> <taglib-location>/WEB-INF/struts-logic-el.tld</taglib-location> </taglib> <taglib> <taglib-uri>jstl/c</taglib-uri> <taglib-location>/WEB-INF/c.tld</taglib-location> </taglib> </web-app>
private String[] flavorIDs; public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } public void setDepartment(int department) { this.department = department; } public void setFlavorIDs(String[] flavorIDs) { this.flavorIDs = flavorIDs; } public String getName() { return name; } public int getAge() { return age; } public int getDepartment() { return department; } public String[] getFlavorIDs() { return flavorIDs; } }
DepartmentBean:
package net.reumann; public class DepartmentBean { private int id; private String description; public DepartmentBean() { }
public DepartmentBean( int id, String description ) { this.id = id; this.description = description; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
FlavorBean:
package net.reumann; public class FlavorBean { private String flavorID; private String description; public FlavorBean() {} public FlavorBean( String flavorID, String description ) { this.flavorID = flavorID; this.description = description; } public void setFlavorID(String flavorID) { this.flavorID = flavorID; } public String getFlavorID() { return flavorID; } public void setDescription(String description) { this.description = description; }
DatabaseException:
package net.reumann; public class DatabaseException extends Exception { public DatabaseException() { super(); } public DatabaseException(String message) { super(message); } }
} catch( DatabaseException de ) { //log error throw de; } return employee; } public EmployeeDTO updateEmployee( EmployeeDTO employee ) throws DatabaseException { //in real life call other business classes to do update try { mimicUpdate( employee ); } catch( DatabaseException de ) { //log error throw de; } return employee; } //this wouldn't be in this service class, but would be in some other business class/DAO private void mimicInsert( EmployeeDTO employee ) throws DatabaseException { //to test an Exception thrown uncomment line below //throw new DatabaseException("Error trying to insert Employee"); } //this wouldn't be in this service class, but would be in some other business class/DAO private void mimicUpdate( EmployeeDTO employee ) throws DatabaseException { //to test an Exception thrown uncomment line below //throw new DatabaseException("Error trying to update Employee"); } public Collection getDepartments() { //call business layer to return Collection of Department beans //since we aren't dealing with the model layer, we'll mimic it here ArrayList list = new ArrayList(3); list.add( new DepartmentBean( 1, "Accounting")); list.add( new DepartmentBean( 2, "IT")); list.add( new DepartmentBean( 3, "Shipping")); return list; }
public Collection getFlavors() { //call business layer to return Collection of Flavors //since we aren't dealing with the model layer, we'll mimic it here ArrayList list = new ArrayList(3); list.add( new FlavorBean( "101", "Chocolate")); list.add( new FlavorBean( "201", "Vanilla")); list.add( new FlavorBean( "500", "Strawberry")); return list; } }
Looking at the form-bean definition above you see that we are using type org.apache.struts.validator.DynaValidatorForm. Magically Struts will make sure to create this bean for us using the form-property fields defined. Since we are actually going to capture more than one ice cream flavor that an employee likes, flavorIDs is defined as a String array. The "methodToCall" property is a property that we are going to use in our DispatchAction
class (more about that later). Remember, just like when using the standard ActionForm, you should keep these form fields set as Strings since an HTML form will only submit the request information in String format. This DynaValidatorForm is used just like a normal ActionForm when it comes to how we define it's use in our action mappings. The only 'tricky' thing is that standard getter and setters are not made since the DynaActionForms are backed by a HashMap. In order to get fields out of the DynaActionForm you would do: String age = formBean.get("age"); Similarly, if you need to define a property: formBean.set("age","33"); You'll see an example of this usage in the SetUpEmployeeAction in the next step. If you want, though, you could set initial values for the properties as you see done above for the department property. This will always set the initial value of the department id to "2."
Our first action-mapping directs us to the SetUpEmployeeAction. If you looked at Lesson 2 you will see that this mapping is almost exactly the same. The only difference is behind the
scenes where "employeeForm" is now a DynaValidatorForm instead of the basic ActionForm. Now we can create the actual SetUpEmployeeAction. Create SetUpEmloyeeAction:
package net.reumann; import org.apache.struts.action.Action; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionForm; import org.apache.struts.action.DynaActionForm; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.Collection; public final class SetUpEmployeeAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
EmployeeService service = new EmployeeService(); Collection departments = service.getDepartments(); Collection flavors = service.getFlavors(); HttpSession session = request.getSession(); session.setAttribute( "departments", departments ); session.setAttribute( "flavors", flavors ); DynaActionForm employeeForm = (DynaActionForm)form; employeeForm.set( "methodToCall", "insert" ); return (mapping.findForward("continue")); } }
This Action is almost identical to the SetUpEmployeeAction in Lessons I and II. Pay attention to the cast of the ActionForm into the type DynaActionForm and the setting of the
'methodToCall' property: DynaActionForm employeeForm = (DynaActionForm)form; employeeForm.set( "methodToCall", "insert" ); The methodToCall being set to "insert" will make more sense after the next step in our lesson. (What we are going to do is use this property to set a hidden form field to "insert" so that our DispatchAction will know to do an insert when the employeeForm.jsp is submitted). Also note that we are not setting the 'department' property here since we set up that an initial value in our DynaValidatorForm. NOTE: It's not really a good idea to hard code your forwards as String literals like I have been doing. It's better to define these as constants in some constants Interface. If ever you decide to change the names that you use for these forwards in your struts-config.xml file you then only need to change the forward definitions in one other class.
#-- messages message.employee.update.success=Successful update of Employee. message.employee.insert.success=Successfully added Employee. #-- exceptions exception.database.error=Problem occurred when performing database operation. Error Message: {0} #-- validation errors errors.required={0} is required. errors.integer={0} must be a whole number. #-- errors headers errors.validation.header=Validation Error: #-- buttons -button.submit=SUBMIT button.update=UPDATE
Notice the exception.database.error declaration. This is the key we provided in our declarative exception for DatabaseException in the struts-config.xml file. The actual error message itself will be passed in to the {0} argument. The next step will explain our validation framework. Take a look at the two validation error declarations above. You'll see how those fit in after reviewing the next step.
Setting validate="true" for an action-mapping to employeeForm will execute validation based on the fields declared in the validation.xml file. The validation above declares that we are going to validate "name" and "age." The possible values for the 'depends' attributes are those that are delcared in the validation-rules.xml. (Of course you can add your own plugins to the framework as well, but most of the basic validations are already defined for you). In our example we are defining both 'name' and 'age' with the 'required' value, and for age we are also providing 'integer.' Upon submission of the employeeForm both fields will be checked that they are not null or left blank and age will also be checked to be sure it is a valid number. One thing you have to make sure of is that you provide the necessary key/value pairs in your ApplicationResources file for the validation errors that could occur. If you go back to step 7 you see we defined: errors.required={0} is required. errors.integer={0} must be a whole number. These are global errors that we could use for all of our validations. The key that we provide for arg0 in the validation.xml file definitions will substitute the proper value in place of {0} if an error occurs. (In the ApplicationResources file you will see name.displayname and age.displayname defined and those values will be substituted for {0} in the appropriate validation).
</logic:messagesPresent> <logic:messagesPresent message="true"> <html:messages id="message" message="true"> <span id="success"><c:out value="${message}"/></span><br> </html:messages> </logic:messagesPresent> <html:form action="employeeAction" focus="name"> <table> <tr> <td >Name:</td> <td><html:text property="name"/></td> </tr> <tr> <td>Age:</td> <td><html:text property="age"/></td> </tr> <tr> <td>Department:</td> <td> <html:select name="employeeForm" property="department"> <html:options collection="departments" property="id" labelProperty="description"/> </html:select> </td> </tr> <tr> <td>Favorite ice cream flavors:</td> <td> <c:forEach var="flavor" items="${flavors}"> <html:multibox name="employeeForm" property="flavorIDs"> <c:out value="${flavor.flavorID}"/> </html:multibox> <c:out value="${flavor.description}"/> </c:forEach> </td> </tr> </table> <html:hidden name="employeeForm" property="methodToCall"/>
<c:choose> <c:when test="${employeeForm.map.methodToCall == 'insert'}"> <html:submit><bean:message key="button.submit"/></html:submit> </c:when> <c:otherwise> <html:submit><bean:message key="button.update"/></html:submit> </c:otherwise> </c:choose> </html:form> </body> </html>
You might be wondering why there are two different versions of the logic:messagesPresent tag blocks. The reason is that one set is used to display ActionErrors which, in this example could be on the page when validation errors are present, and the other other set is used to display normal ActionMessages which, in this case, would be a successful insert message. By adding message="true" to the messages tags we make sure that only ActionMessages are displayed. (Leaving out the message="true" means only ActionErrors will be displayed if present).
index.jsp:
<%@ taglib uri="struts/bean-el" prefix="bean" %> <%@ taglib uri="struts/html-el" prefix="html" %> <html>
<head> <link href="<html:rewrite page="/rr.css" />" rel="stylesheet" type="text/css"> <title><bean:message key="title.employeeApp"/></title> </head> <body> <h1><bean:message key="title.employeeApp"/></h1> <br> <html:link page="/do/setUpEmployeeForm">Add An Employee</html:link> </body> </html>
confirmation.jsp:
<%@ taglib uri="struts/bean-el" prefix="bean" %> <%@ taglib uri="struts/html-el" prefix="html" %> <%@ taglib uri="struts/logic-el" prefix="logic" %> <%@ taglib uri="jstl/c" prefix="c" %> <jsp:useBean id="employee" scope="request" class="net.reumann.EmployeeDTO"/> <html> <head> <title><bean:message key="title.employee.confirmation"/></title> <link href="<html:rewrite page="/rr.css" />" rel="stylesheet" type="text/css"> </head> <body> <h1><bean:message key="title.employee.confirmation"/></h1> <br> <logic:messagesPresent message="true"> <html:messages id="message" message="true"> <span id="success"><c:out value="${message}"/></span><br> </html:messages> </logic:messagesPresent> </body> </html>
error.jsp:
<%@ taglib uri="struts/bean-el" prefix="bean" %> <%@ taglib uri="struts/html-el" prefix="html" %> <%@ taglib uri="jstl/c" prefix="c" %> <html> <head>
<link href="<html:rewrite page="/rr.css" />" rel="stylesheet" type="text/css"> <title><bean:message key="title.error"/></title> </head> <body> <h1><bean:message key="title.error"/></h1> <html:messages id="error"> <span id="error"><c:out value="${error}" escapeXml="false"/></span><br> </html:messages> </body> </html>
This lesson assumes you are using Tomcat 4.1 and that you understand the basics of Struts. Download the rr_lesson_ibatis.zip archive and unzip inside your {tomcat}/webapps directory. rr_lesson_ibatis.zip The application requires a Database to connect to. You need to create and populate the three small tables listed in tables_script.sql. The script will work as is for postgres. Tweak accordingly for your db set up. Make sure you also place your driver in tomcat/common/lib (If you don't have a database set up you could of course just view the source code without running the application). There is also a log4j.properties file in src and classes which you can configure to your liking. Just add the correct path to where you want the log file to be built. The Ant build file will also work if you change the path to where the servlet.jar is located in the build file. Obviously you only need this if you want to tinker around and recompile the code.
<name>driverClassName</name> <value>org.postgresql.Driver</value> </parameter> <parameter> <name>username</name> <value>userNameForDB</value> </parameter> <parameter> <name>factory</name> <value>org.apache.commons.dbcp.BasicDataSourceFactory</value> </parameter> <parameter> <name>maxIdle</name> <value>1</value> </parameter> <parameter> <name>maxActive</name> <value>3</value> </parameter> <parameter> <name>maxWait</name> <value>5000</value> </parameter> <parameter> <name>removeAbandoned</name> <value>true</value> </parameter> <parameter> <name>removeAbandonedTimeout</name> <value>20</value> </parameter> </ResourceParams> </Context> If you change the name of this DataSource to something other than lessonIbatisDatasource make sure to change it also in the resource-ref definition of this lesson's web.xml file: <resource-ref> <res-ref-name>lessonIbatisDatasource</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>
Calling it something more meaningful is a good idea if you are going to have to need multiple config files. Below is the sql-map-config file: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE sql-map-config PUBLIC "-//iBATIS.com//DTD SQL Map Config 1.0//EN" "http://www.ibatis.com/dtd/sql-map-config.dtd"> <sql-map-config> <properties resource="net/reumann/conf/database.properties" /> <datasource name="lessonIbatisDatasource" factory-class="com.ibatis.db.sqlmap.datasource.JndiDataSourceFactory" default="true" > <property name="DBInitialContext" value= "java:comp/env" /> <property name="DBLookup" value="${DatabaseJNDIPath}"/> </datasource> <sql-map resource="net/reumann/sql/EmployeeSQL.xml" /> <sql-map resource="net/reumann/sql/LabelValueSQL.xml" /> </sql-map-config> First, notice the declaration of a properties file. Using a properties file is a good idea since if you change from production to development. You could have different properties files versus having to have separate sql-config files. In this demo we are using the ${DatabaseJNDIPath} key which is defined in the net/reumann/conf/database.properties file. You can also check out the example in the iBATIS docs to see how to set up a Datasource directly in this configuration file instead of setting it up in the container's configuration file. In that example you'll see the benefits using property files to define such things as the driver name, connection URL, etc. Since we set up our DataSource in Tomcat's server.xml file there are not too many things to substitute using a properties file. There are also two declarations of sql-maps: <sql-map resource="net/reumann/sql/EmployeeSQL.xml" /> <sql-map resource="net/reumann/sql/LabelValueSQL.xml" /> These xml files are the ones where our actual SQL statements will be declared. We will examine EmployeeSQL.xml next.
The main SQL map used in this application is EmployeeSQL and is listed below with some comments following the listing: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE sql-map PUBLIC "-//iBATIS.com//DTD SQL Map 1.0//EN" "http://www.ibatis.com/dtd/sql-map.dtd"> <sql-map name="EmployeeSQL"> <cache-model name="employee_cache" reference-type="WEAK"> <flush-interval hours="24"/> <flush-on-execute statement= "insertEmployee" /> <flush-on-execute statement= "updateEmployee" /> </cache-model> <result-map name="employee_result" class="net.reumann.Employee"> <property name="id" column="emp_id"/> <property name="name" column="name"/> <property name="deptId" column="dept_id"/> <property name="deptName" column="dept_name"/> <property name="location" column="location_id" mappedstatement="getLocationById"/> </result-map> <mapped-statement name="insertEmployee"> INSERT INTO employee ( id, name, dept_id, location_id ) VALUES ( #id#, #name#, #deptId#, #locationId# ) </mapped-statement> <mapped-statement name="updateEmployee"> UPDATE employee SET name = #name#, dept_id = #deptId#, location_id = #locationId# WHERE id = #id# </mapped-statement> <mapped-statement name="getLocationById" result-class="net.reumann.Location"> SELECT id, name FROM location where id = #value# </mapped-statement> <dynamic-mapped-statement name="selectEmployeesFromSearch" resultmap="employee_result" cache-model="employee_cache"> SELECT employee.id AS emp_id, employee.name AS name, employee.dept_id AS dept_id, department.name AS dept_name, employee.location_id AS location_id, location.name AS location_name FROM employee, department, location WHERE employee.dept_id = department.id AND
employee.location_id = location.id <dynamic> <isNotEmpty prepend=" AND " property="id"> employee.id = #id# </isNotEmpty> <isNotEmpty prepend=" AND " property="name"> employee.name = #name# </isNotEmpty> <isNotEmpty prepend=" AND " property="deptId"> employee.dept_id = #deptId# </isNotEmpty> <isNotEmpty prepend=" AND " property="locationId"> employee.location_id = #locationId# </isNotEmpty> </dynamic> </dynamic-mapped-statement> </sql-map> COMMENTS: The first thing you'll notice is the set up of a cache model: <cache-model name="employee_cache" reference-type="WEAK"> <flush-interval hours="24"/> <flush-on-execute statement= "insertEmployee" /> <flush-on-execute statement= "updateEmployee" /> </cache-model> Read the iBATIS docs concerning the different cache types. For the majority of the SQL maps "WEAK" will do just fine. Remember iBATIS takes care of caching results for you so you don't have to worry about managing that. Notice that when the insertEmployee or updateEmployee mapped-statement is called the cache is cleared. For this example I just chose an arbitrary 24 hours to also flush the cache. Take a look at the first line of our dynamic-mapped-statement: <dynamic-mapped-statement name="selectEmployeesFromSearch" resultmap="employee_result" cache-model="employee_cache"> Later, in our code when we access iBATIS with the SQL name "selectEmployeesFromSearch" the mapped SQL query above is performed and each row that is returned will populate the object defined in our "result-map" and this Object is added to a List. Using iBATIS, you don't have to worry about looping through a ResultSet and creating your beans. All of this tedious work is taken care of by iBATIS. Notice the result-map definition "employee_result" maps net.reumann.Employee property names with column names: <result-map name="employee_result" class="net.reumann.Employee"> <property name="id" column="emp_id"/> <property name="name" column="name"/> <property name="deptId" column="dept_id"/> <property name="deptName" column="dept_name"/>
<property name="location" column="location_id" mappedstatement="getLocationById"/> </result-map> Each property name in your result-map should match the name of the corresponding property in the net.reumann.Employee bean. If we look at the Employee bean you will also see that it has a property called net.reumann.Location location. Notice in our employee_result resultmap definition we populate this Location bean (which is nested inside of the Employee bean) by calling another mapped-statement: 'getLocationById'. Looking at the Location object, you can see it is a very simple bean and in real life I wouldn't actually make a separate query to another mapped-statement to populate such a small object since it's possible the List of Employee beans being returned could be quite large, and thus each row would have to make a separate call to the DB to populate Location. However, calling other mapped-statements like this comes in EXTREMELY handy when you have to build a complex bean from a bunch of other beans. For example, maybe I want to build a "Company" bean and I might make one call to a mapped-statement to build the Employee List for this Company object and another call to a mapped-statement to build a "FinancialRecord" bean inside of Company. This might look like... <result-map name="company_result" class="net.reumann.Company"> <property name="id" column="company_id"/> <property name="name" column="name"/> <property name="address" column="address"/> <property name="employees" column="company_id" mappedstatement="getEmployees"/> <property name="financeRecord" column="company_id" mappedstatement="getFinanacialRecord"/> </result-map> You are not required to populate a result-map. If your column names match up to the properties of your bean you could use a result-class, as is the case in the above "getLocationsById" mapping. This is usually very easy to do since you could always rename the columns you get back in your query using an AS construct (ie. first_name AS firstName ). If you want, you could also just return a HashMap as the Object and not even bother declaring a bean object to map to. With a Struts application I usually prefer to populate beans since I think it makes displaying the bean properties on JSPs cleaner than grabbing the properties from a Map ( beanName.someProperty seems easier to understand than mapName.value.keyName). More importantly I find it easier and more intuitive to look at bean fields that I'll need to use in my JSP versus having to go to the SQL file to see what the column names were called. It's great, though, to have the flexibility to just use HashMaps if you feel like it. Most of our mapped statements will be passed either a Map of properties/values or an Object with standard get/set methods. The # # fields you see in the mapped statements (ie #name#, #id#) need to match the same properties in the bean you pass in (get methods have to match) or they must match the name of the key if passing in a Map. You'll see how these mappedstatements are called shortly. (Instead of using Beans and Maps, you also could pass in and return a primitive wrapper type which is useful when your query only needs one parameter or you only need one column
back. Examples of all this in the iBATIS docs). You should also read the docs to get a feel for how the dynamic-mapped statements work. In the dynamic-mapped statement example above we will be searching for Employees but some of the criteria may or may not be there from our search, so the SQL WHERE clause is built dynamically based on whether the Map we pass in has values for the different keys. (For example if deptId is null or an empty String, the SQL will be built without creating a where for the deptId). These comments only briefly touch on the SQL map configurations. I repeat: read the iBATIS documenation
<result-map name="label_value_result" class="net.reumann.LabelValue"> <property name="value" column="id" /> <property name="label" column="name" /> </result-map> <mapped-statement name="selectAllDepartments" result-map="label_value_result" cache-model="label_value_cache"> SELECT id, name FROM department ORDER BY name </mapped-statement> <mapped-statement name="selectAllLocations" result-map="label_value_result" cachemodel="label_value_cache"> SELECT id, name FROM location ORDER BY name </mapped-statement> </sql-map>
make sure you end up with a simple bean with the correct types converted from the ActionForm String properties that were entered by the user. Wrapping this plain bean inside of an ActionForm object is one common way. Another common approach is to use the BeanUtils/PropertyUtils copyProperties() method to build your bean from the ActionFrom properties. The DAO used in this example is not complicated either.. Our EmployeeDAO is basically only responsible for making sure it calls the BaseDAO with the proper SQL string. For example, the EmployeeDAO insert method looks like: public int insertEmployee(Object parameterObject) throws DaoException { return super.update("insertEmployee", parameterObject); } which can be called from our Action with: employeeDAOinstance.insertEmployee(employeeForm.getMap()); If you are looking at the actual application code you'll notice that I forgot to mention the EmployeeService class. I could have just skipped using the Service class in this demo since it doesn't do much but act as a layer between your Action class and your call to the DAO. I like to use them, though, since sometimes there are things I need to do besides just a simple DAO operation which I find works well being done in a Service class. For most simple CRUD (create, retrieve, update, delete) operations you could skip using the service class if you so desire. The work of doing the insert then gets handed off to the BaseDAO, which has three simple reusable methods: public List getList(String statementName, Object parameterObject) public Object getObject(String statementName, Object parameterObject) public int update(String statementName, Object parameterObject) The update method is responsible for doing any 'executeUpdate' JDBC calls so our insert is handed off to this method. The update method in the BaseDAO looks like: public int update(String statementName, Object parameterObject) throws DaoException { int result = 0; try { sqlMap.startTransaction(); result = sqlMap.executeUpdate(statementName, parameterObject); sqlMap.commitTransaction(); } catch (SQLException e) { try { sqlMap.rollbackTransaction(); } catch (SQLException ex) { throw new DaoException(ex.fillInStackTrace()); } throw new DaoException(e.fillInStackTrace()); } return result;
} There is a static block in the BaseDAO which sets up the sqlMap instance that the dao uses. Notice how the BaseDAO static block grabs our config file: reader = Resources.getResourceAsReader("net/reumann/conf/sql-map-config.xml"); Once the BaseDao is built your application could reuse it and you just have to take care of creating a few simple DAOs that call the super class BaseDAO, handing it the correct SQL string to use and your parameter object and you're all set. The great thing about this framework is that it is simple, powerful, and flexible and improves the ease in which your code can be maintained. A Special thanks to Clinton Begin for creating iBATIS and for his suggestions for this lesson.