At line 1 added 482 lines. |
__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. |
|
;:''This tutorial depends on __Part IV:__ [Configuring Tiles and Action CRUD methods|ConfiguringTiles].'' |
|
!!About this Tutorial |
This tutorial will show you how to add Validation logic (client and server-side) to the personForm object using Struts' Validator. We'll also create a list screen using the [Display Tag Library|http://displaytag.sf.net] to display all the people in the database. |
|
;:%%(color: blue)''I will tell you how I do stuff in the __Real World__ in text like this.''%% |
|
!Table of Contents |
* [1] Add XDoclet tags (@struts.validator) to Person.java |
* [2] View JSP with validation added and test |
* [3] Add ''testGetPersons'' methods to DAO and Manager Tests |
* [4] Add ''getPersons'' methods to DAO and Manager Interfaces and Implementation classes |
* [5] Add ''testSearch'' methods to Action Test |
* [6] Add ''search '' method to Action |
* [7] Create personList.jsp and Canoo test |
* [8] Add link to menu |
|
!!Add XDoclet tags (@struts.validator) to Person.java [#1] |
To use the Validator with Struts, normally you have to write a validation.xml file by hand. If you're not using AppFuse, you also have to configure the Validator Plugin and error keys in your ApplicationResources.properties. For more information on this, see the [Validation Made Easy Tutorial|http://www.reumann.net/do/struts/lesson3/step8]. |
|
Using XDoclet, it's much easier - we just need to add a couple of ''@struts.validator'' tags to our POJO (Person.java). So open it up (src/ejb/**/persistence/Person.java) and modify your setFirstName and setLastName methods to resemble the following: |
|
[{Java2HtmlPlugin |
|
/** |
* @return Returns the firstName. |
* @struts.validator type="required" |
* @hibernate.property column="first_name" |
*/ |
public String getFirstName() { |
return this.firstName; |
} |
|
/** |
* @return Returns the lastName. |
* @struts.validator type="required" |
* @hibernate.property column="last_name" |
*/ |
public String getLastName() { |
return this.lastName; |
} |
}] |
|
I should mention that you can also add a ''msgkey'' attribute to this tag to override the default message key for this error. |
|
[{Java2HtmlPlugin |
|
@struts.validator type="required" msgkey="errors.required" |
}] |
|
The default key for type="required" is already ''errors.required'', so I usually leave it to the default. This key is defined in web/WEB-INF/classes/ApplicationResources.properties. You'll notice that we put these tags on the ''getters'' of this class even though the [XDoclet documentation|http://xdoclet.sourceforge.net/tags/apache-tags.html#@struts.validator%20(0..*)] says to put them on the setters. This is because we are generating our PersonForm.java - the template file (metadata/template/struts_form.xdt) takes care of putting these tags onto the setters in the generated file. |
|
Now if you save Person.java and run __ant webdoclet__, a validation.xml file will be generated in build/appfuse/WEB-INF/. It's contents should have now have an entry for "personForm". |
|
[{Java2HtmlPlugin |
|
<form name="personForm"> |
<field property="firstName" |
depends="required"> |
|
<arg0 key="personForm.firstName"/> |
</field> |
<field property="lastName" |
depends="required"> |
|
<arg0 key="personForm.lastName"/> |
</field> |
</form> |
}] |
|
To enable validation in our personForm.jsp, you'll need to make sure your personForm.jsp has the following at the bottom: |
|
{{{<html:javascript formName="personForm" cdata="false" |
dynamicJavascript="true" staticJavascript="false"/> |
<script type="text/javascript" |
src="<html:rewrite page="/scripts/validator.jsp"/>"></script>}}} |
|
!!View JSP with validation added and test [#2] |
|
Now that we have Validation configured for this form, whenever this form is used in an action-mapping with validate="true", these rules will be applied. In the [last tutorial|ConfiguringTiles], we added the "savePerson" action-mapping for PersonAction. The XDoclet tags for this action-mapping were: |
|
[{Java2HtmlPlugin |
|
* @struts.action name="personForm" path="/savePerson" scope="request" |
* validate="true" parameter="action" input="editPerson.do?action=Edit" |
}] |
|
So now, as long as your web/pages/personForm.jsp has <html:form action="savePerson">, validation should kick in when we try to save this form. Run __ant db-load deploy__, start Tomcat and go to [http://localhost:8080/appfuse/editPerson.do?id=1]. |
|
If you erase the values in the firstName and lastName fields and click the save button, you should get the following JavaScript alert. |
|
%%(margin: 0 auto; height: 128px; width: 318px) |
[validation-required.png] |
%% |
|
To make sure things are ''really'' working as expected, you can turn off JavaScript and ensure the server-side validation is working. This is easy in [Mozilla Firebird|http://www.mozilla.org/products/firebird/] (my favorite browser), just go to Tools → Options → Web Features and uncheck "Enable JavaScript". Now if you clear the fields and save the form, you should see the following: |
|
%%(border: 1px solid black; margin: 0 auto; height: 202px; width: 423px) |
[validation-required-nojs.png] |
%% |
|
If you don't see these validation errors, there are a couple possibilities: |
|
* The form saves with a success message, but the firstName and lastName fields are now blank. |
;:''This is because the <html:form> in web/pages/personForm.jsp has action="editPerson" - make sure it has __action="savePerson"__.'' |
* You click save, but a blank page appears. |
;:''The blank page indicates that the "input" attribute of you "savePerson" forward is incorrectly configured. Make sure it relates to a local or global action-forward. In this example, it should be __input="edit"__, which points to the .personDetail tile's definition. From my experience, the input's value must be a forward, not a path to an action.'' |
|
If you only want server-side validation (no JavaScript), you can remove the ''onsubmit'' attribute of <html:form> (in web/pages/personForm.jsp) as well as the Validator JavaScript tags at the bottom of the page. |
|
[{Java2HtmlPlugin |
|
<html:javascript formName="personForm" cdata="false" |
dynamicJavascript="true" staticJavascript="false"/> |
<script type="text/javascript" |
src="<html:rewrite page="/scripts/validator.jsp"/>"></script> |
}] |
|
!!Add testGetPersons methods to DAO and Manager Tests [#3] |
To create a List screen (also called a master screen), we need to create methods that will return all the rows from our ''person'' table. Let's start by adding tests for these methods to our PersonDaoTest and PersonManagerTest classes. I usually name this method ''getEntities'' (i.e. getPersons), but you could also use ''getAll'' or ''search'' - it's really just a matter of personal preference. |
|
Open test/dao/**/persistence/PersonDaoTest.java and add a ''testGetPersons'' method: |
|
[{Java2HtmlPlugin |
|
public void testGetPersons() throws Exception { |
person = new Person(); |
List results = dao.getPersons(person); |
assertTrue(results.size() > 0); |
} |
}] |
|
The reason I'm passing in a person object to the ''getPersons'' method is to allow for filtering (based on values in person) in the future. |
|
Now open test/service/**/service/PersonManagerTest.java and add a ''testGetPersons'' method: |
|
[{Java2HtmlPlugin |
|
public void testGetPersons() throws Exception { |
personForm = new PersonForm(); |
List results = mgr.getPersons(personForm); |
assertTrue(results.size() > 0); |
} |
}] |
|
Neither of these classes will compile at this point since the ''getPersons'' method does not exist on our Interfaces (PersonDao and PersonManager), not on our Implementations (PersonDaoHibernate and PersonManagerImpl). Let's giddyup and add those suckers. |
|
!!Add getPersons methods to DAO and Manager Interfaces and Implementation classes [#4] |
First, let's modify our interfaces to include a ''getPersons'' method. Open src/dao/**/persistence/PersonDao.java and add the following: |
|
;:''You will need to import java.util.List in each of these interfaces. In Eclipse, Ctrl+Shift+O is the fastest way.'' |
|
[{Java2HtmlPlugin |
|
public List getPersons(Person person) throws DAOException; |
}] |
|
Then, open src/service/**/service/PersonManager.java and add a similar method: |
|
[{Java2HtmlPlugin |
|
public List getPersons(Object person) throws Exception; |
}] |
|
Now we need to add the implementation (a.k.a. "the meat") for these methods into our DAO and Manager implementations. Open src/dao/**/persistence/PersonDaoHibernate.java and add the following method: |
|
;:%%(color: blue)''I looked in UserDAOHibernate.java and used the getUsers method as a template for getPersons.'' |
|
[{Java2HtmlPlugin |
|
/** |
* @see org.appfuse.persistence.PersonDao#getPersons(org.appfuse.model.Person) |
*/ |
public List getPersons(Person person) throws DAOException { |
return getHibernateTemplate().find("from Person p order by upper(p.firstName)"); |
} |
}] |
|
You'll notice here that we're doing nothing with the ''person'' parameter. This is just a placeholder for now - in the future you may want to filter on it's properties using [Hibernate's Query Language|http://www.hibernate.org/hib_docs/reference/html/query-language.html] (HQL) or using [Criteria Queries|http://www.hibernate.org/hib_docs/reference/html/query-criteria.html]. |
|
Also, add a ''getPersons'' method to src/service/**/service/PersonManagerImpl.java: |
|
[{Java2HtmlPlugin |
|
/** |
* @see org.appfuse.webapp.service.PersonManager#getPersons(java.lang.Object) |
*/ |
public List getPersons(Object obj) throws Exception { |
return dao.getPersons((Person) person); |
} |
}] |
|
Save all your files and make sure everything compiles with __ant clean package-web__. Now you should be able to run both tests by running the following: |
|
* __ant test-dao -Dtestcase=PersonDao__ |
* __ant test-service -Dtestcase=PersonManager__ |
|
If everything works - ''nice job!'' Now we need to add this ''retrieve all'' functionality to the web tier. |
|
!!Add testSearch method to Action Test [#5] |
Open test/web/**/action/PersonActionTest.java and add the following method: |
|
;:%%(color: blue)''I copied the testSearch method from UserActionTest.java and changed the "User" stuff to "Person".'' |
|
[{Java2HtmlPlugin |
|
public void testSearch() throws Exception { |
setRequestPathInfo("/editPerson"); |
addRequestParameter("action", "Search"); |
actionPerform(); |
|
verifyForward("list"); |
|
|
assertTrue(getRequest().getAttribute(Constants.PERSON_LIST) != null); |
verifyNoActionErrors(); |
} |
}] |
|
This class will not compile until you add the PERSON_LIST variable to the src/dao/**/Constants.java file. |
|
;:%%(color: blue)''I usually copy a similar variable that already exists in this file - i.e. USER_LIST.'' |
|
[{Java2HtmlPlugin |
|
/** |
* The request scope attribute that holds the person list |
*/ |
public static final String PERSON_LIST = "personList"; |
}] |
|
Now save all your changes. You won't be able to run __ant test-cactus -Dtestcase=PersonAction__ yet since ''PersonAction.search()'' does not exist (yet). Let's add it. |
|
!!Add search method to Action [#6] |
Open src/web/**/action/PersonAction.java and add the following XDoclet tag at the top - to forward to our list screen. |
|
[{Java2HtmlPlugin |
|
* @struts.action-forward name="list" path=".personList" |
}] |
|
Now add the search method to the body of the PersonAction class. |
|
;:%%(color: blue)''I used UserAction.search() as a template for this method.'' |
|
[{Java2HtmlPlugin |
|
public ActionForward search(ActionMapping mapping, ActionForm form, |
HttpServletRequest request, |
HttpServletResponse response) |
throws Exception { |
if (log.isDebugEnabled()) { |
log.debug("Entering 'search' method"); |
} |
|
PersonForm personForm = (PersonForm) form; |
|
// Exceptions are caught by ActionExceptionHandler |
PersonManager mgr = (PersonManager) getBean("personManager"); |
List persons = mgr.getPersons(convert(personForm)); |
request.setAttribute(Constants.PERSON_LIST, persons); |
|
// return a forward to the person list definition |
return mapping.findForward("list"); |
} |
}] |
|
Now if you run __ant test-cactus -Dtestcase=PersonAction__, you will get an error that the .personList definition does not exist. Or, at least that is what the following error is trying to say: |
|
{{{ |
Testcase: testSearch(org.appfuse.webapp.action.PersonActionTest): FAILED |
was expecting '/appfuse/.personList' but received '/appfuse.personList' |
}}} |
|
!!Create personList.jsp and Canoo test [#7] |
Let's create the JSP to hold our list and a Tile's definition for it. The easiest way to do this is to use the StrutsGen Tool to generate a basic list screen for us. To do this from the command-line, navigate to tools/strutsgen and run __ant -Dform.name=PersonForm__. This will generate a PersonFormList.jsp in tools/strutsgen/build. |
|
Copy PersonFormList.jsp to web/pages/personList.jsp and open it for editing. |
|
At the top of the file is a <bean:struts> tags that exposes the edit screen's forward as a page-scoped variable. This should already have a value of "editPerson". |
|
[{Java2HtmlPlugin |
|
<%-- For linking to edit screen --%> |
<bean:struts id="editURL" forward="editPerson"/> |
}] |
|
Now we need to add this to the metadata/web/global-forwards.xml, as well as one for viewing the list. This way, they will get included in our struts-config.xml file. |
|
[{Java2HtmlPlugin |
|
<forward name="editPerson" path="/editPerson.do"/> |
<forward name="viewPeople" path="/editPerson.do?action=Search"/> |
}] |
|
;:''You'll notice that I'm hardcoding the "action=''$button.name''" into the forwards. This kindof defeats the purpose of i18n and will cause other languages to fail when trying to use these forwards. A possible solution would be to use the Actions and the keys from ApplicationResources.properties to compose the URLs. Since I haven't used AppFuse to develop an application that required any languages other than English, I haven't been inclined to fix this. Solutions to this problem are encouraged!'' |
|
In the first <button> you find in personList.jsp, we need to populate another forward - this time to the Add screen. Make sure your button's onclick event matches the following: |
|
[{Java2HtmlPlugin |
|
onclick="location.href='<html:rewrite forward="editPerson"/>'"> |
}] |
|
The template we used to create this JSP has the column for the id property hard-coded, so XDoclet adds it twice. We need to remove this from personList.jsp - so delete the following from this file: |
|
[{Java2HtmlPlugin |
|
<display:column property="id" sort="true" |
headerClass="sortable" |
titleKey="personForm.id"/> |
}] |
|
;:''If anyone knows of a way to modify the tools/strutsgen/src/DisplayTagList_jsp.xdt to not include this column tag, please let me know.'' |
|
Another thing you'll probably want to change is the plural form of the items you're listing. The generated name in this example is "persons" and it should probably be people. At or near line 31, you should have the following line: |
|
{{{<display:setProperty name="paging.banner.items_name" value="persons"/>}}} |
|
Change it to: |
|
{{{<display:setProperty name="paging.banner.items_name" value="people"/>}}} |
|
Now we need to add a new definition to tiles-config.xml for this list screen. Open web/WEB-INF/tiles-config.xml and add the following XML: |
|
;:%%(color: blue)''I usually copy an existing list definition - i.e. .userList.'' |
|
[{Java2HtmlPlugin |
|
<!-- Person List definition --> |
<definition name=".personList" extends=".mainMenu"> |
<put name="titleKey" value="personList.title" /> |
<put name="headingKey" value="personList.heading" /> |
<put name="content" value="/WEB-INF/pages/personList.jsp"/> |
</definition> |
}] |
|
Finally, we need to add these keys (personList.title and personList.heading) to web/WEB-INF/classes/ApplicationResources.properties. Open this file and add the following: |
|
{{{ |
# -- person list page -- |
personList.title=Person List |
personList.heading=All Persons |
}}} |
|
As a reminder, the {{personList.title}} is what ends up in the brower's title bar (the <title> tag) and |
{{personList.heading}} will be put into an <h1> tag before any page content. At this point, your PersonActionTest should succeed. Save all your changes, navigate to the basedir of your project and try it by executing __ant test-cactus -Dtestcase=PersonAction__. |
|
%%(color:green) |
''__Sweet!__''\\ |
BUILD SUCCESSFUL\\ |
Total time: 1 minute 0 seconds |
%% |
|
At this point, you should be able to view this page in your browser at [http://localhost:8080/appfuse/editPerson.do?action=Search]. |
|
Now that we have a List Screen, let's change the pages that are displayed after adding and deleting a new Person. In src/web/**/PersonAction.java, change the ''mapping.findForward("mainMenu")'' in the ''save'', ''delete'' and ''cancel'' methods to be: |
|
[{Java2HtmlPlugin |
|
return mapping.findForward("viewPeople"); |
}] |
|
You will also need to change ''verifyForward("list")'' to be ''verifyForward("viewPeople")'' in the testRemove method of test/web/**/PersonActionTest.java. Lastly, the Canoo tests "AddPerson" and "DeletePerson" need to be updated. Open test/web/web-tests.xml and change the following line in the "AddPerson" target: |
|
{{{<verifytitle description="Main Menu appears if save successful" |
text=".*${mainMenu.title}.*" regex="true"/>}}} |
|
to: |
|
{{{<verifytitle description="Person List appears if save successful" |
text=".*${personList.title}.*" regex="true"/>}}} |
|
Then in the "DeletePerson" target, change the following line: |
|
{{{<verifytitle description="display Main Menu" |
text=".*$(mainMenu.title}.*" regex="true"/> |
}}} |
|
to: |
|
{{{<verifytitle description="display Person List" |
text=".*$(personList.title}.*" regex="true"/>}}} |
|
We use "viewPeople" instead of "list" so that the search method will be executed, rather than simply forwarding to the .personList definition (which the "list" forward points to). |
|
To test that displaying this page works, we can create a new JSP test in test/web/web-tests.xml: |
|
[{Java2HtmlPlugin |
|
<!-- Verify the people list screen displays without errors --> |
<target name="SearchPeople" |
description="Tests search for and displaying all people"> |
<webtest name="searchPeople"> |
&config; |
<steps> |
&login; |
<invoke description="click View People link" |
url="/editPerson.do?action=Search"/> |
<verifytitle description="we should see the personList title" |
text=".*${personList.title}.*" regex="true"/> |
</steps> |
</webtest> |
</target> |
|
}] |
|
You'll also want to add the "SearchPeople" target to the "PersonTests" target so it will be executed along with all the other person-related tests. |
|
[{Java2HtmlPlugin |
|
<!-- runs person-related tests --> |
<target name="PersonTests" |
depends="SearchPeople,EditPerson,SavePerson,AddPerson,DeletePerson" |
description="Call and executes all person test cases (targets)"> |
<echo>Successfully ran all Person JSP tests!</echo> |
</target> |
}] |
|
Now you can run __ant test-canoo -Dtestcase=SearchPeople__ (or ''ant test-jsp'' if Tomcat isn't running) and hopefully it will result in "BUILD SUCCESSFUL". If so - ''nice work!'' |
|
!!Add link to menu [#8] |
The last step is to make the list, add, edit and delete functions visible to the user. The simplest way is to add a new link to the list of links in web/pages/mainMenu.jsp: |
|
[{Java2HtmlPlugin |
|
<li> |
<html:link forward="viewPeople"> |
<fmt:message key="menu.viewPeople"/> |
</html:link> |
</li> |
}] |
|
Where ''menu.viewPeople'' is an entry in web/WEB-INF/classes/ApplicationResources.properties. |
{{{ |
menu.viewPeople=View People |
}}} |
|
The other (more likely) alternative is that you'll want to add it to the menu. To do this, add the following to web/WEB-INF/menu-config.xml: |
|
[{Java2HtmlPlugin |
|
<Menu name="PeopleMenu" title="menu.viewPeople" forward="viewPeople"/> |
}] |
|
Make sure the above XML is inside the <Menus> tag, but not within another <Menu>. Then add this new menu to web/common/menu.jsp - which should now looks as follows: |
|
[{Java2HtmlPlugin |
|
<%@ include file="/common/taglibs.jsp"%> |
|
<div id="menu"> |
<menu:useMenuDisplayer name="ListMenu" |
permissions="rolesAdapter"> |
<menu:displayMenu name="AdminMenu"/> |
<menu:displayMenu name="UserMenu"/> |
<menu:displayMenu name="PeopleMenu"/> |
<menu:displayMenu name="FileUpload"/> |
<menu:displayMenu name="FlushCache"/> |
<menu:displayMenu name="Clickstream"/> |
</menu:useMenuDisplayer> |
</div> |
}] |
|
Now if you run __ant clean deploy__ and start Tomcat, you should see something like the screenshot below. |
|
%%(border: 1px solid black; margin: 0px auto; width: 551px; height: 184px) |
[new-menu-item.png] |
%% |
|
Notice that there is a new link on the left side (from mainMenu.jsp) and on the right in our menu (from menu.jsp). |
|
!!That's it! |
You've completed the full lifecycle of developing a set of master-detail pages with AppFuse - __congratulations__. Now the real test is if you can run all the tests in your app without failure. To test, stop tomcat and run __ant clean test-all__. This will run all the unit tests within your project. As a reminder, it should be easy to setup and test AppFuse from scratch using __ant setup-db setup-tomcat test-all__. Also, if you're looking for more robust examples - checkout [StrutsResume]. |
|
''__Happy Day!__'' |
%%(color: green) |
BUILD SUCCESSFUL\\ |
Total time: 2 minutes 31 seconds |
%% |