2012年12月17日星期一

how to use ActionBar Sherlock

http://androidfragments.blogspot.com/2011/10/how-to-use-actionbar-sherlock.html

How to use ActionBar Sherlock



Note: ActionBarSherlock has changed significantly since the time of this posting. We will update this post as soon as we can, but in the mean time, please refer to the official documentation at http://actionbarsherlock.com/usage.html



The Android ActionBar is a new widget that replaces the title bar, provides a location for menus and buttons, and provides a source of commonality between Android apps. The Action Bar is included by default in all activities that target Android 3.0 or greater, but in order to get the same effect in earlier versions of android, you were stuck writing a custom solution. ActionBar Sherlock is a nice library that allows you to solve this problem by letting you add ActionBars to your apps all the way back to Android 1.6. When used on Honeycomb and later it uses the default Android classes but on earlier versions it provides a custom replacement that still maintains compatibility with the newer API.

Here's a run down on how to add the library to your project. Note that since ActionBar Sherlock is an extension of the compatibility support library, you should remove the combatibility support jar if you were using it before since it is included with ActionBar Sherlock.

  1. Download the .zip/.tgz and extract it somewhere
  2. Go to eclipse and choose File->New->Project
  3. Choose Android Project
  4. Select Create project from existing source and then browse to the library folder inside the folder you just extracted the .zip/.tgz into
  5. Build Target should be the latest, but your minSdkVersion can be less
  6. Finish the wizard, then right click on the newly created project and go to properties
  7. Under the Android heading, you should see a section for Library with a checkbox IsLibrary. Make sure that's checked.
  8. Now go to the properties for your Android project, then under the Android heading and the Library section choose Add...
  9. You should see the actionbarsherlock library, add this to your project
  10. Lastly, if you were using the compatibility support, you need to delete that jar since it's included in ActionBarSherlock After you have added the library to your project, you will need to take some additional steps to ensure it is being used.

1. Add a theme to your application in the AndroidManifest.xml

?
<application android:icon="@drawable/ic_launcher" android:label="@string/app_name"
android:theme="@style/Theme.Sherlock">


2. Make sure that your activity extends FragmentActivity

?
public class AbbreviationsActivity extends FragmentActivity {


Now you're done! If you want to interact with your ActionBar through Java, make sure you use getSupportActionBar() instead of the standard getActionBar() to get the ActionBar Sherlock version of the action bar. The ActionBar Sherlock website along with the Android docs has information about theming, adding menus and buttons, and changing text and icons. Enjoy!

2012年12月6日星期四

fw: get current username in Spring security

http://www.mkyong.com/spring-security/get-current-logged-in-username-in-spring-security/

In this article, we show you three ways to get current logged in username in Spring Security.

1. SecurityContextHolder + Authentication.getName()

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
@Controller
public class LoginController {
 
  @RequestMapping(value="/login", method = RequestMethod.GET)
  public String printUser(ModelMap model) {
 
      Authentication auth = SecurityContextHolder.getContext().getAuthentication();
      String name = auth.getName(); //get logged in username
 
      model.addAttribute("username", name);
      return "hello";
 
  }
  //...

2. SecurityContextHolder + User.getUsername()

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
@Controller
public class LoginController {
 
  @RequestMapping(value="/login", method = RequestMethod.GET)
  public String printUser(ModelMap model) {
 
      User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
      String name = user.getUsername(); //get logged in username
 
      model.addAttribute("username", name);
      return "hello";
 
  }
  //...

3. UsernamePasswordAuthenticationToken

This is more elegant solution, in runtime, Spring will inject “UsernamePasswordAuthenticationToken” into the “Principal” interface.
import java.security.Principal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
@Controller
public class LoginController {
 
  @RequestMapping(value="/login", method = RequestMethod.GET)
  public String printWelcome(ModelMap model, Principal principal ) {
 
      String name = principal.getName(); //get logged in username
      model.addAttribute("username", name);
      return "hello";
 
  }
  //...

Download Source Code

Custom Roo login

http://sujitpal.blogspot.com/2010/07/ktm-customizing-roo-security.html

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.