Friday, April 25, 2014

JSF 2.2 custom Converter for Primefaces Calendar

Recently, I've got some special requirements for the calendar component of Primefaces. The requirements are:

  • When user enters "300130" for example, the date must be automatically converted to: "30/01/2030" (French pattern: dd/MM/yyyy). Years before 30 are converted to 2000s, those after 30 are converted to 1900s. So, "150277" will be: "15/02/1977"
  • User can enter values with or without slashes so "010202" is the same as "01/02/02" and both must be transformed to "01/02/2002"
  • The dates are displayed independently to Time Zone. So when entering 01/01/2000 it must always be displayed as 01/01/2000 (see this SO question for details)
  • Another requirement that has nothing to do with converter is that the calendar icon must not be selected when clicking on "Tabulation" after filling the date input. This means the tabindex of the icon must be of value -1.

First thoughts

Here for the dates we have more than a pattern to be used in the same calendar. And because of this, the pattern attribute of p:calendar won't be of any use except for formatting dates entered by selecting a value from the calendar dialog.
We have mainly two patterns to be used: yy/MM/yyyy and yyMMyyyy. But, we need to convert values of years having two digits as specified in the first point of the requirements.
So let's begin by implementing a little algorithm for date transformations.
Please note that we will be using Joda-Time for our date manipulations.

Date processing


/**
/**
  * A methods that takes a String value, and format it. Then it transforms years before 30 to 2000s.
  * Years after 30 are transformed to 1900s
  * If the entered value can't be formatted or if the year is contained between 100 and 999,
  * This method returns {@link javax.faces.application.FacesMessage} instead of a Date
  * @param context
  * @param component
  * @param value the String value entered by the user
  * @param pattern the pattern
  * @return
  * @throws ConverterException
  */
 private Object fixDate(FacesContext context, UIComponent component, String value, String pattern) throws ConverterException{
  DateTimeFormatter formatter = DateTimeFormat.forPattern(pattern);
  LocalDateTime ldt = null;
  try{
   ldt = formatter.parseLocalDateTime(value);
  }catch(Exception e){
            Object[] params = new Object[3];
            params[0] = value;
            params[1] = formatter.print(new DateTime( new Date()));
            params[2] = MessageFactory.getLabel(context, component);
            
            return MessageFactory.getMessage("javax.faces.converter.DateTimeConverter.DATE", FacesMessage.SEVERITY_ERROR, params);
  }
  //Get the year and see if the year value is valid, i.e. year must be < 100 or >=1900
  int yy = ldt.getYear();
  if(yy >= 100 && yy <1900){
   return MessageFactory.getMessage(
                    context, "javax.faces.converter.DateTimeConverter.DATE", value,
                    MessageFactory.getLabel(context, component));
  }
  if(yy < 100){
   int c = yy%100;
   if(c <= 30){
    yy = c + 2000;
   }else{
    yy = c + 1900;
   }
   return ldt.withYear(yy).toDate();
  }
  return ldt.toDate();

 }
The code of this method is very simple. And there are comments to help you.

Calendar component

If we had only one pattern, then we would use the pattern attribute of the p:calendar component of Primefaces. But here we have two patterns, and for this, we will use an additional attribute (this is possible with JSF 2.x). So for all my calendar components I will use a custom attribute that will handle all patterns for our dates, example:

<p:calendar converter="#{ourCustomConverter}" custompattern="dd/MM/yyyy;ddMMyyyy" pattern="dd/MM/yyyy" showOn="button" value="#{someBean.someAttr}">
<p:ajax event="change" partialSubmit="true" process="@this" update="@this">
</p:ajax>
</p:calendar>
Here we will still use the pattern attribute for the selection of dates in the calendar dialog.

The custom Converter

Now we will use the custom attribute and the earlier mentioned date transforming method to process user entered dates. For this, we will use a custom converter. For more information about JSF converters please refer to this page. Mainly, we will define the two methods of the javax.faces.convert.Converter interface:
  • getAsObject: this method will convert a String value (technically it's called the "submitted value") to a model data object that will be used during the validation phase as a "local value".
  • getAsString: in some way, this method is the inverse of the previous one, it's used to get a String value from the model objects (dates in our case). The generated Strings are the ones that will be displayed to end user.
So here is our Converter:

import java.text.SimpleDateFormat;
import java.util.Date;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.ConverterException;
import javax.faces.convert.DateTimeConverter;

import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.stereotype.Component;

import com.sun.faces.util.MessageFactory;

/**
 * Converter to be used with dates.
 * Usage: converts a date years, ex: 01/01/14 will be transformed to 01/01/2014 
 * @author Laabidi RAISSI
 *
 */
@Component("ourCustomConverter")
public class DateTimeCustomConverter extends DateTimeConverter{

 private static final Date DEFAULT_END_DATE = new LocalDate(2999, 12, 31).toDate();
 
 @Override
 public Object getAsObject(FacesContext context, UIComponent component, String value) {
  if(value == null){
   return null;
  }
  value = value.split(";")[0]; 
  String pattern = (String)component.getAttributes().get("custompattern");
  String[] patterns = pattern.split(";");
  Object ret = null;
  for(String pat: patterns){
   ret = fixDate(context, component, value, pat);
   if(ret instanceof Date){
    return ret;
   }
  }
  throw new ConverterException((FacesMessage)ret);
 }
 
 public String getAsString(FacesContext context, UIComponent component, Object value) {
  if(value == null){
   return "";
  }
  if (context == null || component == null) {
   throw new NullPointerException();
  }

  try {
   String pattern = ((String)component.getAttributes().get("custompattern")).split(";")[0];
   SimpleDateFormat dateFormat = new SimpleDateFormat(pattern, getLocale());
   String res = dateFormat.format(value);
   String defaultStr = dateFormat.format(DEFAULT_END_DATE);
   if(defaultStr.equals(res)){
    return "";
   }
   return dateFormat.format(value);

  } catch (ConverterException e) {
   throw new ConverterException(MessageFactory.getMessage(context, STRING_ID, value, MessageFactory.getLabel(context, component)), e);
  } catch (Exception e) {
   throw new ConverterException(MessageFactory.getMessage(context, STRING_ID, value, MessageFactory.getLabel(context, component)), e);
  }
 }
 

 /**
  * A methods that takes a String value, and format it. Then it transforms years before 30 to 2000s.
  * Years after 30 are transformed to 1900s
  * If the entered value can't be formatted or if the year is contained between 100 and 999,
  * This method returns {@link javax.faces.application.FacesMessage} instead of a Date
  * @param context
  * @param component
  * @param value the String value entered by the user
  * @param pattern the pattern
  * @return
  * @throws ConverterException
  */
 private Object fixDate(FacesContext context, UIComponent component, String value, String pattern) throws ConverterException{
  DateTimeFormatter formatter = DateTimeFormat.forPattern(pattern);
  LocalDateTime ldt = null;
  try{
   ldt = formatter.parseLocalDateTime(value);
  }catch(Exception e){
            Object[] params = new Object[3];
            params[0] = value;
            params[1] = formatter.print(new DateTime( new Date()));
            params[2] = MessageFactory.getLabel(context, component);
            
            return MessageFactory.getMessage("javax.faces.converter.DateTimeConverter.DATE", FacesMessage.SEVERITY_ERROR, params);
  }
  //Get the year and see if the year value is valid, i.e. year must be < 100 or >=1900
  int yy = ldt.getYear();
  if(yy >= 100 && yy <1900){
   return MessageFactory.getMessage(
                    context, "javax.faces.converter.DateTimeConverter.DATE", value,
                    MessageFactory.getLabel(context, component));
  }
  if(yy < 100){
   int c = yy%100;
   if(c <= 30){
    yy = c + 2000;
   }else{
    yy = c + 1900;
   }
   return ldt.withYear(yy).toDate();
  }
  return ldt.toDate();

 }
}
Please notice that I am using Spring annotations for my project. You can replace the @Component with @FacesConverter.
And that's all we need for our dates conversions.

Tabindex for the calendar icon

This has nothing to do with the converter subject. But since I'm talking about calendars, I thought it might be of use. 
By default, the button that triggers the calendar popup in Primefaces is selected when moving out of the input date via "Tabulation" button. This can be of no use in some situations (like mine since in 90% of cases, user will enter dates manually than selecting it from popup).

To change this behaviour, we need to set the tabindex of this icon to be "-1". To do this, I just used the simplest solution: overriding the JS file of Primefaces named calendar.js with a slightly changed one. All you need to do, is to search in  file for the string: ".ui-datepicker-trigger:button" and add the following line:
triggerButton.attr('tabindex', -1);. So, it will look something like this:

//extensions
        if(this.cfg.popup && this.cfg.showOn) {
            var triggerButton = this.jqEl.siblings('.ui-datepicker-trigger:button');
            triggerButton.html('').addClass('ui-button ui-widget ui-state-default ui-corner-all ui-button-icon-only')
                        .append('ui-button');

            var title = this.jqEl.attr('title');
            if(title) {
                triggerButton.attr('title', title);
            }
            triggerButton.attr('tabindex', -1);
            PrimeFaces.skinButton(triggerButton);
            $('#ui-datepicker-div').addClass('ui-shadow');
        }
The last thing to do, is to tell JSF to include our calendar.js file at last so it overrides the one in Primefaces, for this, use the f:facet tag:

<f:facet name="last">
        <h:outputScript library="js" name="calendar.js"/>
</f:facet>
And that is it!