At line 6 changed 1 line. |
This tutorial will show you how to create a Struts Action, a JUnit Test (using [StrutsTestCase|http://strutstestcase.sourceforge.net/]), and a JSP for the form. The Action we create will talk to the PersonManager we created in the [Creating Managers|CreateManager] tutorial. This tutorial will simplify everything - we will not actually be rendering any data or making the UI look pretty. The [next tutorial|ConfiguringTiles] will show you how to integrate your new JSP into your webapp. |
This tutorial will show you how to create a Struts Action, a JUnit Test (using [StrutsTestCase|http://strutstestcase.sourceforge.net/]), and a JSP for the form. The Action we create will talk to the PersonManager we created in the [Creating Managers|CreateManager] tutorial. |
At line 8 changed 1 line. |
By default, AppFuse ships with [Struts|http://struts.apache.org] as its web framework. As of 1.6, you can use [Spring|http://www.springframework.org] or [WebWork|http://opensymphony.org/webwork] as your web framework. Tapestry and JSF are planned for 1.7 and 1.8 respectively. |
By default, AppFuse ships with [Struts|http://struts.apache.org] as its web framework. As of 1.6+, you can use [Spring|http://www.springframework.org] or [WebWork|http://opensymphony.org/webwork] as your web framework. In 1.7, support was added for using [JSF|http://myfaces.apache.org] or [Tapestry|http://jakarta.apache.org/tapestry]. |
At line 10 changed 1 line. |
To install Spring MVC, navigate to {{extras/spring}}, and view the README.txt. For WebWork, see {{extras/webwork/README.txt}}. You can easily install these options in your project by running "ant install-springmvc" or "ant install-webwork". This tutorial using these two options can be found at: |
To install any of these web frameworks instead of Struts, simply navigate to the ''extras'' directory and into the directory of the framework you want to install. The README.txt file in this directory has further instructions. The tutorials for these other frameworks are listed below. |
At line 12 changed 2 lines. |
* <span style="color: green">__Spring MVC:__</span> [Creating Controllers and JSPs|SpringControllers] |
* <span style="color: blue">__WebWork:__</span> [Creating Actions and JSPs|WebWorkActions] |
* __<span style="color: green">Spring:</span>__ [Creating Spring Controllers and JSPs|SpringControllers] |
* __<span style="color: blue">WebWork:</span>__ [Creating WebWork Actions and JSPs|WebWorkActions] |
* __<span style="color: purple">JSF:</span>__ [Creating JSF Beans and JSPs|JSFBeans] |
* __<span style="color: orange">Tapestry:</span>__ [Creating Tapestry Pages and Templates|TapestryPages] |
At line 24 changed 1 line. |
* [5] Display the JSP in a browser and run the ActionTest |
* [5] Run PersonActionTest |
* [6] Clean up the JSP to make it presentable |
* [7] Create Canoo WebTests to test browser-like actions |
At line 27 changed 1 line. |
Now let's generate our PersonForm object for Struts and our web tier. To do this, we need to add XDoclet tags to the Person.java Object to create our Struts ActionForm. In the JavaDoc for the Person.java file, add the following @struts.form tags (use User.java if you need an example): |
Now let's generate our PersonForm object for Struts and our web tier. To do this, we need to add XDoclet tags to the Person.java Object to create our Struts ActionForm. In the JavaDoc for the Person.java file, add the following @struts.form tag (use User.java if you need an example): |
At line 42 added 2 lines. |
Now if you run __ant gen-forms__, Ant (and XDoclet) will generate a PersonForm.java for you in build/web/gen/**/form. |
|
At line 39 changed 1 line. |
In this step, we'll generate a ''skeleton'' or our JSP for displaying information from the PersonForm. I say ''skeleton'' because it'll just be the <form> itself. It will contain table rows and Struts' <html:text> tags for each property in PersonForm.java. The tool that we use to do this was written by [Erik Hatcher|http://www.blogscene.org/erik/]. It's basically just a single class (FormTagsHandler.java) and a couple of XDoclet templates (FormKeys.xdt and StrutsForm_jsp.xdt). All these files are located in extras/viewgen. |
In this step, you'll generate a JSP page to display information from the Person object. It will contain Struts' JSP tags that render table rows for each property in Person.java. The [AppGen] tool that's used to do this is based off a StrutsGen tool - which was originally written by [Erik Hatcher|http://www.blogscene.org/erik/]. It's basically just a couple of classes and a bunch of XDoclet templates. All these files are located in extras/appgen. |
At line 43 changed 8 lines. |
* Execute __ant compile__ - this generates the PersonForm.java from the Person.java POJO. |
|
* From the command-line, navigate to "extras/viewgen" |
* Execute __ant -Dform.name=PersonForm__ to generate three files in extras/viewgen/build: |
** PersonForm.properties (labels for your form elements) |
** personForm.jsp (skeleton JSP file for viewing a single Person) |
** PersonFormList.jsp (skeleton JSP file for viewing a list of People) |
* Copy the contents of PersonForm.properties into web/WEB-INF/classes/ApplicationResources_en.properties. Here is an example of what you might add to ApplicationResources_en.properties: |
* From the command-line, navigate to "extras/appgen" |
* Execute __ant -Dobject.name=Person -Dappgen.type=pojo -Dapp.module=__ to generate a bunch of files in extras/appgen/build/gen. In fact, it'll generate all the files you need to complete this tutorial. However, let's just grab the ones you need. |
** web/WEB-INF/classes/Person.properties (labels for your form elements) |
** web/pages/personForm.jsp (JSP file for viewing a single Person) |
** web/pages/personList.jsp (JSP file for viewing a list of People) |
* Copy the contents of Person.properties into web/WEB-INF/classes/ApplicationResources.properties. These are all the keys you will need for titles/headings and form properties. Here is an example of what you should add to ApplicationResources.properties: |
At line 54 removed 1 line. |
personForm.firstName=First Name |
At line 59 added 1 line. |
personForm.firstName=First Name |
At line 57 removed 3 lines. |
}}} |
</div> |
* Copy personForm.jsp to web/pages/personForm.jsp. Copy PersonFormList.jsp to web/pages/personList.jsp. ''Notice that each of the new filename's first character is lowercase.'' |
At line 61 changed 3 lines. |
%%note __NOTE:__ If you want to customize the CSS for a particular page, you can add <body id="pageName"/> to the top of the file. This will be slurped up by SiteMesh and put into the final page. You can then customize your CSS on a page-by-page basis using something like the following: |
{{{body#pageName element.class { background-color: blue } }}} |
%% |
person.added=Person has been added successfully. |
person.updated=Person has been updated successfully. |
person.deleted=Person has been deleted successfully. |
At line 65 changed 2 lines. |
* Add keys in ApplicationResources_en.properties the titles and headings in the JSPs |
In the generated JSPs, there are two keys for the title (top of the browser window) and the header (heading in the page). We now need to add these two keys (personDetail.title and personDetail.heading) to ApplicationResources_en.properties. |
# -- person list page -- |
personList.title=Person List |
personList.heading=Persons |
At line 68 removed 3 lines. |
Open web/WEB-INF/classes/ApplicationResources_en.properties and add the following to the bottom of the file: |
|
{{{ |
At line 74 added 2 lines. |
</div> |
* Copy personForm.jsp to web/pages/personForm.jsp. Copy personList.jsp to web/pages/personList.jsp. |
At line 76 changed 1 line. |
%%__NOTE:__ Just above, we added "personForm.*" keys to this file, so why do I use personForm ''and'' personDetail? The best reason is because it gives a nice separation between form labels and text on the page. Another reason is because all the *Form.* give you a nice representation of all the fields in your database. I recently had a client who wanted all fields in the database searchable. This was fairly easy to do. I just looked up all the keys in ApplicationResources.properties which contained "Form." and then put them into a drop-down. On the UI, the user was able to enter a search term and select the column they wanted to search. I was glad I followed this Form vs. Detail distinction on that project!%% |
;: ''The files in the "pages" directory will end up in "WEB-INF/pages" at deployment time. The container provides security for all files below WEB-INF. This applies to client requests, but not to forwards from Struts' ActionServlet. Placing all JSPs below WEB-INF ensures they are only accessed through Actions, and not directly by the client or each other. This allows security to be moved up into the Action, where it can be handled more efficiently, and out of the base presentation layer.'' |
At line 79 added 1 line. |
The web application security for AppFuse specifies that all *.html url-patterns should be protected (except for /signup.html and /passwordHint.html). This guarantees that clients must go through an Action to get to a JSP (or at least the ones in ''pages''). |
At line 79 changed 2 lines. |
!!Create a new ActionTest to test our Action [#3] |
To create a StrutsTestCase Test for PersonAction, start by creating a PersonActionTest.java file in the test/web/**/action directory. |
%%note __NOTE:__ If you want to customize the CSS for a particular page, you can add <body id="pageName"/> to the top of the file. This will be slurped up by SiteMesh and put into the final page. You can then customize your CSS on a page-by-page basis using something like the following: |
{{{body#pageName element.class { background-color: blue } }}}%% |
At line 82 changed 1 line. |
;:%%(color: blue)''As usual, copy → save as an existing ActionTest (i.e. UserActionTest). Replace [[Uu]ser with [[P]erson. You might want to make sure [Cactus|http://jakarta.apache.org/cactus] (StrutsTestCase is an extension of Cactus] tests are running before you copy an existing one. Run __ant test-cactus -Dtestcase=UserAction__ to verify the UserAction works. Stop Tomcat before you do this. |
* Add keys in ApplicationResources.properties the titles and headings in the JSPs |
In the generated JSPs, there are two keys for the title (top of the browser window) and the header (heading in the page). These fields are provided above with key names of personDetail.title and personDetail.heading. |
At line 84 changed 1 line. |
If you did copy UserActionTest, make sure and change ''UserFormEx'' to ''PersonForm''. The reason for ''UserFormEx'' is to support a String[] setter for Roles. Since the UserForm is generated, it's not very feasible to do it in the User.java object.''%% |
''Just above, we added "personForm.*" keys to this file, so why do I use personDetail instead of personForm for the titles and headings? The best reason is because it gives a nice separation between form labels and text on the page. Another reason is because all the *Form.* give you a nice representation of all the fields in your database. |
At line 86 changed 1 line. |
[{Java2HtmlPlugin |
I recently had a client who wanted all fields in the database searchable. This was fairly easy to do. I just looked up all the keys in ApplicationResources.properties which contained "Form." and then put them into a drop-down. On the UI, the user was able to enter a search term and select the column they wanted to search. I was glad I followed this Form vs. Detail distinction on that project!'' |
At line 91 added 6 lines. |
|
!!Create PersonActionTest to test PersonAction [#3] |
To create a StrutsTestCase Test for PersonAction, start by creating a PersonActionTest.java file in the test/web/**/action directory: |
|
[{Java2HtmlPlugin |
|
At line 99 added 3 lines. |
import org.appfuse.Constants; |
import org.appfuse.webapp.form.PersonForm; |
|
At line 98 changed 1 line. |
addRequestParameter("action", "Edit"); |
addRequestParameter("method", "Edit"); |
At line 109 changed 1 line. |
addRequestParameter("action", "Edit"); |
addRequestParameter("method", "Edit"); |
At line 119 changed 1 line. |
addRequestParameter("action", "Save"); |
addRequestParameter("method", "Save"); |
At line 133 changed 1 line. |
addRequestParameter("action", "Delete"); |
addRequestParameter("method", "Delete"); |
At line 152 added 1 line. |
} |
At line 156 changed 1 line. |
Now we have to create an Action (a.k.a. the Controller) to talk to our Manager and retrieve/save our data. In src/web/**/action, create a PersonAction.java file with the following contents: |
In src/web/**/action, create a PersonAction.java file with the following contents: |
At line 158 changed 1 line. |
[{Java2HtmlPlugin |
[{Java2HtmlPlugin |
At line 181 added 2 lines. |
import org.apache.struts.action.ActionMessage; |
import org.apache.struts.action.ActionMessages; |
At line 184 added 3 lines. |
import org.appfuse.model.Person; |
import org.appfuse.service.PersonManager; |
import org.appfuse.webapp.form.PersonForm; |
At line 172 changed 1 line. |
* validate="false" parameter="action" input="mainMenu" |
* validate="false" parameter="method" input="mainMenu" |
At line 175 changed 2 lines. |
|
public ActionForward cancel(ActionMapping mapping, ActionForm form, |
|
public ActionForward cancel(ActionMapping mapping, ActionForm form, |
At line 179 changed 1 line. |
throws Exception { |
throws Exception { |
At line 186 changed 1 line. |
throws Exception { |
throws Exception { |
At line 199 changed 3 lines. |
new ActionMessage("person.deleted", |
personForm.getFirstName() + " " + |
personForm.getLastName())); |
new ActionMessage("person.deleted")); |
At line 203 changed 1 line. |
saveMessages(request, messages); |
// save messages in session, so they'll survive the redirect |
saveMessages(request.getSession(), messages); |
At line 211 changed 1 line. |
throws Exception { |
throws Exception { |
At line 223 changed 1 line. |
request.setAttribute(Constants.PERSON_KEY, convert(person)); |
personForm = (PersonForm) convert(person); |
updateFormBean(mapping, request, personForm); |
At line 232 changed 1 line. |
throws Exception { |
throws Exception { |
At line 247 changed 1 line. |
mgr.savePerson(convert(personForm)); |
Person person = (Person) convert(personForm); |
mgr.savePerson(person); |
At line 252 changed 4 lines. |
new ActionMessage("person.added", |
personForm.getFirstName() + " " + |
personForm.getLastName())); |
request.getSession().setAttribute(Globals.MESSAGE_KEY, messages); |
new ActionMessage("person.added")); |
At line 273 added 3 lines. |
// save messages in session to survive a redirect |
saveMessages(request.getSession(), messages); |
|
At line 260 changed 3 lines. |
new ActionMessage("person.updated", |
personForm.getFirstName() + " " + |
personForm.getLastName())); |
new ActionMessage("person.updated")); |
At line 276 changed 1 line. |
;:''If you are running Eclipse, you might have to "refresh" the project in order to see PersonForm. It lives in build/web/gen, which should be one of your project's source folders. This is the only way for Eclipse to see and import PersonForm, since it is generated by XDoclet and does not live in your regular source tree. If you are not running Eclipse, you will need to manually add "import org.appfuse.webapp.form.PersonForm;" to PersonAction.java and PersonActionTest.java for the generated PersonForm class to be resolved. You will find it in build/web/gen/org/appfuse/webapp/form/PersonForm.java.'' |
;:''If you are running Eclipse, you might have to "refresh" the project in order to see PersonForm. It lives in build/web/gen, which should be one of your project's source folders. This is the only way for Eclipse to see and import PersonForm, since it is generated by XDoclet and does not live in your regular source tree. You can find it at build/web/gen/org/appfuse/webapp/form/PersonForm.java. |
At line 295 added 2 lines. |
;:You can also configure Eclipse to auto-refresh your workspace using: Window > Preferences > General > Workspace > Refresh Automatically.'' |
|
At line 280 changed 1 line. |
Now we need to add the ''edit'' forward and the ''savePerson'' action-mapping, both with are specified in in our PersonActionTest. To do this, we'll add a couple more XDoclet tags to the top of the PersonAction.java file. Do this right above the class declaration. You should already have the XDoclet tag for the ''editPerson'' action-mapping, but I'm showing it here so you can see all the XDoclet tags at the top of this class. |
Now you need to add the ''edit'' forward and the ''savePerson'' action-mapping, both which are specified in in the PersonActionTest. To do this, add a couple more XDoclet tags to the top of the PersonAction.java file. Do this right above the class declaration. You should already have the XDoclet tag for the ''editPerson'' action-mapping, but I'm showing it here so you can see all the XDoclet tags at the top of this class. |
At line 286 changed 1 line. |
* validate="false" parameter="action" input="mainMenu" |
* validate="false" parameter="method" input="mainMenu" |
* |
At line 288 changed 1 line. |
* validate="true" parameter="action" input="edit" |
* validate="true" parameter="method" input="edit" |
At line 290 changed 1 line. |
* @struts.action-forward name="edit" path=".personDetail" |
* @struts.action-forward name="edit" path="/WEB-INF/pages/personForm.jsp" |
At line 297 changed 1 line. |
There are a few keys (ActionMessages) that we need to add to ApplicationResources_en.properties to display the success messages. This file is located in ''web/WEB-INF/classes'' - open it and add the following: |
You might notice that the code you're using to call the PersonManager is the same as the code used in the PersonManagerTest. Both PersonAction and PersonManagerTest are ''clients'' of PersonManagerImpl, so this makes perfect sense. |
At line 299 changed 1 line. |
;:''I usually add these under the {{# -- success messages --}} comment.'' |
Everything is almost done for this tutorial, let's get to running the tests! |
At line 301 changed 4 lines. |
{{{person.added=Information for <strong>{0}</strong> has been added successfully. |
person.deleted=Information for <strong>{0}</strong> has been deleted successfully. |
person.updated=Information for <strong>{0}</strong> has been updated successfully. |
}}} |
!!Run PersonActionTest [#5] |
At line 306 changed 1 line. |
You could use generic ''added'', ''deleted'' and ''updated'' messages, whatever works for you. It's nice to have separate messages in case these need to change on a per-entity basis. |
If you look at our PersonActionTest, all the tests depend on having a record with id=1 in the database (and testRemove depends on id=2), so add that to our sample data file (metadata/sql/sample-data.xml). I'd add it at the bottom - order is not important since it (currently) does not relate to any other tables. |
At line 308 removed 8 lines. |
You might notice that the code we're using to call the PersonManager is the same as the code we used in our PersonManagerTest. Both PersonAction and PersonManagerTest are ''clients'' of PersonManagerImpl, so this makes perfect sense. |
|
Everything is almost done for this tutorial, let's get to running our tests! |
|
!!Run PersonActionTest [#6] |
|
If you look at our PersonActionTest, all our tests depend on having a record with id=1 in the database (and testRemove depends on id=2), so let's add that to our sample data file (metadata/sql/sample-data.xml). I'd just add it at the bottom - order is not important since it (currently) does not relate to any other tables. |
|
At line 334 changed 1 line. |
DBUnit loads this file before we run any of our tests, so this record will be available to our Action test. |
DBUnit loads this file before we run any of our tests, so this record will be available to the PersonActionTest. |
At line 336 changed 1 line. |
Now if you run __ant test-cactus -Dtestcase=PersonAction__ - everything should work as planned. Make sure Tomcat isn't running before you try this. |
Now if you run __ant test-web -Dtestcase=PersonAction__ - everything should work as planned. Make sure Tomcat isn't running before you try this. |
At line 341 changed 2 lines. |
!!Clean up the JSP to make it presentable [#7] |
First, let's clean up our personForm.jsp by making the "id" property a hidden field. Remove the following code block: |
!!Clean up the JSP to make it presentable [#6] |
At line 344 changed 1 line. |
[{Java2HtmlPlugin |
Now let's clean up the generated personForm.jsp. Change the ''action'' of the <html:form> to be "savePerson" so validation will be turned on when saving. Also, change the ''focus'' attribute from focus="" to focus="firstName" so the cursor will be in the firstName field when the page loads (this is done with JavaScript). |
At line 346 changed 9 lines. |
<tr> |
<th> |
<appfuse:label key="personForm.id"/> |
</th> |
<td> |
<html:text property="id" styleId="id"/> |
</td> |
</tr> |
}] |
Another thing you will need to do is comment out the following lines at the bottom of the personForm.jsp. This is because the Validator will throw an exception if a formName is specified and no validation rules exist for it. |
At line 356 changed 1 line. |
And add the following before the <table> tag: |
;:''Personally, I think this is [a bug|http://nagoya.apache.org/bugzilla/show_bug.cgi?id=27316], but the Struts Committers disagreed.'' |
At line 358 changed 1 line. |
[{Java2HtmlPlugin |
{{{<html:javascript formName="personForm" cdata="false" |
dynamicJavascript="true" staticJavascript="false"/> |
<script type="text/javascript" |
src="<html:rewrite page="/scripts/validator.jsp"/>"></script>}}} |
At line 360 changed 2 lines. |
<html:hidden property="id"/> |
}] |
Now if you execute __ant db-load deploy__, start Tomcat and point your browser to [http://localhost:8080/appfuse/editPerson.html?id=1], you should see something like this: |
At line 363 removed 4 lines. |
You should probably also change the ''action'' of the <html:form> to be "savePerson" so validation will be turned on when saving. Also, change the ''focus'' attribute from focus="" to focus="firstName" so the cursor will be in the firstName field when the page loads (this is done with JavaScript). |
|
Now if you execute __ant db-load deploy-web__, start Tomcat and point your browser to [http://localhost:8080/appfuse/editPerson.html?id=1], you should see something like this: |
|
At line 371 removed 1 line. |
Finally, to make this page more user friendly, you may want to add a message for your users at the top of the form, but this can easily be done by adding text (using <fmt:message>) at the top of the personForm.jsp page. |
At line 370 added 4 lines. |
%%note __NOTE:__ Use the __deploy-web__ target if you've changed any files under the ''web'' directory. Otherwise, use __deploy__ which compiles and deploys.%% |
|
Finally, to make this page more user friendly, you may want to add a message for your users at the top of the form, which can easily be done by adding text (using <fmt:message>) at the top of the personForm.jsp page. |
|
At line 382 changed 1 line. |
* Delete - [http://localhost:8080/appfuse/editPerson.html?action=Delete&id=1] (or edit and click on the Delete button). |
* Delete - [http://localhost:8080/appfuse/editPerson.html?method=Delete&id=1] (or edit and click on the Delete button). |
At line 427 added 1 line. |
<verifytext description="verify success message" text="${person.updated}"/> |
At line 445 changed 2 lines. |
<verifytext description="verify success message" |
text="Information for <strong>Abbie Raible</strong> has been added successfully."/> |
<verifytext description="verify success message" text="${person.added}"/> |
At line 460 added 1 line. |
<prepareDialogResponse description="Confirm delete" dialogType="confirm" response="true"/> |
At line 462 added 1 line. |
<verifyNoDialogResponses/> |
At line 461 changed 2 lines. |
<verifytext description="verify success message" |
text="Information for <strong>Matt Canoo</strong> has been deleted successfully."/> |
<verifytext description="verify success message" text="${person.deleted}"/> |
At line 482 changed 1 line. |
''Next Up:'' __Part V:__ [Adding Validation and List Screen|ValidationAndList] - Adding validation logic to the personForm so that firstName and lastName are required fields and adding a list screen to display all person records in the database. |
''Next Up:'' __Part IV:__ [Adding Validation and List Screen|ValidationAndList] - Adding validation logic to the personForm so that firstName and lastName are required fields and adding a list screen to display all person records in the database. |