Apache Wicket Cookbook

By Igor Vaynberg
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Validating and Converting User Input

About this book

Apache Wicket is one of the most famous Java web application frameworks. Wicket simplifies web development and makes it fun. Are you bored of going through countless pages of theory to find out how to get your web development done? With this book in hand, you don't need to go through hundreds of pages to figure out how you will actually build a web application. You will get practical solutions to your common everyday development tasks to pace up your development activities.

Apache Wicket Cookbook provides you with information that gets your problems solved quickly without beating around the bush. This book is perfect for you if you are ready to take the next step from tutorials and step into the practical world. It will take you beyond the basics of using Apache Wicket and show you how to leverage Wicket's advanced features to create simpler and more maintainable solutions to what at first may seem complex problems.

You will learn how to integrate with client-side technologies such as JavaScript libraries or Flash components, which will help you to build your application faster. You will discover how to use Wicket paradigms to factor out commonly used code into custom Components, which will reduce the maintenance cost of your application, and how to leverage the existing Wicket Components to make your own code simpler.

Publication date:
March 2011
Publisher
Packt
Pages
312
ISBN
9781849511605

 

Chapter 1. Validating and Converting User Input

In this chapter, we will cover:

  • Performing form-level custom validation

  • Creating a custom validator

  • Validating unique values like a username or an e-mail address

  • Composing multiple validators into a single reusable validator

  • Converting string inputs to objects

 

Introduction


In this chapter, you will learn how to validate user input inside forms. You will learn how to handle validation constraints that involve more than one field as well as how to create custom validators for individual fields. You will also learn how to convert users' input from strings into Java objects.

 

Performing form-level custom validation


Frequently, complex forms contain dependencies between various fields that need to be validated in a dynamic manner. For example, the value of one field needs to be validated based on rules that vary with the value of another field.

Let's take a look at a search form where the keywords textfield needs to be validated with rules dictated by the selection in the search type drop-down box:

We would like the keywords field to be validated as a zip code or a phone number based on what is selected in the dropdown list.

Getting ready

We will get started by creating the Search Customers form without validation.

Create the page:

HomePage.html

<html>
  <body>
      <h1>Search Customers</h1>
      <div wicket:id="feedback"></div>
        <form wicket:id="form">
          <select wicket:id="type"></select>
          <input wicket:id="keywords" type="text" size="20"/>
          <input type="submit" value="Search"/>
        </form>
    </body>
</html>

HomePage.java

public class HomePage extends WebPage {

  private static final String ZIPCODE = "ZIPCODE";
  private static final String PHONE = "PHONE";
  private static final List<String> TYPES = Arrays.asList(new String[] {
      ZIPCODE, PHONE });

  public HomePage(final PageParameters parameters) {
    add(new FeedbackPanel("feedback"));
    final DropDownChoice<String> type = new DropDownChoice<String>("type",
        new Model<String>(ZIPCODE), TYPES);
    type.setRequired(true);

    final TextField<String> keywords = new TextField<String>("keywords",
        new Model<String>());
    keywords.setRequired(true);

    Form< ? > form = new Form<Void>("form") {
       @Override
      protected void onSubmit() {
        info("Form successfully submitted");
      }
    }
    add(form);
    form.add(type);
    form.add(keywords);
  }
}

Tip

Downloading the example code

You can download the example code files for all Packt books you have purchased from your account at http://www.PacktPub.com. If you purchased this book elsewhere, you can visit http://www.PacktPub.com/ support and register to have the files e-mailed directly to you.

How to do it...

We are going to implement our validation by overriding Form#onValidate() method and putting our validation logic there.

  1. Implement validation logic:

     Form< ? > form = new Form<Void>("form") {
           @Override
          protected void onSubmit() {
            info("Form successfully submitted");
          }
        @Override
          protected void onValidate() {
            super.onValidate();
    
            if (hasError()) {
              return;
            }
    
            final String selectedType = type.getConvertedInput();
            final String query = keywords.getConvertedInput();
            if (ZIPCODE.equals(selectedType)) {
              if (!Pattern.matches("[0-9]{5}", query)) {
                keywords.error((IValidationError)new ValidationError()
                    .addMessageKey("invalidZipcode"));
              }
            }
            else if (PHONE.equals(selectedType)) {
              if (!Pattern.matches("[0-9]{10}", query)) {
                keywords.error((IValidationError)new ValidationError()
                    .addMessageKey("invalidPhone"));
              }
            }
          }
        };
    
        add(form);
        form.add(type);
        form.add(keywords);
      }
    }
  2. Provide definitions for the error message resources used to construct error messages:

    HomePage.properties
    invalidZipcode=Invalid Zipcode
    invalidPhone=Invalid Phone Number

How it works...

Wicket provides ample opportunity for developers to interact with its form processing work-flow. In this particular instance, we use the Form#onValidate() callback to insert our own validation logic. This callback will be invoked after the form has validated all fields and form validators.

The first thing we do is check if the form contains any errors so far, and if it does, skip our validation. We do this by checking the return value of the form's Form#hasError() method:

protected void onValidate() {
  if (hasError()) {
  return;
}

Next, we retrieve the submitted values of the search type and keywords fields:

final String selectedType = type.getConvertedInput();
final String query = keywords.getConvertedInput();

Note

Notice that we use FormComponent#getConvertedInput() method instead of accessing the field's value via its model. We do this because we are still in the validation stage of the form's work-flow and so the models of form components have not yet been updated.

Finally, we check the values and report any errors using the FormComponent#error(IValidationError)FormComponent#error(IValidataionError) method:

if (ZIPCODE.equals(selectedType)) {
  if (!Pattern.matches("[0-9]{5}", query)) {
    keywords.error((IValidationError)new ValidationError()
      .addMessageKey("invalidZipcode"));
  }
}

There's more...

Wicket offers developers more than one place to plug-into form validation workflow. Here we take a look at an alternative.

Making validation logic reusable

If the inter-field constraints being validated will be used on multiple fields across multiple pages and forms, the validation logic can be externalized into Wicket's org.apache.wicket.markup.html.form.validation.IFormValidator.

See also

  • See the Creating a custom validator recipe in this chapter for how to validate individual fields.

  • See the Composing multiple form components into a single reusable component recipe for how to combine form components

 

Creating a custom validator


While Wicket provides a lot of built-in validators to help us with various common constraints, more often than not, we find ourselves needing to perform validation based on some business rules. For this reason, Wicket makes it very easy to implement a custom validation by creating a validator that can be added to form fields.

One example of a custom validation that most web applications will have is a password policy. Outside of the password length itself, which can be validated using Wicket's StringValidator, most applications also require the password to meet other security requirements that would make it more secure.

Let's build a user registration form which enforces the following password requirements:

  • Password must be at least eight characters long

  • Password must contain at least one lower case letter

  • Password must contain at least one upper case letter

  • Password must contain at least one digit

Getting ready

Let's get started by creating the form without password validation:

  1. Create the web page:

    HomePage.html

    <html>
      <body>
      <h1>User Registration</h1>
      <div wicket:id="feedback"></div>
        <form wicket:id="form">
          <p><label>Username</label>: <input wicket:id="username" type="text" size="20"/></p>
          <p><label>Password</label>: <input wicket:id="password1" type="password" size="20"/></p>
          <p><label>Confirm Password</label>: <input wicket:id="password2" type="password" size="20"/></p>
          <input type="submit" value="Register"/>
        </form>
      </body>
    </html>

    HomePage.java

    public class HomePage extends WebPage {
      public HomePage(final PageParameters parameters) {
        add(new FeedbackPanel("feedback"));
     
        TextField< ? > username = new TextField<String>("username", Model.of(""));Model.of(""));
        username.setRequired(true);
    
        FormComponent<String> password1 = new PasswordTextField("password1", Model.of(""));

    Note

    You might be wondering what the Model.of("") does. The method simply returns a new instance of Model constructed with the empty string parameter we passed in. Model comes with a few variations of the of() convenience factory methods which help the compiler infer the generic type rather than forcing us to specify it manually: new Model<String>(""), allowing us to write shorter code in the process.

        password1.setLabel(Model.of("Password"));
    
        FormComponent< ? > password2 = new PasswordTextField("password2", Model
            .of(""));
    
        Form< ? > form = new Form<Void>("form") {
          @Override
          protected void onSubmit() {
            info("Form successfully submitted");
          }
        };
    
        form.add(new EqualPasswordInputValidator(password1, password2));
    
        add(form);
        form.add(username);
        form.add(password1);
        form.add(password2);
      }
    }

How to do it...

  1. Create a Wicket field validator:

    PasswordPolicyValidator.java

    public class PasswordPolicyValidator implements IValidator<String> 
    {
    
      private static final Pattern UPPER = Pattern.compile("[A-Z]");
      private static final Pattern LOWER = Pattern.compile("[a-z]");
      private static final Pattern NUMBER = Pattern.compile("[0-9]");
    
      public void validate(IValidatable<String> validatable) {
        final String password = validatable.getValue();
    
      if (!NUMBER.matcher(password).find()) {
        error(validatable, "no-digit");
    	  }
      if (!LOWER.matcher(password).find()) {
        error(validatable, "no-lower");
             }
       if (!UPPER.matcher(password).find()) {
         error(validatable, "no-upper");
        }
      }
    
      private void error(IValidatable<String> validatable, String errorKey) {
        ValidationError error = new ValidationError();
        error.addMessageKey(getClass().getSimpleName() + "." + errorKey);
        validatable.error(error);
      }
    
    }
  2. PasswordPolicyValidator.properties

    PasswordPolicyValidator.no-digit=${label} must contain at least one digit
    PasswordPolicyValidator.no-lower=${label} must contain at least one lower case letter
    PasswordPolicyValidator.no-upper=${label} must contain at least one upper case letter
  3. Add validation to the form:

        FormComponent<String> password1 = new PasswordTextField("password1", Model.of(""));
        password1.setLabel(Model.of("Password"));
        password1.add(StringValidator.minimumLength(8));
        password1.add(new PasswordPolicyValidator());

    Note

    Notice that we are able to implement the minimum length requirement using Wicket's StringValidator.

How it works...

A field validator in Wicket is any class that implements the org.apache.wicket.validation.IValidator interface and is added to the form component that represents a form field. When the form is submitted Wicket will call all the validators added to each form component and give them a chance to inspect the input and report any errors.

We begin by creating a PasswordPolicyValidator class that implements the IValidator interface:

public class PasswordPolicyValidator implements IValidator<String> 
  {

    public void validate(IValidatable<String> validatable) 
      {
      }

  }

When it is time to validate the field, Wicket will call the validate(IValidatable<String> validatable) method on our validator. The passed in validatable parameter is our validator's view into the field that is being validated. It provides a method to retrieve the current value of the field as well as a method to report errors.

The first thing we must do is retrieve the current value of the field. We do this by calling validatable's IValidatable#getValue() method:

final String password = validatable.getValue();

Note

Notice that we do not check if validatable.getValue() returns null; Wicket will check the value prior to calling validators and if it is null, validators will be skipped.

If a validator does want to be called even if the value is null it can implement a tagging interface: org.apache.wicket.validation.INullAcceptingValidator.

Once we have the value, we can perform our validation. In this case, we use a simple regular expression matcher to implement the password policy checks. Whenever we find a policy violation, we call our error(IValidatable,String) helper method and pass in an errorKey parameter to represent the violation:

error(validatable, "no-digit");

The errorKey parameter, in the above example: "no-digit", is used to construct the error message resource key unique to the violation. This is how a single validator can display multiple error messages.

The error(IValidatable,String) method constructs a ValidationError object with the correct error message resource key and passes it to validatable's IValidatable#error(IValidationError) method to report the error:

private void error(IValidatable<String> validatable, String errorKey) 
{
    ValidationError error = new ValidationError();
    error.addMessageKey(getClass().getSimpleName() + "." + errorKey);
    validatable.error(error);
  }

Note

Using the validator's simple class name concatenated with a more specific error key is Wicket's convention for creating resource keys for validation error messages. It is advised that the users follow the same convention when creating custom validators.

If we take a close look at the code for the validator, we will see that we can construct three distinct error key strings, each representing a single password policy violation:

  • PasswordPolicyValidator.no-digit

  • PasswordPolicyValidator.no-lower

  • PasswordPolicyValidator.no-upper

We use these strings as error message resource keys for the message instead of the message itself, so that we can internationalize them if needed. Therefore, we need to put the resource values somewhere where Wicket can find them. By default, one of the places Wicket will check is a property file named with the same name as the validator class that reported the error. This is why in our example we named the property file PasswordPolicyValidator.properties and put it next to PasswordPolicyValidator.java in your package structure.

There's more...

Validators are only as good as the errors they produce. Wicket offers a lot of ways to build the validation message; here we explore ways of customizing error messages to the specifics of the error.

Using Wicket's built-in error variables

In our example we have defined error messages containing: "${label}". The "${label}" string tells wicket to substitute the entire snippet with the value of a variable named "label". Wicket defines the following standard variables:

  • label: The label of the FormComponent, usually set by calling FormComponent#setLabel(IModel)

  • input: The value being validated

  • name: The Wicket id of the FormComponent

Using custom error variables

Users can provide custom variables to be used when error messages are interpolated; new variables can be added using ValidationError#setVariable(String,Object) method. For example, let's create a MaximumStringLengthValidator:

public class MaximumStringLengthValidator implements IValidator<String>
  {
    private final int max;

    public MaximumStringLengthValidator(int max)
      {
        this.max = max;
      }

  public void validate(IValidatable<String> validatable)
    {
    if (validatable.getValue().length() > max)
      {
        ValidationError error = new ValidationError();
        error.addMessageKey(getClass().getSimpleName());
        error.setVariable("max", max);
        validatable.error(error);
      }
    }
  }

As our validator specifies the maximum length by adding the "max" variable, we can construct error messages that reference it, for example:

MaximumStringLengthValidator=${label} must contain a string no longer than ${max} characters.

See also

  • See the Composing multiple validators into a single reusable validator recipe in this chapter on how to compose multiple validators into a single one.

  • See the Storing module resource strings in package properties recipe for an alternative place to store validation messages.

 

Composing multiple validators into a single reusable validator


It is rare that we come across a field constraint as simple as "the length of entered string must be at least two characters" or "the entered number must be between five and ten". More often, we find ourselves in a situation where a validation constraint consists of two or more simpler ones. Often, we already have the validators for the simpler constraints written and all we need is a simple and reusable way to compose them together.

Let's build a user registration form where the username needs to comply with the following constraints:

  • Username must be between 5 and 20 characters long

  • Username must only contain lower-case letters

Getting ready

Let's get started by creating the form above without the validation.

  1. Create the web page:

    HomePage.html

    <html>
      <body>
      <h1>User Registration</h1>
      <div wicket:id="feedback"></div>
        <form wicket:id="form">
          <p><label>Username</label>: <input wicket:id="username" type="text" size="20"/></p>
          <p><label>Password</label>: <input wicket:id="password1" type="password" size="20"/></p>
          <p><label>Confirm Password</label>: <input wicket:id="password2" type="password" size="20"/></p>
          <input type="submit" value="Register"/>
        </form>
      </body>
    </html>
    

    HomePage.java

    public class HomePage extends WebPage {
      public HomePage(final PageParameters parameters) {
        add(new FeedbackPanel("feedback"));
     
        TextField< ? > username = new TextField<String>("username", Model.of(""));
        username.setRequired(true);
     
        FormComponent<String> password1 = new PasswordTextField("password1", Model.of(""));
        password1.setLabel(Model.of("Password"));
     
        FormComponent< ? > password2 = new PasswordTextField("password2", Model
            .of(""));
     
        Form< ? > form = new Form<Void>("form") {
          @Override
          protected void onSubmit() {
            info("Form successfully submitted");
          }
        };
     
        form.add(new EqualPasswordInputValidator(password1, password2));
     
        add(form);
        form.add(username);
        form.add(password1);
        form.add(password2);
      }
    }

How to do it...

  1. Create the validator for the username field:

    UsernameValidator.java

    public class UsernameValidator extends CompoundValidator<String> {
      public UsernameValidator() {
        add(StringValidator.lengthBetween(5, 20));
        add(new PatternValidator("[a-z]+"));
      }
    }
  2. Install the validator into the username field:

    HomePage.java

        TextField< ? > username = new TextField<String>("username", Model.of(""));
        username.setRequired(true);
        username.add(new UsernameValidator());

How it works...

To implement the username field validator we extend the org.apache.wicket.validation.CompoundValidator class and inside the constructor use its CompoundValidator#add(IValidator) method to add all the individual validators which together define our overall constraint:

public class UsernameValidator extends CompoundValidator<String>
  {
  public UsernameValidator()
    {
      add(StringValidator.lengthBetween(5, 20));
      add(new PatternValidator("[a-z]+"));
    }
  }

The little bit of magic that happens is inside the CompoundValidator#validate(IValidatable) method:

public class CompoundValidator<T> implements IValidator<T>
  {
    public final void validate(IValidatable<T> validatable)
      {
        Iterator<IValidator<T>> it = validators.iterator();
        while (it.hasNext() && validatable.isValid())
          {
            it.next().validate(validatable);
          }
      }
  }

The method loops over all validators that have been added and delegates validation to each one sequentially by invoking their IValidator#validate(IValidatable) method. If a validation error is detected, by checking the IValidatable#isValid() method, the loop is aborted and the validator returns.

Using this trick it is trivial to compose low-level validators into higher-level validators that are reusable across forms and fields.

There's more...

While validators are very flexible it is important not to abuse them. In this section, we will take a look at some pitfalls.

Pitfalls of not encapsulating validation constraints

It seems easier to accomplish this by invoking FormComponent#add(IValidator) multiple times on the username field itself as follows:

TextField<String> username = new TextField<String>("username", Model.of(""));
username.setRequired(true);
username.add(StringValidator.lengthBetween(5, 20));
username.add(new PatternValidator("[a-z]+"));

But, it is important to consider the implications:

  • It is easy to forget to add one of the constraints; sometimes fields will have five or six constraints that make up their overall validation. In these cases it is very easy to leave off a constraint or two which will cause a headache down the road.

  • What happens if the business logic governing the validation constraint changes? We would need to find all the places in our code where the username field is used so we can update it to use the new constraint. It is better to stick to the DRY (don't repeat yourself) principle and have only one place in code that governs this constraint.

Pitfalls of not externalizing validation constraints

Some developers prefer to achieve the same usecase by creating a subclass of the FormComponent instead of the CompoundValidator. For example, following this methodology, the username field can be built as follows:

private class UsernameField extends TextField<String>
  {
    public UsernameField(String id, IModel<String> model)
      {
        super(id, model);
        setLabel(new ResourceModel("field.username.label"));
        add(StringValidator.lengthBetween(5, 20));
      }
  }

While this does not violate the DRY principle for our particular usecase, it does somewhat limit the further composability and reusability of the validation code. For example, suppose we needed to have a Referer field that had the same validation logic as the Username field with the additional constraint that it cannot contain the username of the currently logged in user.

Had we chosen to implement our validation exclusively inside the UsernameField form component, we would be forced to extend it and use a TextField instead of perhaps an AutocompleteTextField. But, had we chosen to implement the validation inside the UsernameValidator, we could simply wrap it inside another compound validator:

public class RefererUsernameValidator extends CompoundValidator<String>
  {
    public UsernameValidator()
      {
        add(new UsernameValidator());
        add(new NotCurrentlyLoggedInUsernameValidator());
      }
  }

See also

  • See the Creating a custom validator recipe in this chapter on how to create field validators.

  • See the the Composing multiple form components into a single reusable component recipe for how to combine form components.

 

Converting string inputs to objects


The web is built around strings: the html documents are strings, the cookies are strings, and the urls are strings as well. The browser, which is how users interact with our web applications, only understands strings. However, it is rare that Java code which drives all these artifacts is also built around strings. Proper Java code is built around objects, and so when we build web applications with Java we often have to convert between objects and strings. The problem is most apparent when working with web forms where fields that represent things other than strings in the Java code (dates, numbers, and currency) have to be converted. Web application frameworks try to ease the pain as much as possible, but in the end the developer always has to write the logic that does the conversion.

Let's build a Search Schedule form where the user has to enter a time value in the h:mma format. The value of the Time field will be represented by a cookbook.Time Java object we will create, and we will use Wicket's conversion infrastructure to allow the Time textfield to seamlessly convert between the cookbook.Time object and its h:mma string representation.

The user enters a correct time value and it is converted to the Time object as shown in the following screenshot:

The user enters an incorrect Time value and a conversion error is reported as shown in the following screenshot:

Getting ready

Let's get started by creating a form with a regular TextField for time.

Create the web page:

HomePage.html

<html>
    <body>
          <h1>Search Schedule</h1>
          <div wicket:id="feedback"></div>
        <form wicket:id="form">
           <p><label>Time</label>: <input wicket:id="time" type="text" size="20"/>(h:mma)</p>
           <input type="submit" value="Search Schedule"/>
        </form>
    </body>
</html>

HomePage.java

public class HomePage extends WebPage
  {

    public HomePage(final PageParameters parameters)
      {
        add(new FeedbackPanel("feedback"));

        TextField<String> time = new TextField<String>("time", Model.of(""));

        Form< ? > form = new Form<Void>("form")
          {
            @Override
            protected void onSubmit()
              {
                info("You entered: " + HomePage.this.time);
              }
          };

        add(form);
        form.add(time);
      }
  }

How to do it...

  1. Create the object to represent the time:

    Time.java

    public class Time implements Serializable
      {
      private int hour;
      private int minute;
      private boolean am = true;
    
    // getters and setters omitted for brevity
    
      @Override
      public String toString()
        {
          return String.format("[Time hour=%d minute=%d am=%b]", hour, minute, am);
        }
      }
  2. Create the converter that will convert the object to a string and back:

    TimeConverter.java

    public class TimeConverter implements IConverter
      {
    
        public Object convertToObject(String val, Locale locale)
          {
            if (Strings.isEmpty(val))
              {
                return null;
              }
    
        String value = val.toLowerCase();
    
        if (!Pattern.matches("[0-9]{1,2}:[0-9]{2}(am|pm)", value))
          {
            error(value, "format");
          }
    
        int colon = value.indexOf(':');
        int hour = Integer.parseInt(value.substring(0, colon));
        int minute = Integer.parseInt(value.substring(colon + 1, colon + 3));
        String meridian = value.substring(colon + 3, colon + 5);
    
        if (hour < 1 || hour > 12)
          {
            error(value, "hour");
          }
        if (minute < 0 || minute > 59)
          {
            error(value, "minute");
          }
    
        Time time = new Time();
        time.setHour(hour);
        time.setMinute(minute);
        time.setAm("am".equals(meridian));
    
        return time;
      }
    
      private void error(String value, String errorKey)
      {
        ConversionException e = new ConversionException("Error converting value: " + value +" to an instance of: " + Time.class.getName());
        e.setSourceValue(value);
        e.setResourceKey(getClass().getSimpleName() + "." + errorKey);
        throw e;
      }
    
      public String convertToString(Object value, Locale locale)
        {
          if (value == null)
            {
              return null;
            }
          Time time = (Time)value;
    
          return String.format("%d:%02d%s", time.getHour(), time.getMinute(), ((time.isAm())
            ? "am"
            : "pm"));
        }
      }
  3. Register the converter in the application subclass:

    WicketApplication.java

    public class WicketApplication extends WebApplication {
     public Class<HomePage> getHomePage() {
        return HomePage.class;
      }
    
      @Override
      protected IConverterLocator newConverterLocator() {
        ConverterLocator locator = (ConverterLocator)super.newConverterLocator();
        locator.set(Time.class, new TimeConverter());
        return locator;
      }
    }
  4. Change the time TextField to use our Time object:

    HomePage.java

    public class HomePage extends WebPage
      {
      private Time time;
    
      public HomePage(final PageParameters parameters)
        {
          TextField<Time> time = new TextField<Time>("time", new PropertyModel<Time>(this, "time"));
    
        }
      }
  5. Create message strings for conversion errors:

    HomePage.html

    TimeConverter.format=${label} does not contain a properly formatted date
    TimeConverter.hour=${label} does not contain a valid hour, must be between 1 and 12
    TimeConverter.minute=${label} does not contain a valid minute, must be between 0 and 59

How it works...

All Wicket components that work with models are aware of Wicket's conversion infrastructure. When a component needs to present its model object as a part of html markup, it first runs it through the conversion infrastructure to convert it to a string. When a component needs to convert a user's input, which is typically a string, back to a model object it once again runs it through the conversion infrastructure, this time to convert the string value back to the object.

Wicket's conversion infrastructure is built around converters, represented by objects that implement org.apache.wicket.util.convert.IConverter the interface, and a converter locator responsible for finding converters that can convert from an object of the given class to a string and back.

Let's start creating our converter by letting it implement the IConverter interface:

public class TimeConverter implements IConverter
  {
    public Object convertToObject(String value, Locale locale)
      {
        return null;
      }

    public String convertToString(Object value, Locale locale)
      {
        return null;
      }
  }

We can see that each converter consists of two methods:

  • convertToObject(): It converts a string representation to the object

  • convertToString(): It converts an object to its string representation

We begin by implementing the easier of the two methods – convertToString() – inside which we simply convert the passed in Time instance to its h:mma string representation:

  public String convertToString(Object value, Locale locale)
    {
    if (value == null)
      {
        return null;
      }
    Time time = (Time)value;

    return String.format("%d:%02d%s", time.getHour(), time.getMinute(), ((time.isAm())
        ? "am"
        : "pm"));
    }

Note

Notice that we must correctly handle null values. Wicket does not automatically handle nulls because there are cases where a null object is not represented by a null string.

With the easy part out of the way, let's implement the convertToObject() method. The first thing we do is check for null:

  if (value == null)
    {
      return null;
    }

As a null string represents a null cookbook.Time instance, we simply return null.

Next we check the overall format of the string, and if it does not match the h:mma format we report an error using the helper error(String,String) method which we will see later:

String value = val.toLowerCase();

if (!Pattern.matches("[0-9]{1,2}:[0-9]{2}(am|pm)", value))
  {
    error(value, "format");
  }

Now that we know the string is in the correct format we can tokenize it and do some further checking:

    int colon = value.indexOf(':');
    int hour = Integer.parseInt(value.substring(0, colon));
    int minute = Integer.parseInt(value.substring(colon + 1, colon + 3));
    String meridian = value.substring(colon + 3, colon + 5);

    if (hour < 1 || hour > 12)
    {
      error(value, "hour");
    }
    if (minute < 0 || minute > 59)
      {
        error(value, "minute");
      }

At this point we know that the user has entered a valid h:mma representation of the string and we can construct and return an instance of cookbook.Time object:

    Time time = new Time();
    time.setHour(hour);
    time.setMinute(minute);
    time.setAm("am".equals(meridian));
    return time;

The only part we have not looked at is the error(String,String) helper method which reports conversion errors. Converters report conversion errors by throwing a org.apache.wicket.util.convert.ConversionException. ConversionException carries with it such information as the value that failed conversion, as well as a message resource key that will be used to display the error. The helper method does just this: construct and throw an instance of ConversionException that contains the proper message resource key:

 private void error(String value, String errorKey)
  {
    ConversionException e = new ConversionException("Error converting value: " + value +
    " to an instance of: " + Time.class.getName());
    e.setSourceValue(value);
    e.setResourceKey(getClass().getSimpleName() + "." + errorKey);
    throw e;
  }

Our converter has three possible errors that it can report to the user as seen in the message resource file we created earlier:

TimeConverter.format=${label} does not contain a properly formatted date
TimeConverter.hour=${label} does not contain a valid hour, must be between 1 and 12
TimeConverter.minute=${label} does not contain a valid minute, must be between 0 and 59

The error helper method creates these keys by concatenating the simple name of the converter with a dot followed by the error key.

Note

Using the converter's simple class name concatenated with a more specific error key is Wicket's convention for creating resource keys for conversion error messages; it is advised that the users follow the same convention when creating custom converters.

With our converter complete, the only remaining task is to register it with Wicket so it can look it up whenever a conversion to or from cookbook.Time is needed. We do this in our application subclass by the overriding application's newConverterLocator() factory method:

public class WicketApplication extends WebApplication
  {
    @Override
    protected IConverterLocator newConverterLocator()
      {
        ConverterLocator locator = (ConverterLocator)super.newConverterLocator();
        locator.set(Time.class, new TimeConverter());
        return locator;
      }
  }

Once this is done, any Component with a model object of type cookbook.Time will use our converter to perform the conversion to a string and back.

There's more...

The recipe has covered the core usecase of converters in Wicket. In this section, we will learn how to use them better.

How automatic type conversion works

You may have noticed that we did nothing special when creating the TextField component in the form:

TextField<Time> time = new TextField<Time>("time", new PropertyModel<Time>(this, "time"));

So how did it know that its model object is of type cookbook.Time? Some models are capable of letting components know the type of the object they contain. PropertyModel is one such model because it implements org.apache.wicket.model.IObjectClassAwareModel. If we did not use such a model we would have to manually set the type on TextField via the FormComponent#setType(Class<?>) method:

TextField<Time> time = new TextField<Time>("time", new Model<Time>(null));
time.setType(Time.class);

Global converters

By registering our converter with the application's ConverterLocator we have made our converter available globally and it will transparently be used any time a conversion to or from cookbook.Time will be needed. However, there are times when we may wish to override the converter on a case by case basis. We can accomplish this by overriding Component#getConverter(Class<?>) method. For example, had we not registered our converter globally we could still use it.

TextField<Time> time = new TextField<Time>("time", new Model<Time>(null)
  {
    public IConverter getConverter(Class<?> type)
      {
        if (Time.class.equals(type)
          {
            return new TimeConverter();
          }
        return super.getConverter(type);
      });
time.setType(Time.class);

More on resource strings

In our example, we have put the message resources into HomePage.properties which only made them available in that particular page. In most cases, however, it may make more sense to put them into Application's global property bundle so they are available everywhere.

See also

  • See the Creating a custom validator recipe in this chapter on how to create field validators.

About the Author

  • Igor Vaynberg

    Igor Vaynberg is a software architect with more than ten years of experience in the software field. His liking for computers was sparked when his parents got him a Sinclair Z80 when he was but ten years old. Since then he has worked with companies both large and small building modular and scalable web applications. Igor's main interest is finding ways to simplify the development of complex user interfaces required by modern web applications. Igor is a committer for the Apache Wicket framework, the aim of which is to simplify the programming model as well as reintroduce OOP to the web UI tier. In his AFK time he enjoys snowboarding with his beautiful wife and playing with his amazing children.

    Browse publications by this author
Book Title
Access this book, plus 7,500 other titles for FREE
Start FREE trial