Wednesday, January 18, 2012

Sons of DHTMLX and Spring #3

Episode 3: Turning and Turning

DHTMLX Spring Adapter
In the picture below you will find  the DHTMLX Spring Adapter class diagram for the dhtmlxGrid component.


click to enlarge


Each adapter implements the Adapter interface, which consists of the single method toXML. The AbstractAdapter class, is the base class of all the adapters and encapsulates  the methods common to all the subclasses. 
The DhtmlxHttpMessageConveter, is the class which integrates the adapters with the Spring framework. 


Spring Message Converters
The Spring MVC allows to handle any format of HTTP request and responses using a HttpMessageConverter implementation. Spring registers a set of default converters, but it is  also possible write your own and register it in the configuration file: 

  1. <mvc:annotation-driven>
  2.   <mvc:message-converters>
  3.     <bean class="com.mylaensys.dhtmlx.adapter.DhtmlxHttpMessageConverter"/>
  4.   </mvc:message-converters>
  5. </mvc:annotation-driven>
Below the code of the message converter.

DhtmlxHttpMessageConverter.java

  1. public class DhtmlxHttpMessageConverter
  2.   extends AbstractHttpMessageConverter<Object> {
  3.     public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
  4.     public DhtmlxHttpMessageConverter() {
  5.         super(new MediaType("text", "xml", DEFAULT_CHARSET));
  6.     }
  7.     @Override
  8.     protected boolean supports(Class<?> clazz) {
  9.         Class[] theInterfaces = clazz.getInterfaces();
  10.         for (int i = 0; i < theInterfaces.length; i++) {
  11.             if( theInterfaces[i].getName().equalsIgnoreCase( Adapter.class.getName() ) ) {
  12.                 return true;
  13.             }
  14.         }
  15.         return false;
  16.     }
  17.     @Override
  18.     protected void writeInternal(Object object, HttpOutputMessage outputMessage)
  19.   throws IOException, HttpMessageNotWritableException {
  20.         Adapter adapter = (Adapter)object;
  21.         outputMessage.getBody().write( adapter.toXML().getBytes() );
  22.     }
  23. }
The supports method detects whether the given class is supported by the converter. In this case the class must implement the Adapter interface. When a class is supported, the HttpMessageConverter invokes the writeInternal method to write the object to the Http response body. 
The writeInternal method of the DhtmlxHttpMessageConverter obtains the representation of the object in XML, invoking the toXML method of the Adapter interface. In the example below, the BookController class returns an instance of the DefaultGridAdapter to the DhtmlxHttpMessageConverter.
BookController.java
  1. @Controller
  2. public class BookController {
  3.     @Autowired
  4.     private BookService bookService;
  5.     @RequestMapping(value = "/books", method = RequestMethod.GET)
  6.     public @ResponseBody DefaultGridAdapter getBooks(@RequestParam("c") String c) {
  7.         DefaultGridAdapter adapter = new DefaultGridAdapter(c,Book.class);
  8.         adapter.setData( bookService.getBooks() );
  9.         return adapter;
  10.     }
  11. }
Another important element is the @ResponseBody which indicates that the return type of a controller method should be written to the HTTP response body, and not placed in a Model, or interpreted as a view name as standard behavior of Spring MVC.


Grid Adapter
The DefaulGridAdapter is a basic adapter for the dhtmlxGrid component. The grid adapter constructor accepts as parameters a string containing the attributes to render, and  the class of the object. The setData method allows to set the collection of data. 
DefaultGridAdapter.java
  1. public class DefaultGridAdapter extends AbstractAdapter implements Adapter {
  2.     private List data;
  3.     private List<String> columnList = new ArrayList<String>();
  4.     private GridInterceptor interceptor = new GridInterceptorImpl();
  5.     public DefaultGridAdapter(String columnList,Class clazz) {
  6.         this.fieldList = getObjectFields(clazz);
  7.         StringTokenizer st = new StringTokenizer(columnList,",");
  8.         while(st.hasMoreTokens()) {
  9.               this.columnList.add(st.nextToken());
  10.         }
  11.     }
  12.     public List getData() {
  13.         return data;
  14.     }
  15.     public void setData(List data) {
  16.         this.data = data;
  17.     }
  18.     public void setInterceptor(GridInterceptor interceptor) {
  19.         this.interceptor = interceptor;
  20.     }
  21.     @Override
  22.     public String toXML() {
  23.         StringBuffer buffer = new StringBuffer();
  24.         buffer.append("<?xml version='1.0' encoding='UTF-8'?>");
  25.         interceptor.onHeader(this,data,buffer);
  26.         interceptor.onStartRows(this, data, buffer);
  27.         for(Object object : data) {
  28.             interceptor.onStartRow(this, object, buffer);
  29.             for(String column : columnList ) {
  30.                 if( fieldList.contains( column ) ) {
  31.                     interceptor.onRenderCell(this, object, column, buffer);
  32.                 }
  33.             }
  34.             interceptor.onEndRow(buffer);
  35.         }
  36.         interceptor.onEndRows(buffer);
  37.         interceptor.onOutput(buffer);
  38.         return buffer.toString();
  39.     }
  40. }
The toXML method invokes the interceptor which contains the logic for the XML generation. Below the default implementation of the GridInteceptor.
GridInterceptorImpl.java
  1. public class GridInterceptorImpl implements GridInterceptor {
  2.     public void onHeader(AbstractAdapter adapter, List list, StringBuffer buffer) {
  3.     }
  4.     public void onStartRows(AbstractAdapter adapter, List list, StringBuffer buffer) {
  5.         buffer.append("<rows>");
  6.     }
  7.     public void onEndRows(StringBuffer buffer) {
  8.         buffer.append("</rows>");
  9.     }
  10.     public void onStartRow(AbstractAdapter adapter, Object object, StringBuffer buffer) {
  11.         buffer.append("<row id='").append( adapter.getPrimaryKey(object).toString() ).append("'>");
  12.     }
  13.     public void onEndRow(StringBuffer buffer) {
  14.         buffer.append("</row>");
  15.     }
  16.     public void onRenderCell(AbstractAdapter adapter, Object object, String column, StringBuffer buffer) {
  17.         Object value = adapter.getObjectValue(object, column);
  18.         buffer.append("<cell><![CDATA[").append( value.toString() ).append("]]></cell>");
  19.     }
  20.     public void onOutput(StringBuffer buffer) {
  21.     }
  22. }
It is possible to write a custom  GridIntercepter and set it via the setInterceptor method. For example to highlight the rows of the grid which match a condition, it is possible extending the DefaultGridInterceptor and overriding the onStartRow method. The code example below highlights in red the books with a price greater than 10. 
  1.  public class GridInterceptorHighLight extends DefaultGridInterceptor {
  2.     public void onStartRow(AbstractAdapter adapter, Object object, StringBuffer buffer) {
  3.         if( object instanceof Book) {
  4.             Book book = (Book)object;
  5.             if( book.getPrice() > 10 ) {
  6.                 buffer.append("<row id='")
  7.       .append( adapter.getPrimaryKey(object).toString() )
  8.       .append("' style='color:red;'>");
  9.             } else {
  10.                 buffer.append("<row id='")
  11.       .append( adapter.getPrimaryKey(object).toString() )
  12.       .append("'>");
  13.             }
  14.         }
  15.     }
  16. }

The same concept applies to the header (onHeader), to a single cell (onRenderCell) and to the end of the XML processing (onOutput). For more information about the grid XML format refer to  DHTMLX documentation.


Form Adapter
The DefaulFormAdapter is a basic adapter for the dhtmlxForm component. The adapter supports the basic operation: read, write and delete. 
GridInterceptorImpl.java
  1. public class DefaultFormAdapter extends AbstractAdapter implements Adapter {
  2.     public static final String Insert = "inserted";
  3.     public static final String Update = "updated";
  4.     public static final String Delete = "deleted";
  5.     private Object data;
  6.     private String operation;
  7.     public DefaultFormAdapter(Object data) {
  8.         initialize( data );
  9.         this.operation = "";
  10.     }
  11.     public DefaultFormAdapter(Object data,String operation) {
  12.         initialize( data );
  13.         this.operation = operation;
  14.     }
  15.     public DefaultFormAdapter(Object data,BindingResult binding) {
  16.          initialize(data);
  17.          if( binding.hasErrors() ) {
  18.              for( FieldError e : binding.getFieldErrors() ) {
  19.                 this.errorList.add(new ErrorMessage(e.getField(), e.getDefaultMessage()));
  20.             }
  21.          }
  22.     }
  23.     private void initialize(Object data) {
  24.         this.fieldList = getObjectFields(data.getClass());
  25.         Object id = getPrimaryKey( data );
  26.         if( id == null ) {
  27.             this.operation = Insert;
  28.         } else {
  29.             this.operation = Update;
  30.         }
  31.         this.data = data;
  32.     }
  33.     public Object getData() {
  34.         return data;
  35.     }
  36.     public void setData(Object data) {
  37.         this.data = data;
  38.     }
  39.     private boolean isWriting() {
  40.         return Insert.equalsIgnoreCase( this.operation ) || Update.equalsIgnoreCase( this.operation ) || Delete.equalsIgnoreCase( this.operation ) ;
  41.     }
  42.     public boolean hasValidData() {
  43.         return errorList.size() == 0;
  44.     }
  45.     public String toErrorXML() {
  46.         StringBuffer buffer = new StringBuffer();
  47.         String id = getPrimaryKey( data ).toString();
  48.         buffer.append("<data>");
  49.         for( ErrorMessage e : errorList ) {
  50.                 buffer.append("<action sid='").append( id == null ? "" : id.toString() ).append( "' ");
  51.                 buffer.append("type='invalid' ");
  52.                 buffer.append("field='" + e.getField() + "' ");
  53.                 buffer.append("message='" ).append( e.getMessage() ).append("'/>");
  54.         }
  55.         buffer.append("</data>");
  56.         return buffer.toString();
  57.     }
  58.     @Override
  59.     public String toXML() {
  60.         StringBuffer buffer = new StringBuffer();
  61.         if( isWriting() ) {
  62.             if( this.errorList.size() == 0 ) {
  63.                 buffer.append( toStoreXML() );
  64.             } else {
  65.                 buffer.append( toErrorXML() );
  66.             }
  67.         } else {
  68.             buffer.append(toDataXML());
  69.         }
  70.         return buffer.toString();
  71.     }
  72.     private String toDataXML() {
  73.         StringBuffer buffer = new StringBuffer();
  74.         buffer.append("<?xml version='1.0' encoding='UTF-8'?>");
  75.         buffer.append("<data>");
  76.         try {
  77.             if( data != null ) {
  78.                 for(String field : fieldList) {
  79.                     Object value = getObjectValue(data, field);
  80.                     buffer.append("<").append(field).append("><![CDATA[").append( value.toString() ).append(  "]]></").append(field).append(">");
  81.                 }
  82.             }
  83.         } catch (Exception e) {
  84.             log.severe(e.getMessage());
  85.         }
  86.         buffer.append("</data>");
  87.         return buffer.toString();
  88.     }
  89.     public String toStoreXML() {
  90.         StringBuffer buffer = new StringBuffer();
  91.         buffer.append("<data>");
  92.         buffer.append("<action type='").append( this.operation ).append("' ");
  93.         if( data != null ) {
  94.             String id = getPrimaryKey( data ).toString();
  95.             buffer.append("sid='").append( id ).append("' ");
  96.             buffer.append("tid='").append( id ).append("'/>");
  97.         } else {
  98.             buffer.append("field='id' ");
  99.             buffer.append("sid='").append( "0" ).append("' ");
  100.             buffer.append("tid='").append( "0" ).append("' ");
  101.             buffer.append("message='" ).append( "invalid data " ).append("'/>");
  102.         }
  103.         buffer.append("</data>");
  104.         return buffer.toString();
  105.     }
  106. }
The form adapter toXML method generates  the appropriate XML message depending on the operation. The toDataXML returns the XML for the load method of the dhtmlxForm, toStoreXML and toErrorXML handle the send method. Below an example of the BookController using the form adapter.     
BookController.java
  1. public class BookController {
  2.     @Autowired
  3.     private BookService bookService;
  4.     @RequestMapping(value = "/books/{id}", method = RequestMethod.GET)
  5.     public @ResponseBody DefaultFormAdapter getBook(@PathVariable("id") final String id) {
  6.         Book book = bookService.getBook(id);
  7.         DefaultFormAdapter adapter = new DefaultFormAdapter( book );
  8.         return adapter;
  9.     }
  10.     @RequestMapping(value = "/book", method = RequestMethod.POST)
  11.     public @ResponseBody DefaultFormAdapter storeBook(@Valid @ModelAttribute Book book, BindingResult binding) {
  12.         DefaultFormAdapter adapter = new DefaultFormAdapter(book,binding);
  13.         if( adapter.hasValidData() ) {
  14.             bookService.store(book);
  15.         }
  16.         return adapter;
  17.     }
  18.     @RequestMapping(value = "/book/{id}", method = RequestMethod.DELETE)
  19.     public @ResponseBody DefaultFormAdapter deleteBook(@PathVariable("id") final String id) {
  20.         Book book = bookService.getBook(id);
  21.         DefaultFormAdapter adapter = new DefaultFormAdapter(book,DefaultFormAdapter.Delete);
  22.         bookService.remove(id);
  23.         return adapter;
  24.     }
  25. }
The getBook method implements the read operation, while storeBook creates or updates  the object. The DefaultFormAdapter constructor used in the storeBook method takes two parameters: the first one is the model object, the second (BindingResult) holds the result of the validation. When the object contains valid data (hasValidData) the controller can perform write operation to the datastore.    


While waiting for the next episode, don't forget to join the virtual strike against Internet censorship.

Stay Tuned!

Wednesday, January 11, 2012

Sons of DHTMLX and Spring #2

Episode 2: Caregiver
In the previous episode the DHTMLX Spring Adatper was introduced, now it is time to take care of the object through validation. 



Forms
Since version 2.0 the Spring framework includes a JSP  tag library,  to make writing HTML forms much easier. A typical HTML form written using Spring JSP form tags  looks like the code below: 
Spring Form
  1. <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
  2. <form:form id="book" method="post">
  3.   <form:label path="author">Author:</form:label>
  4.   <form:input path="author"/> <br/>
  5.   <form:label path="title">Title:</form:label>
  6.   <form:input path="title"/> <br/>
  7.   <form:label path="price">Price:</form:label>
  8.   <form:input path="price"/> <br/>
  9.   <form:label path="sales">Sales:</form:label>
  10.   <form:input path="sales"/> <br/>
  11.   <input type="submit" value="Save">
  12. </form:form>
DHTMLX version 2.6 introduced the dhtmlxForm component in 2010, providing a flexible Javascript API to create rich form, process events and validate data. The version 3.0 released in July 2011 includes a new version of dhtmlxForm, supporting integration with dhtmlxCalendar and dhtmlxCombo, and new ways to position form controls to build sophisticated web forms. Below the  Javascript code to create a DHTMLX form:
DHTMLX Form
  1. <div id="form" style="width:280px;height:250px;"/>
  2. var form_struct = [
  3.   { type: 'input', name: 'author' , value: '', label:'Author:'},
  4.   { type: 'input', name: 'title' , value: '', label:'Title:'},
  5.   { type: 'input', name: 'price' , value: '', label:'Price:'},
  6.   { type: 'input', name: 'sales' , value: '', label:'Sales:'},
  7.   { type: 'button', name: 'button_save', value:'Save'  },
  8.   ];
  9. var formObject = new dhtmlXForm("form",form_struct);
The DHTMLX Tag Library, a JSP Tag library created on top of the DHTMLX component framework, allows to create DHTMLX forms with a very similar syntax to Spring JSP form tags.
DHTMLX Tags Form
  1. <%@ taglib prefix="dhtmlx" uri="http://www.mylaensys.com/dhtmlx" %>
  2. <dhtmlx:form name='book'>
  3.  <dhtmlx:formInput name='author' label='Author:'/>
  4.  <dhtmlx:formInput name='title' label='Title:'/>
  5.  <dhtmlx:formInput name='price' label='Price:'/>
  6.  <dhtmlx:formInput name='sales' label='Sales:'/>
  7.  <dhtmlx:formButton name='button_save' value='Save'/>
  8. </dhtmlx:form>
The code example in this series uses the DHTMLX Tag Library version 1.6, a recent release which include some of the new features of DHTMLX 3.0. Of course it also works without the tags using the Javascript DHTMLX API.

Client Side Validation
The dhtmlxForm let you specify client validation rules at field level using the validate  property. The DHTMLX Tag Library supports the validate property as attribute of the tag. Below the source code of the of the example form:
  1. <%@ taglib prefix="dhtmlx" uri="http://www.mylaensys.com/dhtmlx" %>
  2. <dhtmlx:form name='form'>
  3.   <dhtmlx:formHidden name='id'/>
  4.   <dhtmlx:formLabel name="message" label="" labelWidth="470"/>
  5.   <dhtmlx:formInput name='author' validate="NotEmpty"/>
  6.   <dhtmlx:formLabel name="authorError" label=""/>
  7.   <dhtmlx:formInput name='title' validate="NotEmpty"/>
  8.   <dhtmlx:formLabel name="titleError" label=""/>
  9.   <dhtmlx:formInput name='price' validate="ValidInteger"/>
  10.   <dhtmlx:formInput name='sales' validate="ValidInteger"/>
  11.   <dhtmlx:formLabel name="message" label="">
  12.   <dhtmlx:formLabel name="dummy" label="" labelWidth="120"/>
  13.     <dhtmlx:formNewColumn/>
  14.     <dhtmlx:formButton name='button_upd' value='Update'/>
  15.     <dhtmlx:formNewColumn/>
  16.     <dhtmlx:formButton name='button_close' value='Close'/>
  17.   </dhtmlx:formLabel>
  18. </dhtmlx:form>
In order to minimize the client to server data exchange, it is a 'user friendly' practice to perform client side validation before sending data to the server. I used the expression 'user friendly' because the client side validation can be easily bypassed.


Server Side Validation 
Staring from release 3, Spring has introduced several enhancements to the validation framework, including Bean Validation API (JSR-303). It is possible to declare validation constraints using annotations. This example has been written to run on the Google App Engine (using JDO). In the Book class below, in addition to the validation, you will find the persistence annotations.
Book.java
  1. public class Book {
  2.   @PrimaryKey
  3.   @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  4.   private Long id;
  5.   @Persistent
  6.   @NotEmpty
  7.   @Size(min = 5, max = 20)
  8.   private String author;
  9.   @Persistent
  10.   @NotEmpty
  11.   @Size(min = 5, max = 20)
  12.   @BookConstraint( message = "title already exists")
  13.   private String title;
  14.   @Persistent
  15.   private Integer sales;
  16.   @Persistent
  17.   private Integer price;
  18. }
A custom constraint annotation BookConstraint has been added to the title attribute, in order to evaluate if the book already exists in the datastore. You can find additional information about writing custom constraint in Spring documentation.
BookConstraint.java
  1. @Documented
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Target({ElementType.METHOD,ElementType.FIELD,ElementType.ANNOTATION_TYPE })
  4. @Constraint(validatedBy = BookValidator.class)
  5. public @interface BookConstraint {
  6.     String message() default "{some.error.code}";
  7.     Class<?>[] groups() default {};
  8.     Class<? extends Payload>[] payload() default {};
  9. }
BookValidatior.java
  1. public class BookValidator implements ConstraintValidator<BookConstraint,String> {
  2.   @Autowired
  3.   private BookDao bookDao;
  4.   @Override
  5.   public void initialize(BookConstraint bookConstraint) {
  6.   }
  7.   @Override
  8.   public boolean isValid(String title, ConstraintValidatorContext context) {
  9.     return bookDao.findByTitle( title ).size() < 2;
  10.   }
  11. }
After validation the BookController receives an instance of the Book class initialized with the form data and a BindingResult object containing the errors detected.
BookController.java
  1. @Controller
  2. public class BookController {
  3.     @Autowired
  4.     private BookService bookService;
  5.     @RequestMapping(value = "/book", method = RequestMethod.POST)
  6.     public @ResponseBody DefaultFormAdapter
  7.   storeBook(@Valid @ModelAttribute Book book, BindingResult binding) {
  8.         DefaultFormAdapter adapter = new DefaultFormAdapter(book,binding);
  9.         if( adapter.hasValidData() ) {
  10.             bookService.store(book);
  11.         }
  12.         return adapter;
  13.     }
  14. }
The dhtmlxForm posts the data performing an Ajax request. To return the errors triggered by the validation, the DHTMLX Spring Adapter creates an XML message. Below a XML response  example including one error message:
  1. <data>
  2.   <action
  3.    sid="42"
  4.    type="invalid"
  5.    field="title"
  6.    message="error !">
  7.   </action>
  8. </data>
On client side, the Javascript function getErrors parses the response message and sets the text on the error labels.
  1. function getErrors(form,response) {
  2.   var errorList = new Array();
  3.   $('action', response).each(function(i) {
  4.         field = $(this).attr("field");
  5.         if( field != undefined ) {
  6.             message = $(this).attr("message");
  7.             form.setValidateCss(field, false);
  8.             form.setItemLabel(field + "Error" , message );
  9.             errorList.push( field );
  10.         }
  11.   });
  12.   return errorList;
  13. }
  14. form.attachEvent("onButtonClick", function(id) {
  15.   if (id == "button_upd") {
  16.     clearMessages();
  17.     form.send('/app/book', function(loader, response) {
  18.           errorList = getErrors( form , response );
  19.     });
  20.   }
  21. });


For those who like sequence diagrams see below.




In the next episode, the design of the DHTMLX Spring adapter will be presented in detail. For those who can't wait the application, running on the Google App Engine, is available  here.

Stay Tuned!