Thursday, July 22, 2010

KTM - Customizing Roo Security

KTM is a read-write app, ie, its functionality is driven by data that is entered into it. So I needed some way to restrict different classes of user to different areas of the app. Specifically, I would like one class of user (administrator) to control the Person entity, another class (managers) to control the Client, Project, Item and Allocations entities, and yet another class (developers) to control the Task and Hours entities. By control, I mean the ability to create, update or delete an entity - read-only operations, such as show, list, find, etc, are unrestricted.

Generate default Security Setup

I started off by letting Roo generate the default security setup, by issuing the following command from the Roo shell.

1
security setup

This creates and modifies a bunch of files in the app, but the one of interest to me was the applicationContext-security.xml file, which is shown below. I have edited it slightly to make it more readable. I also removed the password hashes in the password attribute for the user tag and replaced it with ellipsis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-3.0.xsd">

    <!-- HTTP security configurations -->
    <http auto-config="true" use-expressions="true">
      <form-login login-processing-url="/static/j_spring_security_check"
           login-page="/login" 
           authentication-failure-url="/login?login_error=t"/>
        <logout logout-url="/static/j_spring_security_logout"/>
        
        <!-- Configure these elements to secure URIs in your application -->
        <intercept-url pattern="/choice/**" access="hasRole('ROLE_ADMIN')"/>
        <intercept-url pattern="/member/**" access="isAuthenticated()" />
        <intercept-url pattern="/resources/**" access="permitAll" />
        <intercept-url pattern="/static/**" access="permitAll" />
        <intercept-url pattern="/**" access="permitAll" />
    </http>

    <!-- Configure Authentication mechanism -->
    <authentication-manager alias="authenticationManager">
      <!-- SHA-256 values can be produced using 'echo -n
             your_desired_password | sha256sum' (using normal 
             *nix environments) -->
      <authentication-provider>
        <password-encoder hash="sha-256"/>
        <user-service>
          <user name="admin" password="..." authorities="ROLE_ADMIN"/>
          <user name="user" password="..." authorities="ROLE_USER"/>
        </user-service>
      </authentication-provider>
    </authentication-manager>
</beans:beans>

As you can see, its completely generic, it has no references to the application generated so far. My understanding is that this is meant more as a template that you have to customize for your app.

Modifying Person

The Roo generated security configuration presented above uses an in-memory authentication provider that is configured using the user-service element. Since my app maintains a set of Person entities, I figured I could build a custom provider that used Person data. To do that though, I needed to add a password field to Person. I do that from the Roo shell.

I also wanted to use the Person's email address as his username. This has the advantage of being unique and is easy to type (compared to the Person.name which contains the full name). So I would also need to be able to look a Person up by his email address. Once again, I use the Roo shell to generate the finder.

1
2
field string --fieldName password --class com.healthline.ktm.domain.Person
finder add --finderName findPeopleByEmailAddress

This would save the password in plain-text in the database, which is probably not desirable. Unfortunately there is no --password switch for the Roo field command. So I added a method to encrypt the password in the Person bean to a 32-character MD5 Hash and annotate it so it is called before an INSERT or UPDATE.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Person {
  ...
  @PrePersist
  @PreUpdate
  protected void encryptPassword() {
    if (password != null && (! password.matches("^[0-9a-fA-F]+$"))) {
      // prevent encryption if already encrypted
      password = DigestUtils.md5DigestAsHex(password.getBytes());
    }
  }
}

The Roo generated JSPs for Person will also need to be modified. Before I do that though, I want to make sure that our modifications don't get overwritten the next time I do something with the entity, so I set automaticallyMaintainView=false in the @RooWebScaffold annotation for PersonController.

For the create.jspx and update.jspx, the changes involve replacing the <form:input> tag for the password field with <form:password> tag. By default, the form:password tag will initialize the field, so for the update.jspx, I added another attribute showPassword="true" so it doesn't.

I left the generated password in for the list.jspx and show.jspx, since its encrypted on its way in anyway, and what shows up is a 32-character hex string which wouldn't mean much to most people, and because it could be helpful for debugging if you are the developer.

Custom Authentication Provider

My custom authentication provider extends AbstractUserDetailsAuthenticationProvider, which works with username/password kind of setups. Since KTM deals with projects, the administrator's details are not recorded in the Person table. So the provider declares a administrator pseudo-user, the username and password for which is injected into the provider via Spring configuration.

The provider calls its retrieveUser() method to authenticate the user using the email address as username and entered password for password, encrypting the password to match the one it looks up using the Person.findPeopleByEmailAddress() from the database. It then uses the WorkRole enum value in the Person entity to figure out the authorizations. For WorkRole.Manager, the GrantedAuthority is ROLE_MANAGER, for WorkRole.Developer it is ROLE_DEVELOPER, and for WorkRole.Combined, it is ROLE_MANAGER and ROLE_DEVELOPER. For the admin user, the GrantedAuthority is ROLE_ADMIN. The method returns a populated UserDetails object if the login succeeded, or throws a BadCredentialsException with the appropriate message if not.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package com.healthline.ktm.security;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityNotFoundException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.Query;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;

import com.healthline.ktm.domain.Person;
import com.healthline.ktm.domain.WorkRoles;

@Service("ktmAuthenticationProvider")
public class KtmAuthenticationProvider extends 
    AbstractUserDetailsAuthenticationProvider {

  private final Logger logger = Logger.getLogger(getClass());

  private String adminUser;
  private String adminPassword;
  
  @Required
  public void setAdminUser(String adminUser) {
    this.adminUser = adminUser;
  }
  
  @Required
  public void setAdminPassword(String adminPassword) {
    this.adminPassword = adminPassword;
  }
  
  @Override
  protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    return;
  }

  @Override
  protected UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    String password = (String) authentication.getCredentials();
    if (! StringUtils.hasText(password)) {
      throw new BadCredentialsException("Please enter password");
    }
    String encryptedPassword = DigestUtils.md5DigestAsHex(password.getBytes()); 
    UserDetails user = null;
    String expectedPassword = null;
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
    if (adminUser.equals(username)) {
      // pseudo-user admin (ie not configured via Person)
      expectedPassword = DigestUtils.md5DigestAsHex(adminPassword.getBytes()); 
      // authenticate admin
      if (! encryptedPassword.equals(expectedPassword)) {
        throw new BadCredentialsException("Invalid password");
      }
      // authorize admin
      authorities.add(new GrantedAuthorityImpl("ROLE_ADMIN"));
    } else {
      try {
        Query query = Person.findPeopleByEmailAddress(username);
        Person person = (Person) query.getSingleResult();
        // authenticate the person
        expectedPassword = person.getPassword();
        if (! StringUtils.hasText(expectedPassword)) {
          throw new BadCredentialsException("No password for " + username + 
            " set in database, contact administrator");
        }
        if (! encryptedPassword.equals(expectedPassword)) {
          throw new BadCredentialsException("Invalid Password");
        }
        // authorize the person
        WorkRoles role = person.getWorkRole();
        switch (role) {
          case Manager:
            authorities.add(new GrantedAuthorityImpl("ROLE_MANAGER"));
            break;
          case Combined:
            authorities.add(new GrantedAuthorityImpl("ROLE_MANAGER"));
            authorities.add(new GrantedAuthorityImpl("ROLE_DEVELOPER"));
            break;
          case Developer:
            authorities.add(new GrantedAuthorityImpl("ROLE_DEVELOPER"));
            break;
          default:
            // should never happen since Person will have one of
            // the above WorkRoles defined, but just in case we
            // decide to add a new role in the future...
            throw new BadCredentialsException("User:[" + username + 
              "] has unknown role: " + role);
        }
      } catch (EntityNotFoundException e) {
        throw new BadCredentialsException("Invalid user");
      } catch (NonUniqueResultException e) {
        throw new BadCredentialsException(
          "Non-unique user, contact administrator");
      }
    }
    return new User(
      username,
      password,
      true, // enabled 
      true, // account not expired
      true, // credentials not expired 
      true, // account not locked
      authorities
    );
  }
}

In the applicationContext-security.xml file, the KtmAuthenticationProvider bean replaces the default in-memory authentication provider generated by Roo. Here is what the block under the "Configure Authentication Mechanism" looks like with my custom authentication provider.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  <!-- Configure Authentication mechanism -->
  <beans:bean id="ktmAuthenticationProvider" 
      class="com.healthline.ktm.security.KtmAuthenticationProvider">
    <beans:property name="adminUser" value="admin"/>
    <beans:property name="adminPassword" value="admin"/>
  </beans:bean>
  
  <authentication-manager alias="authenticationManager">
    <authentication-provider ref="ktmAuthenticationProvider"/>
  </authentication-manager>

Customizing Intercept URL Patterns

Now that our authentication provider returns a UserDetail with a List of GrantedAuthority objects that correspond to our app, we can update the block titled "HTTP Security configurations" in the applicationContext-security.xml. This snippet from my updated applicationContext-security.xml file is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
  <!-- HTTP security configurations -->
  <http auto-config="true" use-expressions="true" 
      access-denied-page="/app/accessDenied">
    <form-login login-processing-url="/static/j_spring_security_check" 
        login-page="/login" 
        authentication-failure-url="/login?login_error=t"/>
    <logout logout-url="/static/j_spring_security_logout"/>
        
    <!-- ROLE_ADMIN has create/edit/delete on Person -->
        
    <intercept-url pattern="/person/form" access="hasRole('ROLE_ADMIN')"/>
    <intercept-url pattern="/person/\\d+/form" access="hasRole('ROLE_ADMIN')"/>
    <intercept-url pattern="/person/**" method="DELETE" 
      access="hasRole('ROLE_ADMIN')"/>
        
    <!-- ROLE_MANAGER has create/edit/delete on Client, Project, -->
    <!-- Item, Allocations                                       -->
      
    <intercept-url pattern="/client/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/client/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/client/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <intercept-url pattern="/project/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/project/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/project/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <intercept-url pattern="/item/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/item/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/item/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <intercept-url pattern="/allocations/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/allocations/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/allocations/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <!-- ROLE_DEVELOPER has create/edit/delete on Task, Hours -->
    
    <intercept-url pattern="/task/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/task/\\d+/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/task/**" method="DELETE" 
      access="hasRole('ROLE_DEVELOPER')"/>
        
    <intercept-url pattern="/hours/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/hours/\\d+/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/hours/**" method="DELETE" 
      access="hasRole('ROLE_DEVELOPER')"/>

    <!-- Everything else can be accessed by anybody, logged in or not -->
    <intercept-url pattern="/resources/**" access="permitAll" />
    <intercept-url pattern="/static/**" access="permitAll" />
    <intercept-url pattern="/**" access="permitAll" />
  </http>

The inline comments are pretty-self explanatory - the XML above basically codifies the rules outlined in the first paragraph in this post. Since Roo generates a REST-ful app, the deletes are handled using a HTTP delete method, which we need to handle using the method attribute in the intercept-url elements above as explained here.

At this point, if someone clicks "Create new Person" from the main page, they will be presented with a login screen. Once they login with admin/admin (as configured in the KtmAuthenticationProvider Spring config), they would then see the Create new Person screen. The same workflow would exist for the other "Create" links - based on the authorization, they would either be sent to the form page or to an "Access Denied" page (more on this in a bit).

Turning off Unauthorized Functionality

The interception style of authenticating is good for external facing web applications (such as websites), since most of the content is for public consumption, and you authenticate either when a user decides to participate, or when you are exposing personal content. For intranet/tool applications such as KTM, I prefer to start the user with a login page, and expose only the functionality which they are authorized for when they do login.

Spring Security (or Acegi) has some JSP tags which allow you to do this fairly easily. If I wanted to go the "put everything behind a login screen" route, I would need to wrap the protected portions (of menu.jspx) in <sec:authorize> tags and replace index.jspx with login.jspx (I think, haven't tried it). But I was too lazy to do this - since this would be an Intranet app, doing it one way or the other is merely a matter of user training.

I did, however, want to hide the update and delete icons from the "List all XXX" pages from unauthorized users. So I basically had to do wrap the update and delete icon columns in the table representing the listing, and to bind the sec: namespace to the URI for the Spring security TLD. The basic pattern is:

1
2
3
  <sec:authorize access="hasRole('ROLE_ADMIN')">
    // stuff you want to show only to admin goes here
  </sec:authorize>

Once this was done, the person listing page will look different based on whether you are logged in as "admin" (the one on the left below), or if you are either not logged in, or logged in with some other role (the one on the right below).

Static Access Denied Page

One of the problems with the intercept pattern is multiple roles. For example, if a user clicks on "Create a Person", and is presented with a login screen, it is not immediately obvious what he should login as. For example, if he logs in as himself, and he has ROLE_MANAGER, then what happens is that he falls through to a 403 Access Denied page served by the container (in my case Jetty).

So I decided to build an accessDenied.jspx page along the same lines as resourceNotFound.jspx - ie, a static page with an appropriate message and a short description of the different roles and what functions they can perform. It looks like this:

To build this page, I copied resourceNotFound.jspx into accessDenied.jspx and changed the title and problemdescription label names, then mimicked its configuration by doing a "find | xargs grep". Here are the locations I had to modify.

  • messages*.properties - created the contents of the accessdenied.title and accessdenied.problemdescription labels in 5 different languages and add them as properties in these files.
  • webmvc-config.xml - add in a line <mvc:view-controller path="/aceessDenied.jspx"/>, similar to the one for resourceNotFound. This creates a "static" Spring controller.
  • web.xml - add an <error-page> entry for HTTP error code 403 for /app/accessDenied.
  • views.xml (top level) - In the top level views.xml tiles definition file (under WEB-INF/views), I created an entry pointing to the actual JSPX file.
  • applicationContext-security.xml - I added the access-denied-page attribute to the http element in our applicationContext-security.xml pointing to /app/accessDenied. This is already in the snippet shown above.

Manual Change Password Controller

With the setup described so far, the administrator is the only one who can create or update a user's password. This is a bit inflexible in most real-world scenarios - ideally, the user should be able to change his password to something else. I built a simple manual Roo controller for this. Skeleton code is generated using the Roo shell:

1
controller class --class com.healthline.ktm.web.ChangePasswordController

The generated controller is similar to the SimpleFormController of the pre-annotation Spring days. I don't know about you, but to me, the SimpleFormController, while powerful, was anything but simple, and my work did not involve that much form handling anyway, so I never used it. In any case, based on the advice from here and here, I managed to figure out how to work with it, although the code I ended up with exposed different, but equivalent, method signatures compared to the one generated.

First, the controller. As you can see, it exposes a named bean via the @ModelAttribute annotation. When the "Change Password" link is clicked, the index() method is called. This sends an empty model attribute bean (the form bean) to the JSP which displays the form. When the user fills out the form and clicks submit, the update() method is called, which validates the data using the @Autowired validator instance. If everything is good, it sets the encrypted password into the Person instance and updates it, and forwards to the "thanks" view, powered by the thanks() method. If not, the form is redisplayed with the appropriate error message(s).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.healthline.ktm.web;

import java.util.List;

import javax.persistence.Query;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.healthline.ktm.domain.Person;
import com.healthline.ktm.fbo.ChangePasswordForm;
import com.healthline.ktm.fbo.ChangePasswordValidator;

@RequestMapping("/changepassword/**")
@Controller
public class ChangePasswordController {

  private final Logger logger = Logger.getLogger(getClass());

  @Autowired private ChangePasswordValidator validator;
  
  @ModelAttribute("changePasswordForm")
  public ChangePasswordForm formBackingObject() {
    return new ChangePasswordForm();
  }

  @RequestMapping(value="/changepassword/index")
  public String index() {
    return "changepassword/index";
  }

  @RequestMapping(value="/changepassword/update", method=RequestMethod.POST)
  public String update(
      @ModelAttribute("changePasswordForm") ChangePasswordForm form, 
      BindingResult result) {
    validator.validate(form, result);
    if (result.hasErrors()) {
      return "changepassword/index"; // back to form
    } else {
      String newPassword = form.getNewPassword();
      Query query = Person.findPeopleByEmailAddress(form.getEmailAddress());
      Person person = (Person) query.getSingleResult();
      person.setPassword(newPassword);
      person.merge();
      return "changepassword/thanks";
    }
  }
  
  @RequestMapping(value="/changepassword/thanks")
  public String thanks() {
    return "changepassword/thanks";
  }
}

The form bean exposed through the @ModelAttribute annotation is a POJO that exposes getters and setters for the form fields. Like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.healthline.ktm.fbo;

public class ChangePasswordForm {

  private String emailAddress;
  private String oldPassword;
  private String newPassword;
  private String newPasswordAgain;

  // getters and setters omitted, use your IDE to fill them out  
}

The controller also autowires in a custom validator to validate the fields.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.healthline.ktm.fbo;

import javax.persistence.EntityNotFoundException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.Query;

import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import com.healthline.ktm.domain.Person;

@Service("changePasswordValidator")
public class ChangePasswordValidator implements Validator {

  @Override
  public boolean supports(Class<?> clazz) {
    return ChangePasswordForm.class.equals(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    ChangePasswordForm form = (ChangePasswordForm) target;
    String emailAddress = form.getEmailAddress();
    try {
      Query query = Person.findPeopleByEmailAddress(emailAddress);
      Person person = (Person) query.getSingleResult();
      String storedPassword = person.getPassword();
      String currentPassword = DigestUtils.md5DigestAsHex(
        form.getOldPassword().getBytes());
      if (! currentPassword.equals(storedPassword)) {
        errors.rejectValue("oldPassword", "changepassword.invalidpassword");
      }
      String newPassword = form.getNewPassword();
      String newPasswordAgain = form.getNewPasswordAgain();
      if (! newPassword.equals(newPasswordAgain)) {
        errors.reject("changepassword.passwordsnomatch");
      }
    } catch (EntityNotFoundException e) {
      errors.rejectValue("emailAddress", "changepassword.invalidemailaddress");
    } catch (NonUniqueResultException e) {
      errors.rejectValue("emailAddress", 
        "changepassword.duplicateemailaddress");
    }
  }
}

On the JSP side, I modified the generated index.jspx to look like this, using snippets from the various other generated JSPX files to make the end result look similar to the other forms.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<div xmlns:c="http://java.sun.com/jsp/jstl/core" 
    xmlns:form="http://www.springframework.org/tags/form" 
    xmlns:jsp="http://java.sun.com/JSP/Page" 
    xmlns:spring="http://www.springframework.org/tags" version="2.0">
  <jsp:output omit-xml-declaration="yes"/>
  <script type="text/javascript">dojo.require('dijit.TitlePane');dojo.require('dijit.form.SimpleTextarea');dojo.require('dijit.form.FilteringSelect');</script>
  <div id="_title_div">
    <spring:message code="label.changepassword" var="title_msg"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <spring:url value="/changepassword" var="form_url"/>
    <spring:message var="title" code="label.changepassword"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <form:form action="/ktm/changepassword/update" method="POST" commandName="changePasswordForm">
      <div id="changepassword_emailaddress">
        <label for="_emailaddress_id">Email Address:</label>
        <form:input cssStyle="width:250px" id="_changepassword_emailaddress" maxlength="30" path="emailAddress"/>
        <br/>
        <form:errors cssClass="errors" path="emailAddress"/>
      </div>
      <br/>
      <div id="changepassword_oldpassword">
        <label for="_oldpassword_id">Current Password:</label>
        <form:password cssStyle="width:250px" id="_changepassword_oldpassword" maxlength="30" path="oldPassword"/>
        <br/>
        <form:errors cssClass="errors" path="oldPassword"/>
      </div>
      <br/>
      <div id="changepassword_newpassword">
        <label for="_newpassword_id">New Password:</label>
        <form:password cssStyle="width:250px" id="_changepassword_newpassword" maxlength="30" path="newPassword"/>
        <br/>
        <form:errors cssClass="errors" path="newPassword"/>
      </div>
      <br/>
      <div id="changepassword_newpasswordagain">
        <label for="_newpasswordagain_id">New Password (again):</label>
        <form:password cssStyle="width:250px" id="_changepassword_newpasswordagain" maxlength="30" path="newPasswordAgain"/>
        <br/>
        <form:errors cssClass="errors" path="newPasswordAgain"/>
      </div>
      <br/><br/>
      <div class="submit" id="changepassword_submit">
        <spring:message code="button.save" var="save_button"/>
        <script type="text/javascript">Spring.addDecoration(new Spring.ValidateAllDecoration({elementId: 'proceed', event : 'onclick'}));</script>
        <input id="proceed" type="submit" value="${save_button}"/>
      </div>
      <br/>
      <form:errors cssClass="errors" delimiter="&lt;p/&gt;"/>
    </form:form>
  </div>
</div>

The thanks.jspx is even simpler. Here it is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<div xmlns:c="http://java.sun.com/jsp/jstl/core" 
    xmlns:form="http://www.springframework.org/tags/form" 
    xmlns:jsp="http://java.sun.com/JSP/Page" 
    xmlns:spring="http://www.springframework.org/tags" version="2.0">
  <jsp:output omit-xml-declaration="yes"/>
  <script type="text/javascript">dojo.require('dijit.TitlePane');dojo.require('dijit.form.SimpleTextarea');dojo.require('dijit.form.FilteringSelect');</script>
  <div id="_title_div">
    <spring:message code="label.changepassword" var="title_msg"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <spring:url value="/changepassword" var="form_url"/>
    <spring:message var="title" code="label.changepassword"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <spring:message code="changepassword.thankyoumessage" var="thankyou_message"/>
    <h3>${thankyou_message}</h3>
  </div>
</div>

As with the accessDenied.jspx, I had to add a bunch of labels for values of keys that are referred to from the JSPX files and the Validator. Here they are (for messages.properties).

1
2
3
4
5
6
7
#changepassword
changepassword.invalidpassword=Invalid Current Password
changepassword.passwordsnomatch=Passwords do not match
changepassword.invalidemailaddress=Invalid Email Address
changepassword.duplicateemailaddress=\
Duplicate Email Address, contact administrator
changepassword.thankyoumessage=Your password has been changed.

I also had to register the thanks.jspx into the views.xml tiles definition file for changepassword, similar to the index.jspx that was already set in there by Roo.

Once all this was set up, clicking the Change Password link from the left nav led to the form shown on the left below. On hitting submit after entering my email address, old and new password, I get the confirmation message shown on the right below.

Conclusion

I still haven't gotten to the "interesting" part of my application :-). But the process of customizing the standard Roo authentication template for my own purposes has taught me a great deal. I found it fairly easy to do the customization, even though a lot of time was spent trying to find all the places to update. But that is a one time learning effort, the process should go much quicker the next time.

Building the Change Password functionality has also taught me about the mechanics of building a manual Roo controller, and the places to add and update, so hopefully I will be able to concentrate on application logic for the next (and final) part of this project.

Friday, July 16, 2010

KTM - Basic Customizations

The KTM application that I generated last week from my Roo script was nice but pretty basic. It provides a menu page, and a Create/List page for each database entity in the system. Within the list page are links that allow you to update or delete an entry. The interface probably doesn't compare all that well against something custom-built, but has all the features that you need to enter data. In fact, the cookie cutter interface can be an advantage - once you are used to it, you can move to using other applications (with similar cookie-cutter interfaces) without much of a learning curve.

However, KTM does need to be customized somewhat from what Roo generated. For one, you want to be able to tell it apart from other Roo generated applications you may be using. There are also some simple customizations that has the potential to enhance its usability. Most of these involve either writing no code or very little code.

This post describes these changes as a sequence of steps that I performed to get from the KTM interface shown at the bottom of the last post to the one shown at the bottom of this one. Where applicable, I will describe the code changes (Roo script and Java snippets) also.

Cosmetic Customizations

This just involves replacing the following image resources with custom ones. I used GIMP to build these versions. I am only marginally familiar with GIMP, but it is very well documented on the Internet, so it was not too much of a problem.

  • banner-graphic.png - I initially tried to modify the banner-graphic.png file provided, but then realized that it may be better to just create a similarly sized banner using GIMP from scratch. The cat image comes from a site offering free stock photos of cats (I think its from FotoSearch, but not sure.
  • favicon.ico - I took the stock photo and then scaled it to 16x16 pixels, saved it as a PPM, then used ppmtowinincon to convert it to an ICO file as described here.
  • springsource-logo.png - I built this from scratch, using the SpringSource icon already present in the original, taking the original Roo favicon ICO file and converting it back to a PPM using winicontoppm, and incorporating it into a "Spring-Roo powered logo".

As you see, I chose orange as my base color for the banner. The next thing I did was to change the color pallete in standard.css and alt.css from the olive green default to my banner background color.

I then went in to messages.properties and changed the welcome.titlepane, welcome.h3 and welcome.text properties so that the application name is in all-caps, and the welcome text contains a brief description of what the application does (or will do once its finished) instead of the Spring-Roo blurb.

Just to keeping things complete, and since Roo was kind enough to generate for me message_xx.properties files for 5 other locales, I went to Babel Fish and made translations of these three welcome.* properties to German, Dutch, Spanish and Italian and stuck them into the respective files. For the last one (English to Swedish) I used this Free Online English to Swedish Translater (there are other languages) powered by WordTran/NeuroTran. The translations may not be perfect, since they are machine generated, but they are good placeholders for when I actually have users in these locales :-).

Usability Customizations

This group of customizations are driven by usability considerations. While trying to enter data, I found places where these changes would make the application more usable. Some of these changes are driven using Roo scripts (resulting in the appropriate code being generated) or making (minor) tweaks the generated Java code (and letting Roo round-tripping make the associated changes). Broadly, there are four main classes of these customizations.

  • Overriding toString() for reference fields
  • Adding backreferences
  • Reordering entity fields
  • Adding Finders where appropriate
  • Reordering menu items

Overriding toString()

The default Roo generated toString() method is in the AspectJ class (ITD) referenced from the entity class using @RooToString annotation, and basically builds a pipe-delimited string of all the member variables in the entity. This creates really long entries in the drop down on the many side of the @OneToMany relationships in my model.

To fix that, I declared toString() methods in Client, Item, Person, Project and Task classes, removing the @RooToString annotation. This causes Roo to remove the associated ITD and honor my toString(). For the first four, the toString() just returns the name, like so:

1
2
3
4
    @Override
    public String toString() {
      return name;
    }

In case of Task, it is a join of the Item name and the task name, since a task name is not necessarily unique by itself, but is more likely to be unique for a given Item.

1
2
3
4
5
6
7
8
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append(item == null ? "NULL" : item.toString()).
          append(" :: ").
          append(name);
        return buf.toString();
    }

The Roo docs state (or imply) that its not necessary to remove the @RooToString annotation by hand, but I had problems with mvn complaining about multiple toString() methods on application startup on my first try. Removing the annotation manually is not a big deal, so I just did it for the other entities - Roo immediately removes the associated toString() Aspect.

I also wanted to enforce name uniqueness for the first four classes and the (item,task) combination unique for the Task, but Roo does not provide a --unique switch during creation. I tried the @Column(unique=true) and also the class level @UniqueConstraint annotations, but was unable to make them work. There is an open bug to have the --unique feature, so I figured that when it became available, I could just use that, and did not investigate the JPA approach further.

Another thing I would like to see in the Roo scripting language is the ability to define how the generated code should look, such as indents (I use 2 spaces as you can see from my previous posts, while Roo generated code uses 4), style (K&R, GNU, etc) but that would be more of an icing on the cake kind of thing. I don't see a bug open for that in the Roo JIRA, so maybe once I am done with my application, I would request this.

Adding Backreference fields

While entering data for the various entities, I noticed that it would be more convenient if I knew about what the parent entity was in some cases. So I added backreferences (really reference fields) from project to client, task to project, hours to project and hours to person. Here is the relevant bits from my log.roo file.

1
2
3
4
5
6
7
8
field reference --class com.healthline.ktm.domain.Project \
  --fieldName client --type com.healthline.ktm.domain.Client
field reference --class com.healthline.ktm.domain.Task \
  --fieldName project --type com.healthline.ktm.domain.Project
field reference --class com.healthline.ktm.domain.Task \
  --fieldName project --type com.healthline.ktm.domain.Project
field reference --class com.healthline.ktm.domain.Hours \
  fieldName assignee --type com.healthline.ktm.domain.Person

Having these fields in here also allows us to set up finders for each of these entities (see below).

Reordering Entity Fields

I went into each entity class and reordered the member variables so the order in which they appear in the Create/Update form make sense for a person trying to enter data. I followed the same convention throughout, with name and backreference type reference fields at the top, followed by the regular fields, followed by drop downs to other non-backref type reference fields.

Adding Finders

Based on entering the data, I added finders where I thought they would be useful. For example, one may want to see all Items for a given Project, or all Tasks for a given Assignee, etc. Here is the relevant snippet from my log.roo file for the finders.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Finders for Item: find by project
finder list --class com.healthline.ktm.domain.Item
finder add --finderName findItemsByProject
// Finders for Task: find by person, item, project
finder list --class com.healthline.ktm.domain.Task
finder add --finderName findTasksByItem
finder add --finderName findTasksByProject
finder add --finderName findTasksByAssignee
// Finders for Hours: find by person, task
finder list --class com.healthline.ktm.domain.Hours
finder add --finderName findHoursesByAssignee
finder add --finderName findHoursesByTask
// Finders for Allocations: find by project, person
finder list --class com.healthline.ktm.domain.Allocations
finder add --finderName findAllocationsesByProject
finder add --finderName findAllocationsByAssignee

This immediately results in the ITDs for the Finders to be generated, and the relevant menu entries on the LHS navigation bar to be populated with Finder links.

Reordering Menu Items

Finally, I decided to group the entities by who was most likely to use them. For example, Persons would typically be created by an Administrator. Projects and Items would be created by managers. Managers would also be making changes to Project/Person Allocations. Developers would break up Items into Tasks and estimate them, as well as enter Hours burned for each task, so it makes sense to reorder them accordingly.

This involved changing the menu.jspx file - nothing major, just copying and pasting <li> blocks around. This is a bit risky, since any change to an entity would wipe out my changes, so I decided to do this last. In any case, at this point, changes to my entities should be minor, and so if necessary, I guess I will have to redo the changes (or grab the old version from my source repository).

Result

You can see the result of all these changes below. As you can see, the customizations themselves are not that major, but they definitely make a difference to the looks and usability of the application.

Most of the time I spent on KTM this week involved either thinking about what customization I wanted to do, figuring out where to put it in the Roo generated code, and what sequence to do it in so there is no repeated effort (ie, Roo does not overwrite my changes as a result of some other change). I guess that is as it should be for a RAD tool, you spend more time thinking than doing.

I also spent some time figuring out how to write my custom controllers that would take advantage of the data entered into the app to do the "interesting stuff" I keep talking about - I did make a small start in that direction, but don't have anything significant to write about.

Another thing I want to do is to apply security to the app. Setting up the default security with an in-memory authentication provider is easy enough (one line of Roo script code). However, I would like to use the Person data I am capturing to drive the security. I have some code written already, and hope to complete it in time for my next post.

Friday, July 09, 2010

Herding Cats, Roo and the birth of KTM

This week I bring together three separate threads of thought that have been bouncing around in my head, and attempt to join them together into something approaching a cohesive whole, followed by an incomplete implementation of that whole.

Herding Cats

I have always been a bit of a sucker for self-improvement books. Early on in my career when I had to travel a lot, I would end up buying one everytime I travelled, usually at the airport/train station bookstore. I don't buy these quite so often nowadays, but a few years ago, the Agile/Pragmatic movement found me (or I found them), and I ended up buying some again. One of these books was Herding Cats: A Primer for Programmers Who Lead Programmers by J Hank Rainwater.

Despite the mixed reviews this book has received on Amazon, I found the book quite entertaining and yes, even somewhat useful. I think its a matter of perspective. You can choose to be offended by the stereotyping of programmers, or you can think of the book as a design document of how to deal with your coworkers, based on insufficient data (quite a common occurrence in our business). The rest of the data must come from the reader's environment and the design document tweaked based on this data for the book to be useful.

In the book, Rainwater describes a PC based application that he used for keeping track of what his programmers were tasked with. In the spirit of tweaking the design, I decided to build something similar, but which conforms more closely with my environment.

Roo

A few weeks ago, I attended a talk on Spring-Roo by Ramnivas Laddad at the eBig Java SIG. I had heard of Roo and knew it was the Java equivalent of Ruby-on-Rails, but did not have the time to look at further. The talk took us through setting up Roo and using it to build a web application from scratch. What the talk did for me was get me out of my inertia (at least with respect to Roo) and prompt me to think about learning it so I could use it for a real application.

Apart from the productivity gain that is an implicit expectation from any RAD tool, what I am looking from Roo is eliminating the drudgery of the CRUD crud (pardon the pun) that accompanies almost every database backed web application. Most web applications are designed to do something "interesting", but to do that you need data, and you have to build in the interface to capture that data. Roo does that for you, allowing you to focus on the interesting part of the application.

For those of you who are unfamiliar with Roo, it is a bi-directional RAD tool that allows a programmer to use a scripting language to generate (fairly complete) CRUD web applications using Java and JSP (and AspectJ). The generated code is in the form of a Maven2 web project, and uses components (such as JPA, Hibernate, Spring-Web, etc) that Java developers are either already familiar with or can pick up fairly easily. The net effect is that the generated code looks quite familiar to a Java programmer, and thus (this is the important part) can be customized without too much effort.

Roo uses the ActiveRecord pattern, so an entity contains the functions to persist itself. However, much of the persistence code and other code that are unlikely to need customization are hidden away into the generated AspectJ files. With the associated Spring Tool Suite (STS) plugin for Eclipse (which I don't use since MyEclipse does not support it yet), the AspectJ files are hidden by default in the IDE. So customization effectively means that you can modify the entity class itself, including class and method level annotations, any extra controller classes that you generate, and any generated JSPX files once you annotate the controller appropriately.

Burndown Charts, or Managing by Numbers

At my previous employer, we practiced Agile Software Development. One of the things that I liked about this methodology was the use of Burndown Charts.

With Burndown Charts, a manager (PM or lead programmer) is responsible for breaking the project into a set of high level items and assigning them to various programmers. The programmer is responsible for breaking the item down into manageable chunks, and providing realistic estimates (in hours) for completion of these tasks. Simplistically, the sum of the estimates divided by the number of available programmer hours is the estimate for the entire project.

The approach has a number of advantages over the more traditional approach of making a guesstimate of the project duration, padding by an arbitary value and setting the deadline, and then working like hell to meet it. For the programmer, it means that they get to set the estimate for stuff they are going to do - given their knowledge of the task (which is why they were assigned to it) and their own capabilities, they are inarguably the best people to come up with this number. For the manager, it means more accurate project estimates and fewer missed deadlines. Programmers are more likely to honor estimates they have provided than those handed to them, so in this situation, they would usually work harder if they are in danger of missing one. On the other hand, even if the computed value is not in line with business goals, it provides an early warning, and the estimates can be tweaked in consultation with the programmer to either work more hours or cut non-essential features.

Since Burndown Charts are generated off progress data (hours burned per task) programmers enter daily, the chart also provides an early warning of someone in danger of missing a task deadline and jeopardizing the project. Corrective action can then be taken, usually involving the programmer having to work extra hours to get back on target. However, since the whole thing is driven by numbers, this is usually a mutual agreement between manager and programmer rather than a confrontational situation common in traditional shops.

Having experienced the power of Burndown Charts, I've wanted to implement something similar ever since my duties included managing other people. Since I carry a full programmer's load, I don't have too much time left to do typical "management stuff". Luckily for me, the people reporting to me are quite skilled, and they take as much pride in their work as I do, so I've been getting by without spending too much time on it. However, part of my job involves knowing "where person X is with task Y and how much more time will it take" - based on how busy I am or how recently I have spoken with X, the answer could be either spot on or the less satisfactory "I don't know but I will find out". What Burndown Charts have the potential to do is to replace that part of my job almost completely with a computer.

It also has the potential to eliminate the manual work involving estimating new projects - I typically build the item list and then go around to my assignees in order to get their estimates to incorporate it into the master document. However, because of the work required, that document is never updated, so a Burndown Chart based solution would be much more preferable.

KTM - Bringing it together

The Burndown Chart that we used at my previous employer was Excel based, so my first attempt was to build something similar with OpenOffice Calc, but I am no Excel/Calc macros guru, so I eventually abandoned it in favor of something more interesting.

The second attempt was shortly after learning Ruby-on-Rails and reading the Herding Cats book. I built my database model (based on the book) and got the basic skeleton working, but decided to switch to Java when I discovered that I had to really learn Ruby to do anything beyond getting the data entry screens working.

Writing in Java was no picnic either (really boring because of the repetitive nature of the app), so I decided to build my own CRUD app generator. It worked, though not as well as Roo. Code generation was one-way, and it did not generate JSPs that were as pretty or as powerful as Roo. I don't remember what happened after that, but I guess I got interested in something else...

So the current attempt has two main goals - to learn Roo by building something that hits Roo's sweet spot, ie, a database backed CRUD web application, and to build me something that can help me with my management responsibilities. The plan is to use Roo to build the data entry screens, then use standard Spring/Java/JSP to build the rest of the application to support Burndown Charts.

In case you are curious, the name of the application comes from the US pronounciation for Kathmandu, the capital of Nepal - (cat-man-do) - as opposed to the more correct (IMO, at least based on geographical proximity) pronounciation that I am used to - (cuth-mun-do). The name and its most common variant is already being used by a couple of projects on Sourceforge, so I decided to call it "KTM", the airport code for Kathmandu. Its also shorter, so less chances of making a typo on the URL.

Application Generation

The object model for my entities is shown below. I used the three entities in the Herding Cats book, and added four of my own, since my objective is to support more than just the simple reporting interface of the original application.

You can regenerate the application locally by installing Roo (I used version 1.0.2-RELEASE) and running the following Roo script. The script is adapted from my log.roo file. My log.roo file is much more messier. I started with a database model as with RoR, then realized midway that the Roo approach favors an object model instead. I ended up removing and re-adding quite a few fields and entities using Roo's round trip support. I have also put in comments inside the script file, as well as resequenced the calls to make them easier to understand.

Before running the script, you will need to create an empty MySQL database called ktmdb (or something else if you modify the --databaseName in the persistence setup command in the script). You will definitely also need to update the --userName and --password values to match your database username and password. You will also need to join the lines terminated with "\" with the next line - I put in the "\" for readability.

Then create a directory for the project, cd to it, and then run the following script through Roo (cat script.roo | roo or use the script command).

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
// Spring Roo 1.0.2.RELEASE [rev 638] log opened at 2010-07-06 08:16:03
project --topLevelPackage com.healthline.ktm
persistence setup --provider HIBERNATE --database MYSQL \
    --databaseName ktmdb --userName root --password orange
// :::: create our enums first ::::
// :::: create enum WorkRoles ::::
enum type --class com.healthline.ktm.domain.WorkRoles
enum constant --name Developer
enum constant --name Manager
enum constant --name Combined
// :::: create enum ClientTypes ::::
enum type --class com.healthline.ktm.domain.ClientTypes
enum constant --name AdInitiatives
enum constant --name NetworkPartners
enum constant --name BizDev
enum constant --name Internal
// :::: create enum ProjectTypes ::::
enum type --class com.healthline.ktm.domain.ProjectTypes
enum constant --name Billable
enum constant --name Maintenance
enum constant --name Investment
enum constant --name Rework
// :::: create Person domain object ::::
entity --class com.healthline.ktm.domain.Person
field string --fieldName name --notNull --sizeMin 2 --sizeMax 32
field enum --fieldName workRole --type com.healthline.ktm.domain.WorkRoles
field string --fieldName emailAddress --notNull 
field string --fieldName telephoneNumber --notNull
field number --fieldName availableHours --notNull \
    --type java.lang.Integer --min 1 --max 8
// :::: create Client domain object ::::
entity --class com.healthline.ktm.domain.Client 
field string --fieldName name --notNull --sizeMax 40
field string --fieldName salesContact --notNull --sizeMax 40
field enum --fieldName clientType --type com.healthline.ktm.domain.ClientTypes
field reference --fieldName engrContact --notNull \
    --type com.healthline.ktm.domain.Person
// :::: create Project domain object ::::
entity --class com.healthline.ktm.domain.Project
field string --fieldName name --notNull --sizeMin 2
field string --fieldName description --notNull --sizeMin 2 --sizeMax 255
field date --fieldName startDate --notNull --type java.util.Date
field date --fieldName endDate --notNull --type java.util.Date
field reference --fieldName originator --type com.healthline.ktm.domain.Person
field reference --fieldName client --type com.healthline.ktm.domain.Client
field enum --fieldName projectType --type com.healthline.ktm.domain.ProjectTypes
// :::: create Item domain object ::::
entity --class com.healthline.ktm.domain.Item
field string --fieldName name --notNull --sizeMin 2 --sizeMax 64
field string --fieldName description --notNull --sizeMin 2 --sizeMax 255
field reference --fieldName assignedTo --type com.healthline.ktm.domain.Person
// :::: create Task domain object ::::
entity --class com.healthline.ktm.domain.Task
field string --fieldName name --notNull --sizeMin 2 --sizeMax 64
field string --fieldName description --notNull --sizeMin 2 --sizeMax 255
field number --fieldName estimatedHours --notNull --type java.lang.Integer
// :::: create Hours domain object ::::
entity --class com.healthline.ktm.domain.Hours
field date --fieldName recordedDate --type java.util.Date
field number --fieldName actualHours --type java.lang.Integer
// :::: create Allocations domain object ::::
entity --class com.healthline.ktm.domain.Allocations 
field date --fieldName recordedDate --type java.util.Date
field number --fieldName percentAllocated --type java.lang.Integer \
    --notNull --min 1 --max 100
// :::: Associations ::::
// :::: Project --(1:m)--> Item ::::
field set --class com.healthline.ktm.domain.Project --fieldName items \
    --cardinality ONE_TO_MANY --element com.healthline.ktm.domain.Item
field reference --class com.healthline.ktm.domain.Item --fieldName project \
    --type com.healthline.ktm.domain.Project
// :::: Item --(1:m)--> Task ::::
field set --class com.healthline.ktm.domain.Item --fieldName tasks \
    --element com.healthline.ktm.domain.Task
field reference --class com.healthline.ktm.domain.Task --fieldName item \
    --type com.healthline.ktm.domain.Item
// :::: Task --(1:m)--> Hours ::::
field set --class com.healthline.ktm.domain.Task --fieldName hours \
    --cardinality ONE_TO_MANY --element com.healthline.ktm.domain.Hours
field reference --class com.healthline.ktm.domain.Hours --fieldName task \
    --type com.healthline.ktm.domain.Task
// ::: Person --(1:m)-->Task ::::
field set --class com.healthline.ktm.domain.Person --fieldName tasks \
    --cardinality ONE_TO_MANY --element com.healthline.ktm.domain.Task
field reference --class com.healthline.ktm.domain.Task --fieldName person \
    --type com.healthline.ktm.domain.Person
// :::: Project --(m:m)--> Person ::::
// :::: Since we need to hold data for each relation, we model this  ::::
// :::: as a pair of (1:m) relations from each entity to Allocations ::::
field set --class com.healthline.ktm.domain.Project --fieldName allocations \
    --cardinality ONE_TO_MANY --element com.healthline.ktm.domain.Allocation
field reference --class com.healthline.ktm.domain.Allocations \
    --fieldName project --type com.healthline.ktm.domain.Project
field set --class com.healthline.ktm.domain.Person --fieldName allocations \
    --cardinality ONE_TO_MANY --element com.healthline.ktm.domain.Allocations
field reference --class com.healthline.ktm.domain.Allocations \
    --fieldName person --type com.healthline.ktm.domain.Person
// :::: forcibly create the database. Not sure if this is needed but ::::
// :::: this was what I did after a lot of changes - drop database,  ::::
// :::: recreate and run this                                        ::::
perform tests
// :::: build the web layer ::::
controller all --package com.healthline.ktm.web
quit

I also configured the Jetty plugin in the generated POM to listen on port 8888 and to log using the generated log4j.properties file. Here is the snippet of the XML.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  <build>
    ...
    <plugins>
      ...
      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>maven-jetty-plugin</artifactId>
        <version>6.1.10</version>
        <configuration>
          <scanIntervalSeconds>10</scanIntervalSeconds>
          <systemProperties>
            <systemProperty>
              <name>log4j.configuration</name>
              <value>file:./src/main/resources/META-INF/spring/log4j.properties</value>
            </systemProperty>
            <systemProperty>
              <name>jetty.port</name>
              <value>8888</value>
            </systemProperty>
          </systemProperties>
        </configuration>
      </plugin>
      ...
    </plugins>
  </build>

Running mvn jetty:run command at command line on the project root directory brings up the server and I get this page at localhost:8888/ktm on my browser. I also navigated through to the other pages and things look functional.

 

If you decide to enter data that you don't want to disappear after a server restart, you should change the database update mode from "create" to "update" in persistence.xml.

Now what?

Running the script above created 300+ files - it took me about 6 hours to do this - this includes learning Roo, building a Pygments parser for Roo (so I can present a nicely colorized script on this page), designing the object model and writing and refining the script. So it appears that learning Roo is definitely time well-spent :-).

However, we are far from done. There is some basic customization that needs to be done to reorder the entities on the pages, add appropriate finders, add customizations (company logo, etc), and to build in security. I also have to figure out how to add my own controllers into the Roo app. I plan to do these in the coming weeks, and if its worth writing about, to write about it as well.