At line 18 added 2 lines. |
* [7] Show an Entry's Category, and allow modification of it |
* [8] Add the ability to add and delete entries |
At line 20 changed 1 line. |
In order for the Struts version of AppFuse to allow editing of a Weblog's attributes from the User object, you need to modify User.java to support indexed properties. You can do this by adding ''indexedProperties="true"'' to the @struts.form tag in User.java. |
In order for the Struts version of AppFuse to allow editing of a Weblog's attributes from the User object, you need to modify __User.java__ to support indexed properties. You can do this by adding ''indexedProperties="true"'' to the @struts.form tag in User.java. |
At line 24 changed 1 line. |
* @struts.form include-all="true" extends="BaseForm" indexedProperties="true"}] |
@struts.form include-all="true" extends="BaseForm" indexedProperties="true"}] |
At line 26 changed 1 line. |
A weblog record has 3 fields: weblogId, blogTitle and dateCreated. The blogTitle is the only field that users should be able to edit. The rest of them can be set programmatically. Open ''web/pages/userForm.jsp'' and add the following code to the bottom of the form (between </table> and </form>. |
%%note __NOTE:__ While you're at it, you might as well add this same line to __Weblog.java__. You can also add it to __Entry.java__ and __Category.java__, but you don't need ''indexedProperties="true"''.%% |
At line 30 added 2 lines. |
A weblog record has 3 fields: weblogId, blogTitle and dateCreated. The blogTitle is the only field that users should be able to edit. The rest of them can be set programmatically. Open __web/pages/userForm.jsp__ and add the following code to the bottom of the form (between </table> and </form>. |
|
At line 33 removed 1 line. |
<input type="hidden" name="weblogs[<c:out value='${index.index}'/>].username" value="<c:out value='${blog.username}'/>"/> |
At line 37 removed 1 line. |
|
At line 50 changed 1 line. |
<img src="http://appfuse.org/appfuse/images/iconWarning.gif" class="icon" alt="Warning"> |
<img src="http://demo.appfuse.org/appfuse/images/iconWarning.gif" class="icon" alt="Warning"> |
At line 52 changed 1 line. |
<img src="http://appfuse.org/appfuse/images/iconWarning.gif" class="icon" alt="Warning"> |
<img src="http://demo.appfuse.org/appfuse/images/iconWarning.gif" class="icon" alt="Warning"> |
At line 54 changed 1 line. |
<img src="http://appfuse.org/appfuse/images/iconWarning.gif" class="icon" alt="Warning"> |
<img src="http://demo.appfuse.org/appfuse/images/iconWarning.gif" class="icon" alt="Warning"> |
At line 62 changed 1 line. |
To fix saving a User, add ''convertLists(user)'' to the save() method of __UserAction.java__: |
To fix saving a User, add ''convertLists(user)'' to the save() method of __UserAction.java__ (in src/web/**/webapp/action): |
At line 72 changed 1 line. |
In addition, you can add ''convertLists(user)'' to ''UserAction.edit()'' and get rid of the <fmt:formatDate> tag around the ''dateCreated'' property. |
In addition, you can add ''convertLists(user)'' to ''UserAction.edit()'': |
At line 76 added 7 lines. |
[{Java2HtmlPlugin |
|
userForm.setConfirmPassword(userForm.getPassword()); |
convertLists(userForm); |
updateFormBean(mapping, request, userForm); |
}] |
|
At line 85 added 8 lines. |
This will allow you to get rid of the <fmt:formatDate> tag around the ''dateCreated'' property. |
|
{{{ |
<input type="hidden" name="weblogs[<c:out value='${index.index}'/>].dateCreated" value="<c:out value='${blog.dateCreated}'/>"/> |
}}} |
|
Run __ant deploy__, wait for Tomcat to reload your application, and then try saving the User Profile again. This time it should succeed - and you can also change the blog title if you so choose. |
|
At line 94 added 18 lines. |
|
In order to edit a Weblog object, and it's children (Users and Entries), you need to create a __WeblogAction.java__ class. Before you do that, you'll need to add a couple constants to ''src/dao/**/__Constants.java__'': |
|
[{Java2HtmlPlugin |
|
/** |
* The request scope attribute that holds the weblog form. |
*/ |
public static final String WEBLOG_KEY = "weblogForm"; |
|
/** |
* The request scope attribute that holds the weblog list |
*/ |
public static final String WEBLOG_LIST = "weblogList"; |
}] |
|
Then [download WeblogAction.java|WeblogAction.java] and put it in your ''src/web/**/webapp/action'' directory. This class already has the ''convertLists(Object)'' methods that you added to __UserAction.java__. |
|
At line 113 added 70 lines. |
|
Add the following i18n keys to ''web/WEB-INF/classes/__ApplicationResources.properties__''. You'll need these for the master/detail screen when editing weblogs. |
|
{{{ |
# -- weblog form -- |
weblogForm.weblogId=Weblog Id |
weblogForm.blogTitle=Blog Title |
weblogForm.dateCreated=Date Created |
weblogForm.entries=Entries |
weblogForm.users=Users |
|
weblog.added=Weblog has been added successfully. |
weblog.updated=Weblog has been updated successfully. |
weblog.deleted=Weblog has been deleted successfully. |
|
# -- weblog list page -- |
weblogList.title=Weblog List |
weblogList.heading=Weblogs |
|
# -- weblog detail page -- |
weblogDetail.title=Weblog Detail |
weblogDetail.heading=Weblog Information |
}}} |
|
Create [weblogList.jsp] and [weblogForm.jsp] files in ''web/pages''. |
|
Add a "WeblogMenu" to ''web/WEB-INF/menu-config.xml'': |
|
{{{<Menu name="WeblogMenu" title="weblogList.title" page="/weblogs.html"/>}}} |
|
Add this menu to ''web/pages/__menu.jsp__'': |
|
{{{ |
... |
<menu:displayMenu name="WeblogMenu"/> |
</menu:useMenuDisplayer> |
}}} |
|
There is an issue with Struts where the client-side validation blows up if you have <html:javascript> tags in your page and no validation rules defined. To prevent this from happening, add at least validation rule to __Weblog.java__. For example, that ''blogTitle'' is a required field. |
|
[{Java2HtmlPlugin |
|
/** |
* @hibernate.property column="blog_title" not-null="true" |
* @struts.validator type="required" |
*/ |
public String getBlogTitle() { |
return blogTitle; |
} |
}] |
|
At this point, you should be able to run __ant deploy reload__ and navigate to the "Weblog List" (from the menu). You should also be able to perform CRUD on a weblog object. |
|
%%note __NOTE:__ If you get a "invalid LOC header (bad signature)" error, you'll need to stop/start Tomcat to get rid of it. To prevent it from happening in the future, you'll need to patch [build.xml|https://appfuse.dev.java.net/source/browse/appfuse/build.xml?r1=1.101&r2=1.102] and [metadata/web/web-settings.xml|https://appfuse.dev.java.net/source/browse/appfuse/metadata/web/web-settings.xml?r1=1.13&r2=1.14] ([APF-123|http://issues.appfuse.org/browse/APF-123]).%% |
|
%%(margin: 0 auto; width: 688px; margin-top: 10px) |
[WeblogCRUD.png] |
%% |
|
One issue you might run into when adding a new Weblog is that a ''created date'' is not set. To fix this, add the following in __WeblogAction.java__ (in the ''save()'' method): |
|
[{Java2HtmlPlugin |
|
if (isNew) { |
weblog.setDateCreated(new Date()); |
} |
|
mgr.saveObject(weblog); |
}] |
|
At line 184 added 90 lines. |
|
In order to view and edit the Users associated with a Weblog, you need to add some code to __weblogForm.jsp__ that will allow you to do this. After the "dateCreated" row, add the following: |
|
[{Java2HtmlPlugin |
|
<tr> |
<th class="tallCell"><fmt:message key="weblogForm.users"/>:</th> |
<td> |
<nested:iterate property="users" id="user" indexId="index"> |
<nested:hidden property="password"/> |
<nested:hidden property="confirmPassword"/> |
<nested:text property="username" readonly="true"/> |
<nested:text property="firstName"/> <nested:text property="lastName"/> |
<nested:hidden property="addressForm.city"/> |
<nested:hidden property="addressForm.province"/> |
<nested:hidden property="addressForm.country"/> |
<nested:hidden property="addressForm.postalCode"/> |
<nested:hidden property="passwordHint"/> |
<nested:hidden property="website"/> |
<nested:hidden property="email"/> |
<nested:hidden property="version"/> |
<nested:hidden property="enabled"/> |
Roles: |
<c:forEach var="role" items="${user.roles}" varStatus="status"> |
<c:out value="${role.name}"/><c:if test="${!status.last}">,</c:if> |
<input type="hidden" name="users[<%=index%>].userRoles" value="<c:out value="${role.name}"/>" /> |
</c:forEach> |
<br /> |
</nested:iterate> |
</td> |
</tr> |
}] |
|
The WeblogForm is request-scoped, so that's why you have to put all the attributes of user as hidden fields in the page. Other options include making the form session-scoped, as well as re-fetching the object in your Action before saving it. All of these approaches have issues: |
|
* Request-scoped: Everything has to be in the page as editable or hidden fields. If you leave a field out, it'll get set to null when you save the page. There may also be security implications with using hidden fields - so don't use this approach if you have sensitive data. |
* Session-scoped: You have to clean up the session after successfully saving the form. |
* Re-fetching before save: You have to figure out which fields have changed and merge the two objects together. |
|
The ''request-scoped'' method is used in this example because it's one of the easiest to understand. This particular "editing users in a weblog form" probably wouldn't be used in the real world (rather you'd have a <select multiple="multiple"%gt;). However, it does show you how to edit a many-to-many on both ends. |
|
In __UserAction.java__, there is logic to grab the usersRoles, fetch the Role objects from the database, and set them on the User object. You need to replicate this functionality in WeblogAction. Add the following just after the ''convertLists()'' call in __WeblogAction__.''save()'': |
|
[{Java2HtmlPlugin |
|
convertLists(weblog); |
|
// loop through and make sure all the roles are saved on User |
if (weblog.getUsers() != null) { |
for (int i=0; i < weblog.getUsers().size(); i++) { |
User user = (User) weblog.getUsers().get(i); |
String[] userRoles = request.getParameterValues("users[" + i + "].userRoles"); |
|
for (int j = 0; userRoles != null && j < userRoles.length; j++) { |
String roleName = userRoles[j]; |
user.addRole((Role)mgr.getObject(Role.class, roleName)); |
} |
} |
} |
}] |
|
Run __ant deploy db-load__ and navigate to the Weblog with an [id of 2|http://localhost:8080/appfuse/editWeblog.html?weblogId=2]. This weblog should have one user assigned to it - the ''mraible'' user. To add an additional user, run the following against your database: |
|
{{{insert into weblog_user values ('tomcat', 2);}}} |
|
If you're using < AppFuse 1.9, You may notice that the ''disabled'' fields are highlighted with a border when you click on those fields - even though they aren't editable. To fix this, [apply this patch|https://appfuse.dev.java.net/source/browse/appfuse/web/scripts/global.js?r1=1.12&r2=1.13]. Also, note the use of ''class="tallCell'' on the <th> that holds the ''Users:'' caption. This is used to put the caption at the top of the cell. After inserting the ''tomcat'' user and refreshing the page - your screen should resemble the image below. |
|
%%(border: 1px solid black; margin: 0 auto; height: 215px; width: 712px) |
[WeblogWithUsers.png] |
%% |
|
You should be able to modify the first and last name of the listed users without any issues. |
|
;:''You may notice that clicking "Refresh" on your browser after saving a Weblog results in an error. To fix this, you can use the redirect-after-post pattern. In the __WeblogAction__.''save()'' method, change:'' |
|
[{Java2HtmlPlugin |
|
return mapping.findForward("edit"); |
}] |
|
To: |
|
[{Java2HtmlPlugin |
|
return new ActionForward("/editWeblog.html?weblogId=" + weblog.getWeblogId(), true); |
}] |
|
|
%%note __NOTE:__ In Struts 1.2.7, you can use the [ActionRedirect|http://struts.apache.org/api/org/apache/struts/action/ActionRedirect.html] to implement post-and-redirect a bit easier. However, Struts 1.2.7 has [some issues|http://raibledesigns.com/page/rd?anchor=failed_upgrade_to_struts_1], so AppFuse still uses 1.2.4 at the time of this writing.%% |
|
At line 275 added 193 lines. |
|
To edit a Weblog's entries on the same weblog form, you need to add some more code, just after the ''Users'' row. |
|
[{Java2HtmlPlugin |
|
<tr> |
<th class="tallCell"><fmt:message key="weblogForm.entries"/>:</th> |
<td> |
<nested:iterate property="entries" id="entry" indexId="index"> |
<div id="entry<%=index%>"> |
<nested:hidden property="entryId"/> |
<nested:hidden property="weblogId"/> |
<nested:hidden property="categoryId"/> |
<nested:hidden property="timeCreated"/> |
<nested:textarea property="text" style="width: 400px; height: 100px"/> |
<br /> |
Posted at: <nested:write property="timeCreated"/></span> |
</div> |
</nested:iterate> |
</td> |
</tr> |
}] |
|
Run __ant deploy db-load__ and view the [same weblog|http://localhost:8080/appfuse/editWeblog.html?weblogId=2] again. You should see a screen like the one below: |
|
%%(width: 690px; height: 316px; border: 1px solid black; margin: 10px auto 0 auto) |
[WeblogWithEntry.png] |
%% |
|
If you try to clicking the "Save" button, you'll get a nice and descriptive <span style="background: #ffd">javax.servlet.ServletException: BeanUtils.populate</span> stack trace. This is caused by the fact that the ''struts_form.xdt'' (prior to AppFuse 1.9) did not account for plurals that ended in "ies". To fix this, replace ''metadata/templates/struts_form.xdt'' with [the latest one from CVS|https://appfuse.dev.java.net/source/browse/*checkout*/appfuse/metadata/templates/struts_form.xdt] (right-click, save as). |
|
<a name="timestampissue"></a> |
Run __and clean deploy__ and try clicking the ''Save'' button again. This time you might get the following lovely error: |
|
<div style="border: 2px solid red; background: #ffd; padding: 5px"> |
<img src="http://demo.appfuse.org/appfuse/images/iconWarning.gif" class="icon" alt="Warning" /> |
<strong>The process did not complete. Details should follow.</strong><br/> |
<img src="http://demo.appfuse.org/appfuse/images/iconWarning.gif" class="icon" alt="Warning" /> |
Could not convert java.lang.String to java.sql.Timestamp<br/> |
</div> |
|
I'll admit, I struggled with this issue for ''hours'' and I still don't have a good solution. From my hours of trial-and-error, the only conclusion I can come up with is that Struts (and Commons BeanUtils in particular) can't handle java.util.Date and java.sql.Timestamp together very well. If you know of a solution that'll allow displaying and saving a date and timestamp on the same form, please [comment on APF-176|http://issues.appfuse.org/browse/APF-176]. The problem is basically that I can't get the DateConverter to recognize Date as a java.util.Date - for some reason it thinks it's a Timestamp in the following logic: |
|
[{Java2HtmlPlugin |
|
if (value instanceof Date) { |
DateFormat df = new SimpleDateFormat(DateUtil.getDatePattern()); |
// this works in unit tests, but when running in Tomcat, a java.util.Date |
// comes through as a java.sql.Timestamp - wierd eh? |
if (value instanceof Timestamp) { |
df = new SimpleDateFormat(TS_FORMAT); |
} |
|
try { |
return df.format(value); |
} catch (Exception e) { |
e.printStackTrace(); |
throw new ConversionException("Error converting Date to String"); |
} |
} |
}] |
|
For the purpose of this tutorial, you'll need to replace your ''src/service/org/appfuse/util/__DateConverter.java__'' with the [latest one from CVS|https://appfuse.dev.java.net/source/browse/*checkout*/appfuse/src/service/org/appfuse/util/DateConverter.java]. After replacing this file and running __ant deploy__, you should be able to save the form successfully. |
|
!!Show an Entry's Category, and allow modification of it [#7] |
|
The last piece of the relationships puzzle is to show an entry's category, as well as allow it to be changed. To show the category of an entry, change: |
|
[{Java2HtmlPlugin |
|
Posted at: <nested:write property="timeCreated"/> |
}] |
|
To: |
|
[{Java2HtmlPlugin |
|
<span style="color: #333; font-size: 11px; margin: 0 5px"> |
<strong>Category:</strong> <nested:write property="category.name"/>, |
<strong>Posted at:</strong> <nested:write property="timeCreated"/></span> |
}] |
|
Run __ant deploy-web__ and refresh the weblog form. Your screen should resemble the screenshot below: |
|
%%(width: 700px; height: 316px; border: 1px solid black; margin: 10px auto 0 auto) |
[WeblogWithCategory.png] |
%% |
|
In order to allow modification of the Category, you'll need to change the ''categoryId'' from being a hidden field to being a drop-down. With Struts, the easiest way to populate a drop-down is when the application starts up. You ''could'' populate it in the Action's ''edit()'' method, but if any validation errors occur, the variable would not be re-added to the request. |
|
Add an AVAILABLE_CATEGORIES field to ''src/dao/**/__Constants.java__'': |
|
[{Java2HtmlPlugin |
|
/** |
* The name of the available categories list in application scope |
*/ |
public static final String AVAILABLE_CATEGORIES = "availableCategories"; |
}] |
|
Grab the list of possible categories in ''src/web/**/listener/__StartupListener.java__'' and stuff them into the servlet context: |
|
[{Java2HtmlPlugin |
|
context.setAttribute(Constants.AVAILABLE_ROLES, mgr.getAllRoles()); |
context.setAttribute(Constants.AVAILABLE_CATEGORIES, mgr.getObjects(Category.class)); |
}] |
|
If you're using < AppFuse 1.9, you'll need to fix [LookupManager.java|http://fisheye4.cenqua.com/browse/~raw,r=1453/appfuse/trunk/src/service/org/appfuse/service/LookupManager.java] and [LookupManagerImpl.java|http://fisheye4.cenqua.com/browse/~raw,r=1453/appfuse/trunk/src/service/org/appfuse/service/impl/LookupManagerImpl.java] so the above code works. |
|
Remove the hidden ''categoryId'' field from __weblogForm.jsp__: |
|
[{Java2HtmlPlugin |
|
<nested:hidden property="categoryId"/> |
}] |
|
And change <nested:write property="category.name"/> to: |
|
[{Java2HtmlPlugin |
|
<nested:select property="categoryId" style="color: #333; font-size: 11px; margin: 2px 10px 0 0"> |
<html:options collection="availableCategories" property="categoryId" labelProperty="name"/> |
</nested:select> |
}] |
|
Run __ant deploy__ and refresh your browser. You should now be able to modify the category for an entry. |
|
This covers most of the things you need to know for using Hibernate's relationships and modifying child elements. However, it doesn't cover adding or deleting child elements. |
|
!!Add the ability to add and delete entries [#8] |
|
There are many different ways to add and delete child elements from a form. This section is a work in progress and shows a quick-n-dirty (complete with bugs!) way of doing this. |
|
In __weblogForm.jsp__, add a "Delete" button just after the "Posted at" information: |
|
[{Java2HtmlPlugin |
|
Posted at: <nested:write property="timeCreated"/></span> |
<input type="button" class="button" name="delete" style="font-size: 11px" |
onclick="parentNode.parentNode.removeChild($('entry<%=index%>'))" value="Delete"/> |
}] |
|
Run __ant deploy-web__, refresh your browser - and you should be able to delete the entry. Click "Save" after clicking "Delete" to complete the process. While this works, it's not the ideal solution. Since it just removes the entry (and it's form elements) from the form, it doesn't re-adjust the index numbers on other entries. This means that deleting will only work for the last entry. In addition, the entry won't be deleted from the "entry" table (try ''select * from entry''). Both of these issues could likely be fixed if you linked the "Delete" button to an action (or Ajax) that deleted the entry from the database. |
|
To add a new entry, there are also a number of things you can do. The simplest one I've found is to duplicate an existing set of entry fields, re-index the input element names and clear the values. To do this: add the following after the ending </nested:iterate> for the "entries": |
|
[{Java2HtmlPlugin |
|
<hr /> |
<input type="button" class="button" onclick="toggleDisplay('newentry'); |
if ($('newentry').style.display == '') { |
var entries = parentNode.getElementsByTagName('div'); |
$('newentry').innerHTML = entries[entries.length-2].innerHTML; |
// reset all fields to blank, and change their names |
var inputs = $('newentry').getElementsByTagName('input'); |
var index = inputs[0].name.substring(inputs[0].name.indexOf('[')+1, inputs[0].name.indexOf(']')); |
var next = parseInt(index) + 1; |
for (i=0; i < inputs.length; i++) { |
inputs[i].name = inputs[i].name.replace(index, next); |
if (inputs[i].name == 'delete') { |
inputs[i].onclick = |
function() { $('newentry').innerHTML = ''; $('newentry').style.display='none'; }; |
} else if (inputs[i].name.indexOf('weblogId') == -1) { |
inputs[i].value = ''; |
} |
} |
// reset any textareas |
var boxes = $('newentry').getElementsByTagName('textarea'); |
for (i=0; i < boxes.length; i++) { |
boxes[i].name = boxes[i].name.replace(index, next); |
boxes[i].value = ''; |
} |
// reset any selects |
var dropdowns = $('newentry').getElementsByTagName('select'); |
for (i=0; i < dropdowns.length; i++) { |
dropdowns[i].name = dropdowns[i].name.replace(index, next); |
dropdowns[i].selectedIndex = 0; |
} |
boxes[0].focus(); |
this.value = this.value.replace('Add', 'Cancel'); |
} else { |
this.value = this.value.replace('Cancel', 'Add'); |
}" value="Add New Entry"/> |
<div id="newentry" style="display: none"></div> |
}] |
|
Run __ant deploy-web db-load__ and you should be able to add a new Entry by clicking on the <button>Add New Entry</button> button. There are a couple of issues with this technique: |
|
* Adding a new entry will only work if there is (at least) one entry already in the weblog. |
* The "timeCreated" field won't be set as part of the new entry. |
|
Since this tutorial is designed to show Hibernate relationships and how to edit them (moreso than techniques for adding/deleting child records), I'll leave the issues with Delete and Add as an exercise for the reader. <img src="http://raibledesigns.com/images/smileys/wink.gif" alt="Wink" style="vertical-align: middle" /> Of course, if you do figure out a good solution - please send an e-mail to the mailing list (or enter an issue in [JIRA|http://issues.appfuse.org]) so we can enhance this tutorial. |