Tapestry Annotations

As part of a recent prototyping effort, BC and I plunged head first into the world of annotations. Part of this exercise involved migrating some Tapestry pages from using .page files to Tapestry annotations.

We struggled a bit with this so we thought it worthy of a discussion here in order to share our pain.

We were working on a simple login page for a portal. The page needed only to get a username and password and post it to the server. Simple right? Well it was when we were working with the java class/page file/html template mechanism. It was when we tried to replace the .pagefile with annotations that things started to become painful.

Here’s the original – functioning – page file:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE page-specification PUBLIC
  "-//Apache Software Foundation//Tapestry Specification 4.0//EN"
  "http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd"
  >

<page-specification class="ca.intelliware.example.page.Login" >
  <description>Login</description>

  <inject property="userContext" type="state" object="userContext"/>
  <inject property="authenticationService" object="spring:ca.intelliware.example.service.AuthenticationService" />
  <bean name="validationDelegate" class="org.apache.tapestry.valid.ValidationDelegate"/>

  <!-- form fields -->

  <component id="loginForm" type="Form">
    <binding name="success" value="listener:login"/>
  </component>

  <component id="username" type="TextField">
    <binding name="value" value="ognl:username"/>
    <binding name="displayName" value="literal:Username"/>
  </component>

  <component id="password" type="TextField">
    <binding name="value" value="password"/>
    <binding name="displayName" value="literal:Password"/>
    <binding name="hidden" value="true"/>
  </component>

</page-specification>

As Tapestry neophytes we were pretty damned excited to get this working. Our java class at the time looked like this:

import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.event.pageEvent;
import org.apache.tapestry.html.BasePage;
import org.apache.tapestry.valid.ValidationDelegate;

import ca.intelliware.example.model.User;
import ca.intelliware.example.model.UserContext;
import ca.intelliware.example.service.AuthenticationService;

public abstract class Login extends BasePage {

  public abstract String getUsername();
  public abstract void setUsername(String username);

  public abstract String getPassword();
  public abstract void setPassword(String password);

  public abstract AuthenticationService getAuthenticationService();

  public abstract UserContext getUserContext();
  public abstract void setUserContext(UserContext userContext);

  public abstract ValidationDelegate getValidationDelegate();
  public abstract void setValidationDelegate(ValidationDelegate validationDelegate);

  public void login(IRequestCycle cycle) {
    getUserContext().setAttemptedLoginUserId(getUsername());

    AuthenticationService service = getAuthenticationService();

    User user = service.authenticate(getUsername(), getPassword());
    if (user == null) {
      setPassword(null);
    } else {
      getUserContext().setCurrentUser(user);
      cycle.sendRedirect("Homepage.html");
    }
  }

  public void pageBeginRender(PageEvent event) {
    if(event.getRequestCycle().getParameter("error") != null) {
      ValidationDelegate delegate = getValidationDelegate();
      delegate.setFormComponent(null);
      delegate.record("Login failed", null);
      setUsername(getUserContext().getAttemptedLoginUserId());
    }
  }
}

We started by deleting the .page file and annotating the java class. The first problem we ran into was that without a .pagefile Tapestry couldn’t figure out the mapping between the html template and the java class. The error messages didn’t clearly indicate that our Login class wasn’t being found. Tapestry was happily trying to run without our java class by using the default BasePage class. This was rather frustrating.

The solution was to create a Tapestry application file:

<?xml version="1.0"?>
<!DOCTYPE application PUBLIC
  "-//Apache Software Foundation//Tapestry Specification 4.0//EN"
  "http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd">
<application>
  <meta key="org.apache.tapestry.page-class-packages" value="ca.intelliware.example.page" />
</application>

Note: We saved this file as “/WEB-INF/app.application”; the “app” part refers to the servlet-name of the Tapestry servlet in our web.xml. This file magically gets picked up when the org.apache.tapestry.ApplicationServletinitializes.

The real problem we ran into was having to add abstract methods for the TextField components for the username and password. Part of the problem was that we used the same names for our bean properties (username and password) as for our jwcids. That confused us.

When we first tried to get the annotations working we tried this approach:

@Component (type="TextField", bindings={ "value=ognl:username", ...})
  public abstract String getUsername();
  public abstract void setUsername(String username);
  ...

The error messages seemed to indicate that our method signature should be changed to this:

@Component (type="TextField", bindings={ "value=ognl:username", ...})
  public abstract TextField getUsername();
  public abstract void setUsername(TextField username);
  ...

Then it complained that we had a setter method so we removed that.
The next problem we were faced with was the ognl value. It was displaying not the value of the username but the toString() output of the TextField – (e.g. “@TextField13254345354”).

We then tried to directly access the value of the TextField in the ognl directive like this:

value=ognl:username.value

Tapestry was having none of that!

Next we tried to create a derived property:

public String getUsernameValue() {
  return (String)getUsername().getValue();
}

Nope. That didn’t work either.
It was at this point that the error messages became incomprehensible. We went for help. Kevin was able to show us that the actual error was a StackOverflow due to an infinite loop and explained to us that what our code was actually doing was recursively calling the TextField constructor.

The solution was to treat the bean component (e.g. username) and the corresponding TextField (e.g. usernameTextField) separately. Here’s the final java class:

package ca.intelliware.example.page;

import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.RedirectException;
import org.apache.tapestry.annotations.Bean;
import org.apache.tapestry.annotations.Component;
import org.apache.tapestry.annotations.InjectObject;
import org.apache.tapestry.event.pageEvent;
import org.apache.tapestry.form.Form;
import org.apache.tapestry.form.TextField;
import org.apache.tapestry.valid.ValidationDelegate;

import ca.intelliware.example.model.User;
import ca.intelliware.example.service.AuthenticationService;

public abstract class Login extends UserContextPage {

  @Component(id = "loginForm", type = "Form", bindings = { "success=listener:login" })
  public abstract Form getLoginForm();

  @Component(type = "TextField", bindings = { "value=ognl:username", "displayName=literal:Username" })
  public abstract TextField getUsernameTextField();

  @Component(type = "TextField", bindings = { "value=ognl:password", "displayName=literal:Password", "hidden=true" })
  public abstract TextField getPasswordTextField();

  @InjectObject("spring:ca.intelliware.example.service.AuthenticationService")
  public abstract AuthenticationService getAuthenticationService();

  @Bean
  public abstract ValidationDelegate getValidationDelegate();

  public abstract String getUsername();
  public abstract void setUsername(String username);
  public abstract String getPassword();
  public abstract void setPassword(String password);

  public void login(IRequestCycle cycle) {
    getUserContext().setAttemptedLoginUserId(getUsername());

    AuthenticationService service = getAuthenticationService();

    User user = service.authenticate(getUsername(), getPassword());
    if (user != null) {
      getUserContext().setCurrentUser(user);
      throw new RedirectException("Homepage.html");
    } else {
      setPassword("");
    }
  }

  public void pageBeginRender(PageEvent event) {
    if(event.getRequestCycle().getParameter("error") != null) {
      ValidationDelegate delegate = getValidationDelegate();
      delegate.setFormComponent(null);
      delegate.record("Login failed", null);
    }
  }
}

The result: It was nice to be able to get rid of the .page file but it was a little annoying to have what seems like duplication as far as the username and password components/bean values were concerned. Interestingly enough Kevin’s team made a conscious decision not to use annotations for precisely this reason.

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

Leave a Reply