четверг, 16 мая 2013 г.

Part 1. Spring Roo + JSF + Spring Security: New user registration and reCaptcha


This is an english translation of my post in Russian.
This post will be useful for Spring Roo beginners, like me. I highly appreciate your comments and advices.

New user registration is a coomon task for the most of web applications. Despite this fact, nor current Roo version, nor "parent" Spring Security, support or scaffold user registration form. Most tutorials also ignore it.

Another problem arise when you choose to scaffold JSF web layer instead of standard MVC. In the Spring Roo, after web jsf setup command run, the security setup command becames automatically disabled.  I will consider manual security configuration in the Part 2 of the serie, now let's do the user registration interface.

I use 64-bit STS 3.2.0 under Ubuntu 12.04. Project script looks as follows:

project --topLevelPackage org.test.sec2 --projectName test.sec2 --java 7 --packaging JAR
jpa setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY
entity jpa --class ~.domain.Person --activeRecord false --equals --testAutomatically

field string --fieldName login --sizeMax 25 --notNull

field string --fieldName password --sizeMax 25 --notNull

repository jpa --interface ~.domain.PersonRepository --entity ~.domain.Person

web jsf setup --theme CASABLANCA

web jsf all --package ~.web
When you create your project in the STS via Dashboard or menu, the first command will be run by STS/Roo itself, using the project and package names you entered. Then you should start from the jpa setup in the STS Roo console, opened for you brand new project.

Alternatevely, you can use CLI Roo interface and run this script as a whole, using script command. After it, open STS and import the project as Existing Maven project.

At the moment you have ready to use web application, configured to use Hipersonic-in-memory database engine and ORM Hibernate. Database contains a table Person with two fields: login and password. User interface is scaffolded using JSF Primefaces.

Now, run the project, it should work. Try to create users.

When you create a user, you surely noticed that:
1. The field "password" is entered in plain text, not aisterics.
2. There is absent a password confirmation field.
3. There is absen a protection from bots - a captcha.

So, let's fix it.

1.Preaarangements: push-in refactor of the public HtmlPanelGrid populateCreatePanel() method

If you look carefully at src/main/webapp/pages/person.xhtml, you'll realise that dialog window you played with when created users, is generated dynamically.

To see this code we need to access a particular Roo-generated file with declarations. But, for convinience and protection, STS/Roo developers hide these files from us. That is why before the next paragraph reading, in the Package Explorer view, please press down-oriented small grey triangle, and in the dropped menu remove the tickmark next to the Hide generated Spring Roo ITD's item.

Now, when you look at the code of appeared /test.sec2/src/main/java/org/test/sec2/web/PersonBean_Roo_ManagedBean.aj file, you'll realise that public HtmlPanelGrid populateCreatePanel() methos is responsible for appearance of the create dialog fields.

A warning at the beginning of any Roo-generated *.aj files say:
// WARNING: DO NOT EDIT THIS FILE. THIS FILE IS MANAGED BY SPRING ROO.
Because when our project is updated or even Roo version is changed, these files can be changed wothout any warnings and any customisation will be lost.

That is why we'll move this method into the class itself: /test.sec2/src/main/java/org/test/sec2/web/PersonBean.java.
In the Outline view expand PersonBean_Roo_ManagedBean.aj node, click raght button on populateCreatePanel method and select Refactor->Push In... from drop-down, then point to PersonBean.java class.

Open PersonBean.java, it must looks as follows:
package org.test.sec2.web;

import javax.el.ELContext;
...

@RooSerializable
@RooJsfManagedBean(entity = Person.class, beanName = "personBean")
public class PersonBean {

    public HtmlPanelGrid populateCreatePanel() {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        Application application = facesContext.getApplication();
        ExpressionFactory expressionFactory = application.getExpressionFactory();
        ELContext elContext = facesContext.getELContext();
       
        HtmlPanelGrid htmlPanelGrid = (HtmlPanelGrid) application.createComponent(HtmlPanelGrid.COMPONENT_TYPE);
         .......
2. Aisterics and password==confirmation

Let's analyse populateCreatePanel method. For every Person's entity field there are three elements created: label OutputLabel, text input InputText and error message pop-up window Message.

On the other side, Primefaces has excellent password tag with asterics and passwords matching functionality (the only thing you should do to let Primefaces check matching automatically is to set match parameter in the first password tag).

So, insert the following code instead of the current three "paragraphs" for the password field:

OutputLabel passwordCreateOutput = (OutputLabel) application.createComponent(OutputLabel.COMPONENT_TYPE);
        passwordCreateOutput.setFor("passwordCreateInput");
        passwordCreateOutput.setId("passwordCreateOutput");
        passwordCreateOutput.setValue("Password:");
        htmlPanelGrid.getChildren().add(passwordCreateOutput);
               
        Password passwordCreateInput = (Password) application.createComponent(Password.COMPONENT_TYPE);
        passwordCreateInput.setId("passwordCreateInput");
        passwordCreateInput.setValueExpression("value", expressionFactory.createValueExpression(elContext, "#{personBean.person.password}", String.class));
        LengthValidator passwordCreateInputValidator = new LengthValidator();
        passwordCreateInputValidator.setMaximum(25);
        passwordCreateInput.addValidator(passwordCreateInputValidator);
        passwordCreateInput.setRequired(true);
        passwordCreateInput.setMatch("passwordCreateInput2");
        htmlPanelGrid.getChildren().add(passwordCreateInput);
       
        Message passwordCreateInputMessage = (Message) application.createComponent(Message.COMPONENT_TYPE);
        passwordCreateInputMessage.setId("passwordCreateInputMessage");
        passwordCreateInputMessage.setFor("passwordCreateInput");
        passwordCreateInputMessage.setDisplay("icon");
        htmlPanelGrid.getChildren().add(passwordCreateInputMessage);
       
        OutputLabel passwordCreateOutput2 = (OutputLabel) application.createComponent(OutputLabel.COMPONENT_TYPE);
        passwordCreateOutput2.setFor("passwordCreateInput2");
        passwordCreateOutput2.setId("passwordCreateOutput2");
        passwordCreateOutput2.setValue("Password Again:");
        htmlPanelGrid.getChildren().add(passwordCreateOutput2);
       
        Password passwordCreateInput2 = (Password) application.createComponent(Password.COMPONENT_TYPE);
        passwordCreateInput2.setId("passwordCreateInput2");
        passwordCreateInput2.setValueExpression("value", expressionFactory.createValueExpression(elContext, "#{personBean.person.password}", String.class));
        LengthValidator passwordCreateInputValidator2 = new LengthValidator();
        passwordCreateInputValidator2.setMaximum(25);
        passwordCreateInput2.addValidator(passwordCreateInputValidator);
        passwordCreateInput2.setRequired(true);
        htmlPanelGrid.getChildren().add(passwordCreateInput2);
       
        Message passwordCreateInputMessage2 = (Message) application.createComponent(Message.COMPONENT_TYPE);
        passwordCreateInputMessage2.setId("passwordCreateInputMessage2");
        passwordCreateInputMessage2.setFor("passwordCreateInput2");
        passwordCreateInputMessage2.setDisplay("icon");
        htmlPanelGrid.getChildren().add(passwordCreateInputMessage2);
Save the class and reload the page in browser, check that you get error message when passwords mismatch.

3. Captcha

Say thanx to Primefaces developers. There is a tag for de-facto standard captcha - reCaptcha from Google.

To make reCaptcha working you need obtain public and private keys. Register or login in the Google reCaptcha service and take keys for the localhost server.

Put them into /test.sec2/src/main/webapp/WEB-INF/web.xml as follows:

    <context-param>
        <param-name>primefaces.PRIVATE_CAPTCHA_KEY</param-name>
        <param-value>your private key</param-value>
    </context-param>
    <context-param>
        <param-name>primefaces.PUBLIC_CAPTCHA_KEY</param-name>
        <param-value>your public key</param-value>
    </context-param>

Now we need some witchcraft. Find the dialog createForm in /test.sec2/src/main/webapp/pages/person.xhtml. You must delete here dynamic="true" in the <p:dialog>  tag. Otherwise reCaptcha won't work because, as guru say, p:dialog is the pain in ass.

After voodoo, insert a little bit customised captcha using standard <p:captcha ....> tag. To have user-friendly error messages, provide label parameter, otherwise message shown will be from j_026, what can make user a little bit frustrated.

Finally, the dialog should look as following:

<p:dialog id="createDialog" header="#{messages.label_create} Person" modal="true" widgetVar="createDialogWidget" dynamic="true" visible="#{personBean.createDialogVisible}" resizable="true" maximizable="true" showEffect="fade" hideEffect="explode">
      <p:ajax event="close" update=":dataForm:data" listener="#{personBean.handleDialogClose}" />
      <p:outputPanel id="createPanel">
        <h:form id="createForm" enctype="multipart/form-data">
          <h:panelGrid id="createPanelGrid" columns="3" binding="#{personBean.createPanelGrid}" styleClass="dialog" columnClasses="col1,col2,col3" />
         
          <p:captcha label="reCaptcha" theme="white"/>
         
          <p:commandButton id="createSaveButton" value="#{messages.label_save}" action="#{personBean.persist}" update="createPanelGrid :growlForm:growl" />
          <p:commandButton id="createCloseButton" value="#{messages.label_close}" onclick="createDialogWidget.hide()" type="button" />
        </h:form>
      </p:outputPanel>
    </p:dialog>


Voila!