Tapestry - Part III: Input Validation

Introduction

In previous blogs we showed how to setup a simple “hello world” web application using Tapestry including an API. One of the issues we ran into is that some names are reserved in the application for page names, so they can’t be used as input names for the application. Here we will show how the input can be filtered and meaningful error messages can be created for the user. This approach allows you to create a better user experience and safer applications.

Prerequisites

Setup

Grab the code hello_tapestry_api from the previous blog and use it as starting point. Import that project into Eclipse and run it with the following configuration:

run configuration - main

and

run configuration - JRE

If everything is configured properly, you will get the basic hello-world page:

final result

(If you are running a JDK < 1.8 you should also add -XX:MaxPermSize=256M to JRE config.).

If you enter API as name you are pointed to a placeholder page. This page can be used to give information about the API for the web application. You are not meant to end up here when entering a name. In the next step we will catch the reserved name and give the user a message.

Adding form validation

The first thing we need to do is create a stop on the webpage for the errors to be rendered. This can be done in the form mix-in in the tml file by adding

<t:errors/>

The complete index.tml will look like this:

<html t:type="layout" title="Hello Index"
      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">

	<t:if test="result" >
	 	 <span id="result"> 	 
			<h1>Hello ....</h1>
			<br></br>
			<p>${nameOfUser}</p>
		</span>

	    <else>
		</else>
      </t:if>

	<t:form t:id="inputform">
	 	<t:errors/>
	 	Name :<t:textfield t:id="nameOfUser"/> 
    </t:form>    
</html>

Now the page is setup we need to get a reference to the form so we can set an error message if needed. Up till now we let Tapestry do this for us, but now we make the name and variable explicit by adding a member variable to the index class. This is achieved by using

@Component(id = "inputform")
private Form inputform;

 

Tapestry has a method that validates all the components on a page called onValidate(). We can now call this method to validate the page and its embedded form. To dubbel check that we have injected the inputform correctly we also check for null.

void onValidate()
{
  logger.info("validating user input form");
  Boolean check = inputform == null;
  logger.info(check.toString());
  inputform.recordError(nameOfUserField, "Invalid name.");
}

This onValidate will always give error and is only for illustration.

invalid name

Adding real checks

We expect all names to be consisting of characters only. To enforce this rule we check

private Boolean hasOnlyCharacters(String stringToCheck)
{
   return stringToCheck.matches("[a-zA-Z]+");
}

In a previous blog we found an issue that some valid names are used by the applications. These names give unexpected and potentially dangerous side effects. We fix this by validating the input.

  private Boolean isReservedName(String stringToCheck)
  {
	  String[] pages = {"Index", "Api", "Error404"};
	  
	  if(stringToCheck == null)
		return false;
	  
	  Boolean isPageName = false;
	  for(String page : pages)
	  {
		  if (page.compareToIgnoreCase(stringToCheck) == 0)
			  isPageName = true;
	  }
	  
	  return isPageName;
  }
}

Note that does introduce a little bit of duplication. The list of page names is already available in Layout object. To solve this we have to create a page manager service and inject it into the Index page. This we’ll leave for another blog for now.

To use the validation methods we need to rewrite the onValidate() method to

@Log 
void onValidate() 
 { 
       logger.info("validating user input form : " + nameOfUser); 
       inputform.clearErrors(); 
       Boolean hasOnlyCharacters = hasOnlyCharacters(nameOfUser); 
       
       if(!hasOnlyCharacters) 
             inputform.recordError("A name should consist of characters only."); 
       if(isReserverdName(nameOfUser)) 
             inputform.recordError("This is a reserverd word that can't be used as name.");
 }

This gives us:

reserved name

If you are setting up project with multiple input text fields it might be good to use form.recordError(textField, errorMessage). This will give the message in the box above the form, but also under the text field. This is very useful for larger forms. Using this will give you result like this

Characters only

Finally, if the input is not valid you probably don’t want to show the result box as the result is in a dubious state. To hide the box we have to set the result member variable to false. We do this when the page is rendered in the setupRender() method.

 void setupRender()
 {
	 if (nameOfUser != null)
	 {
		 result &= hasOnlyCharacters(nameOfUser);
		 result &= !isReservedName(nameOfUser);
	 }
 }

Code

The code is available here : hello_tapestry_validation

Complete listing of Index.java

package nl.uglyduckling.hello.pages;

import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.Log;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.corelib.components.TextField;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.slf4j.Logger;


/**
 * Start page of application hello.
 */
public class Index
{
  @Inject
  private Logger logger;

  @Property
  private String nameOfUser;
  
  @Property
  private Boolean result;
  
  @Component(id = "nameOfUser")
  private TextField nameOfUserField;

  @Component(id = "inputform")
  private Form inputform;

 @Log
 void onActivate(String nameOfUser)
 {
	logger.info(nameOfUser);
	this.nameOfUser = nameOfUser;
	result = isNameResult(nameOfUser);
 }
 
 void setupRender()
 {
	 if (nameOfUser != null)
	 {
		 result &= hasOnlyCharacters(nameOfUser);
		 result &= !isReservedName(nameOfUser);
	 }
 }

  @Log
  Object[] onPassivate() {
	  logger.info(nameOfUser);
      return new String[] { nameOfUser };
  }

  @Log
  void onValidate()
  {
	  logger.info("validating user input form : " + nameOfUser);
	  inputform.clearErrors();
	  Boolean hasOnlyCharacters = hasOnlyCharacters(nameOfUser);
	  
	  if(!hasOnlyCharacters)
		  inputform.recordError("A name should consist of characters only.");

	  if(isReservedName(nameOfUser))
	    inputform.recordError("This is a reserverd word that can't be used as name.");
  }
  
  private Boolean isNameResult(String nameToCheck)
  {
	  return nameToCheck != null && nameToCheck.length() > 0;
  }
  
  private Boolean hasOnlyCharacters(String stringToCheck)
  {
	  return stringToCheck != null && stringToCheck.matches("[a-zA-Z]+");
  }
  
  private Boolean isReservedName(String stringToCheck)
  {
	  String[] pages = {"Index", "Api", "Error404"};
	  
	  if(stringToCheck == null)
		return false;
	  
	  Boolean isPageName = false;
	  for(String page : pages)
	  {
		  if (page.compareToIgnoreCase(stringToCheck) == 0)
			  isPageName = true;
	  }
	  
	  return isPageName;
  }
}

Leave a Reply

Your email address will not be published. Required fields are marked *