Raible's Wiki

Raible Designs
Wiki Home
News
Recent Changes

AppFuse

Homepage
  - Korean
  - Chinese
  - Italian
  - Japanese

QuickStart Guide
  - Chinese
  - French
  - German
  - Italian
  - Korean
  - Portuguese
  - Spanish
  - Japanese

User Guide
  - Korean
  - Chinese

Tutorials
  - Chinese
  - German
  - Italian
  - Korean
  - Portuguese
  - Spanish

FAQ
  - Korean

Latest Downloads

Other Applications

Struts Resume
Security Example
Struts Menu

Set your name in
UserPreferences


Referenced by
HibernateRelationshi...




JSPWiki v2.2.33

[RSS]


Hide Menu

HibernateRelationshipsUI


This is version 47. It is not the current version, and thus it cannot be edited.
[Back to current version]   [Restore this version]


Part II: Create Weblog UI - A HowTo for creating a UI (in Struts) for managing the DAOs created in the Hibernate Relationships tutorial. I've eliminated creating managers in this tutorial for a couple reasons: 1) to reduce the amount of code and 2) to show you that you don't have to create them.

About this Tutorial

In this section, you'll create a UI that has the following features.
  • Edit a User and their Weblogs (many-to-many)
  • CRUD a Weblog and its Users (many-to-many)
  • CRUD a Weblog and its Entries (one-to-many)
  • Add a Category drop-down for Entries (many-to-one)

Table of Contents

  • [1] Modify userForm.jsp to allow creating/editing a weblog (many-to-many)
  • [2] Modify the UserAction to support editing a user's weblogs
  • [3] Create a WeblogAction to handle CRUD-ing Weblogs
  • [4] Create JSPs to display a Weblog's information
  • [5] Add the ability to edit users from the Weblog Detail Screen
  • [6] Add the ability to edit entries from the Weblog Detail Screen
  • [7] Show an Entry's Category, and allow modification of it
  • [8] Add the ability to add and delete entries

Modify userForm.jsp to allow creating/editing a weblog [#1]

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.


@struts.form include-all="true" extends="BaseForm" indexedProperties="true"

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".

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>.

<fieldset style="position: absolute; top: 190px; left: 520px">
    <legend style="font-weight: bold">Weblogs</legend>
    <c:forEach var="blog" items="${userForm.weblogs}" varStatus="index">
        <input type="hidden" name="weblogs[<c:out value='${index.index}'/>].weblogId" value="<c:out value='${blog.weblogId}'/>"/>
        <input type="hidden" name="weblogs[<c:out value='${index.index}'/>].dateCreated" value="<fmt:formatDate value='${blog.dateCreated}' pattern="MM/dd/yyyy"/>"/>
        <input type="text" name="weblogs[<c:out value='${index.index}'/>].blogTitle" size="40" value="<c:out value='${blog.blogTitle}'/>"/><br/>
    </c:forEach>
</fieldset>

Run ant clean deploy, go to http://localhost:8080/appfuse/editProfile.html and login as "tomcat/tomcat". You should see a screenshot like the one below.

UserWithWeblog.png

If you try to save at this point, you'll probably get the following error:

Warning The process did not complete. Details should follow.
Warning IllegalArgumentException occurred calling getter of org.appfuse.model.Weblog.weblogId
Warning object is not an instance of declaring class

This happens because the "weblogs" property of User is populated with a bunch of WeblogForm objects, instead of Weblog objects.

Modify the UserAction to support editing a user's weblogs [#2]

To fix saving a User, add convertLists(user) to the save() method of UserAction.java (in src/web/**/webapp/action):


        convertLists(user);

        try {
            mgr.saveUser(user);

In addition, you can add convertLists(user) to UserAction.edit():


        userForm.setConfirmPassword(userForm.getPassword());
        convertLists(userForm);
        updateFormBean(mapping, request, userForm);

NOTE: The reason convertLists(Object) isn't called automatically by the convert(Object) method is because it invokes lazy-loaded collections. APF-81

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.

Create a WeblogAction to handle CRUD-ing Weblogs [#3]

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:


    /**
     * 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(info) and put it in your src/web/**/webapp/action directory. This class already has the convertLists(Object) methods that you added to UserAction.java.

Create JSPs to display a Weblog's information [#4]

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(info) and weblogForm.jsp(info) 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.


    /**
     * @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: 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 and metadata/web/web-settings.xml (APF-123).
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):


        if (isNew) {
            weblog.setDateCreated(new Date());
        }

        mgr.saveObject(weblog);

Add the ability to edit users from the Weblog Detail Screen [#5]

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:


    <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():


        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 = (Userweblog.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. 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. 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.

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:


    return mapping.findForward("edit");

To:


    return new ActionForward("/editWeblog.html?weblogId=" + weblog.getWeblogId()true);

NOTE: In Struts 1.2.7, you can use the ActionRedirect to implement post-and-redirect a bit easier. However, Struts 1.2.7 has some issues, so AppFuse still uses 1.2.4 at the time of this writing.

Add the ability to edit entries from the Weblog Detail Screen [#6]

To edit a Weblog's entries on the same weblog form, you need to add some more code, just after the Users row.


    <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 again. You should see a screen like the one below:

WeblogWithEntry.png

If you try to clicking the "Save" button, you'll get a nice and descriptive javax.servlet.ServletException: BeanUtils.populate 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 (right-click, save as).

Run and clean deploy and try clicking the Save button again. This time you might get the following lovely error:

Warning The process did not complete. Details should follow.
Warning Could not convert java.lang.String to java.sql.Timestamp

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. 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:


        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. 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:


Posted at: <nested:write property="timeCreated"/>

To:


<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:

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:


    /**
     * 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:


    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 and LookupManagerImpl.java so the above code works.

Remove the hidden categoryId field from weblogForm.jsp:


<nested:hidden property="categoryId"/>

And change <nested:write property="category.name"/> to:


<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:


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":


<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>


Attachments:
WeblogWithUsers.png Info on WeblogWithUsers.png 12462 bytes
WeblogAction.java Info on WeblogAction.java 5803 bytes
weblogList.jsp Info on weblogList.jsp 1483 bytes
WeblogWithEntry.png Info on WeblogWithEntry.png 14386 bytes
WeblogCRUD.png Info on WeblogCRUD.png 38859 bytes
UserWithWeblog.png Info on UserWithWeblog.png 10692 bytes
weblogForm.jsp Info on weblogForm.jsp 1782 bytes
WeblogWithCategory.png Info on WeblogWithCategory.png 15572 bytes


Go to top   More info...   Attach file...
This particular version was published on 06-Nov-2006 13:52:47 MST by MattRaible.