Creating A Simple DateTime Field in Tapestry5

Recently I had a requirement for a date-field which could take date-time as input. Tapestry5 has already such a component DateField but comes with a date-picker which I did not want. So I decided to create my own.

This is the source code

package com.googlecode.tawus.components;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

import org.apache.tapestry5.Binding;
import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.FieldValidationSupport;
import org.apache.tapestry5.FieldValidator;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.ValidationException;
import org.apache.tapestry5.ValidationTracker;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.corelib.base.AbstractField;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.services.ComponentDefaultProvider;
import org.apache.tapestry5.services.Request;

import com.googlecode.tawus.TawusSymbolConstants;

/**
 * A simple date field which does not contain a date picker. It is sometimes
 * required for have a date picker which is required for time only
 */
public class SimpleDateField extends AbstractField {

   @Parameter(required = true, principal = true, autoconnect = true)
   private Date value;

   @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
   private DateFormat format;

   @Parameter(defaultPrefix = BindingConstants.VALIDATE)
   private FieldValidator<object> validate;

   @Environmental
   private ValidationTracker validationTracker;

   @Inject
   private FieldValidationSupport fieldValidationSupport;

   @Symbol(TawusSymbolConstants.DEFAULT_DATE_FORMAT)
   @Inject
   private String defaultDateFormat;

   @Inject
   private ComponentDefaultProvider defaultProvider;

   @Inject
   private ComponentResources resources;

   @Inject
   private Request request;

   @Inject
   private Messages messages;

   @Inject
   private Locale locale;

   DateFormat defaultFormat() {
      final DateFormat dateFormat;
      if ("locale".equalsIgnoreCase(defaultDateFormat)) {
         dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, locale);
      } else {
         dateFormat = new SimpleDateFormat(defaultDateFormat);
      }
      dateFormat.setLenient(false);
      return dateFormat;
   }

   final Binding defaultValidate() {
      return defaultProvider.defaultValidatorBinding("value", resources);
   }

   @Override
   protected void processSubmission(String elementName) {
      String value = request.getParameter(elementName);
      validationTracker.recordInput(this, value);

      Date parseValue = null;
      try {
         if (InternalUtils.isNonBlank(value)) {
            parseValue = format.parse(value);
         }
      } catch (ParseException pe) {
         validationTracker.recordError(this, messages.format(
               "date-value-not-parseable", value));
         return;
      }
      putPropertyNameIntoBeanValidationContext("value");
      try {
         fieldValidationSupport.validate(parseValue, resources, validate);
         this.value = parseValue;
      } catch (ValidationException ve) {
         validationTracker.recordError(this, ve.getMessage());
      }

      removePropertyNameFromBeanValidationContext();
   }

   void beginRender(MarkupWriter writer) {
      String value = validationTracker.getInput(this);

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

      writer.element("input", "type", "text", "value", value, "id",
            getClientId(), "name", getControlName());
      if (isDisabled()) {
         writer.attributes("disabled", "disabled");
      }

      putPropertyNameIntoBeanValidationContext("value");
      validate.render(writer);
      removePropertyNameFromBeanValidationContext();
      resources.renderInformalParameters(writer);
      decorateInsideField();

      writer.end();
   }

   private String formatCurrentValue() {
      final String value;
      if (this.value == null) {
         value = "";
      } else {
         value = format.format(this.value);
      }
      return value;
   }

   @Override
   public boolean isRequired() {
      return validate.isRequired();
   }

}

Obviously, this code needs a lot of explanation

Let us start from the beginning

@SupportsInformalParameters
public class SimpleDateField extends AbstractField {

@SupportsInformalParameters allows this component to have html attributes other than the @Parameters it defines. e.g style, class, width etc.

Why to extend from AbstractField ? AbstractField implements Field which has the following methods

  1. getControlName() :- returns the name for the input field
  2. getLabel() :- It returns the label for the input filed
  3. isDisabled() :- whether the field has to be rendered disabled
  4. isRequired() :- whether the field is required
  5. getClientId() :- It returns the javascript id. This method is inherited from ClientElement

So if you want you can implement that yourself. It is not that difficult but why re-invent the wheel

Now we declare the parameters for this component.


   @Parameter(required = true, principal = true, autoconnect = true)
   private Date value;

   @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
   private DateFormat format;

   @Parameter(defaultPrefix = BindingConstants.VALIDATE)
   private FieldValidator<object> validate;

We have value as our first parameter and set it to autoconnect so that we can get away with setting only the id of a component and the value will be set to the property of the same name in the container component. We have format parameter to set the date-format that you want the date to be input in and finally we have the validate field to set our validators

This is followed by injecting services that we need later


   @Environmental
   private ValidationTracker validationTracker;

   @Inject
   private FieldValidationSupport fieldValidationSupport;

   @Symbol(TawusSymbolConstants.DEFAULT_DATE_FORMAT)
   @Inject
   private String defaultDateFormat;

   @Inject
   private ComponentDefaultProvider defaultProvider;

   @Inject
   private ComponentResources resources;

   @Inject
   private Request request;

   @Inject
   private Messages messages;

   @Inject
   private Locale locale;

ValidationTracker is for reporting any form based errors. FieldValidationSupport is for validating input. An application level symbol has been injected to the get defaultDateFormat at the application level. Messages have been injected for accessing locale messages and locale to get the locale based date format.

Now what if we want to set some default values for the parameters in case the parameters are not provided


   DateFormat defaultFormat() {
      final DateFormat dateFormat;
      if ("locale".equalsIgnoreCase(defaultDateFormat)) {
         dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, locale);
      } else {
         dateFormat = new SimpleDateFormat(defaultDateFormat);
      }
      dateFormat.setLenient(false);
      return dateFormat;
   }

   final Binding defaultValidate() {
      return defaultProvider.defaultValidatorBinding("value", resources);
   }

Here we set the default value for the format and a default validator. The next step is to display our component in an html form. We do that in the beginRender phase

   void beginRender(MarkupWriter writer) {
      String value = validationTracker.getInput(this);

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

      writer.element("input", "type", "text", "value", value, "id",
            getClientId(), "name", getControlName());
      if (isDisabled()) {
         writer.attributes("disabled", "disabled");
      }

      putPropertyNameIntoBeanValidationContext("value");
      validate.render(writer);
      removePropertyNameFromBeanValidationContext();
      resources.renderInformalParameters(writer);
      decorateInsideField();

      writer.end();
   }

We have written an input using MarkupWriter and asked our FieldValidator to render if it has to. Before asking FieldValidator to write we again set the BeanValidationContext to the current property so that BeanFieldValidator is in the right context when rendering. We also call renderInformalParameters() to render any informal parameters. Finally we ask ValidationDecorator to decorate the inside of the input tag and close the tag.

Now starts the real action


   @Override
   protected void processSubmission(String elementName) {
      String value = request.getParameter(elementName);
      validationTracker.recordInput(this, value);

      Date parseValue = null;
      try {
         if (InternalUtils.isNonBlank(value)) {
            parseValue = format.parse(value);
         }
      } catch (ParseException pe) {
         validationTracker.recordError(this, messages.format(
               "date-value-not-parseable", value));
         return;
      }
      putPropertyNameIntoBeanValidationContext("value");
      try {
         fieldValidationSupport.validate(parseValue, resources, validate);
         this.value = parseValue;
      } catch (ValidationException ve) {
         validationTracker.recordError(this, ve.getMessage());
      }

      removePropertyNameFromBeanValidationContext();
   }

This method is called by AbstractField at the time of submission. Here we retrieve the value from the request. We register it with the validationTracker. We parse the input and in case of error record the error. We then call putPropertyNameIntoBeanValidationContext(). This method sets the beanvalidationcontext’s current property to “value”. BeanValidationContext is later used by BeanFieldValidator to validate a bean using JSR-303 support. (Will talk about that someday). We validate the field using BeanValidationSource and the validate parameter and later remove the property name from the BeanValidationContext using removePropertyNameFromBeanValidationContext().

Advertisements

Tagged: , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: