Tuesday, 12 October 2010

f:convertNumber parses to double not BigDecimal

The default behaviour of the f:convertNumber, implemented with NumberConverter, in converting a decimal String of "1.11", is to convert it to a double and then JSF converts that to a BigDecimal. Most of the time you won't notice this happening until you use CallableStatement.setBigDecimal to invoke a stored procedure. In MS SQL Server, you get the following error:
com.microsoft.sqlserver.jdbc.SQLServerException: Error converting data type nvarchar to decimal.
The cause of this is because inside the BigDecimal it stored "1100000000000000976996261670137755572795867919921875" as a result of converting the String to a double. This floating point error is the motivation for using BigDecimal in the first place. And of course because the SQL Server JDBC driver didn't know how to handle the BigDecimal properly.

Looking into ConvertNumber, it uses one of two converters (DecimalFormat or NumberFormat) depending on whether 'pattern' is used. In Java 5, they added setParseBigDecimal(boolean newValue) in order to yield the desired output.

You can't easily change the NumberConverter but you can very simply add your own. To do so, I grabbed the source of NumberConverter from Mojarra 2.0.2 and copied all of it into my own class: net.devgrok.BigDecimalConverter.
@FacesConverter(forClass = BigDecimal.class, value="bigdecimal")
public class BigDecimalConverter implements Converter, PartialStateHolder {

// ... existing code

  public Object getAsObject(FacesContext context, UIComponent component, String value) {

// ... existing code

      // Create and configure the parser to be used
      parser = getNumberFormat(locale);
      if (((pattern != null) && pattern.length() != 0) || "currency".equals(type))
      {
        configureCurrency(parser);
      }
      parser.setParseIntegerOnly(isIntegerOnly());
      boolean groupSepChanged = false;

      // BEGIN BigDecimal HACK
      if (parser instanceof DecimalFormat)
      {
        ((DecimalFormat) parser).setParseBigDecimal(true);
      }
      // END BigDecimal HACK
      
// ... existing code

  }

// ... existing code

}
Then just create a file META-INF/custom.taglib.xml:
<?xml version="1.0" encoding="UTF-8"?>

<facelet-taglib xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd"
    version="2.0">

    <namespace>http://devgrok.net/facestags</namespace>

    <tag>
        <tag-name>convertBigDecimal</tag-name>
        <converter>
            <converter-id>bigdecimal</converter-id>
        </converter>
    </tag>

</facelet-taglib>

And to use:
<?xml version="1.0" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
  xmlns:ui="http://java.sun.com/jsf/facelets"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:f="http://java.sun.com/jsf/core"
  xmlns:fn="http://java.sun.com/jsp/jstl/functions"
  xmlns:c="http://java.sun.com/jsp/jstl/core"
  xmlns:dg="http://devgrok.net/facestags"
  >

  from goes here etc
  
  <h:inputText id="txtPrice" value="#{bean.item.price}">
    <dg:convertBigDecimal pattern="##0.00" />
  </h:inputText>
  
</ui:composition