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.
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:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="com.mylaensys.dhtmlx.adapter.DhtmlxHttpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
Below the code of the message converter.
DhtmlxHttpMessageConverter.java
public class DhtmlxHttpMessageConverter
extends AbstractHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
public DhtmlxHttpMessageConverter() {
super(new MediaType("text", "xml", DEFAULT_CHARSET));
}
@Override
protected boolean supports(Class<?> clazz) {
Class[] theInterfaces = clazz.getInterfaces();
for (int i = 0; i < theInterfaces.length; i++) {
if( theInterfaces[i].getName().equalsIgnoreCase( Adapter.class.getName() ) ) {
return true;
}
}
return false;
}
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Adapter adapter = (Adapter)object;
outputMessage.getBody().write( adapter.toXML().getBytes() );
}
}
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
@Controller
public class BookController {
@Autowired
private BookService bookService;
@RequestMapping(value = "/books", method = RequestMethod.GET)
public @ResponseBody DefaultGridAdapter getBooks(@RequestParam("c") String c) {
DefaultGridAdapter adapter = new DefaultGridAdapter(c,Book.class);
adapter.setData( bookService.getBooks() );
return adapter;
}
}
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
public class DefaultGridAdapter extends AbstractAdapter implements Adapter {
private List data;
private List<String> columnList = new ArrayList<String>();
private GridInterceptor interceptor = new GridInterceptorImpl();
public DefaultGridAdapter(String columnList,Class clazz) {
this.fieldList = getObjectFields(clazz);
StringTokenizer st = new StringTokenizer(columnList,",");
while(st.hasMoreTokens()) {
this.columnList.add(st.nextToken());
}
}
public List getData() {
return data;
}
public void setData(List data) {
this.data = data;
}
public void setInterceptor(GridInterceptor interceptor) {
this.interceptor = interceptor;
}
@Override
public String toXML() {
StringBuffer buffer = new StringBuffer();
buffer.append("<?xml version='1.0' encoding='UTF-8'?>");
interceptor.onHeader(this,data,buffer);
interceptor.onStartRows(this, data, buffer);
for(Object object : data) {
interceptor.onStartRow(this, object, buffer);
for(String column : columnList ) {
if( fieldList.contains( column ) ) {
interceptor.onRenderCell(this, object, column, buffer);
}
}
interceptor.onEndRow(buffer);
}
interceptor.onEndRows(buffer);
interceptor.onOutput(buffer);
return buffer.toString();
}
}
The toXML method invokes the interceptor which contains the logic for the XML generation. Below the default implementation of the GridInteceptor.
GridInterceptorImpl.java
public class GridInterceptorImpl implements GridInterceptor {
public void onHeader(AbstractAdapter adapter, List list, StringBuffer buffer) {
}
public void onStartRows(AbstractAdapter adapter, List list, StringBuffer buffer) {
buffer.append("<rows>");
}
public void onEndRows(StringBuffer buffer) {
buffer.append("</rows>");
}
public void onStartRow(AbstractAdapter adapter, Object object, StringBuffer buffer) {
buffer.append("<row id='").append( adapter.getPrimaryKey(object).toString() ).append("'>");
}
public void onEndRow(StringBuffer buffer) {
buffer.append("</row>");
}
public void onRenderCell(AbstractAdapter adapter, Object object, String column, StringBuffer buffer) {
Object value = adapter.getObjectValue(object, column);
buffer.append("<cell><![CDATA[").append( value.toString() ).append("]]></cell>");
}
public void onOutput(StringBuffer buffer) {
}
}
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.
public class GridInterceptorHighLight extends DefaultGridInterceptor {
public void onStartRow(AbstractAdapter adapter, Object object, StringBuffer buffer) {
if( object instanceof Book) {
Book book = (Book)object;
if( book.getPrice() > 10 ) {
buffer.append("<row id='")
.append( adapter.getPrimaryKey(object).toString() )
.append("' style='color:red;'>");
} else {
buffer.append("<row id='")
.append( adapter.getPrimaryKey(object).toString() )
.append("'>");
}
}
}
}
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
public class DefaultFormAdapter extends AbstractAdapter implements Adapter {
public static final String Insert = "inserted";
public static final String Update = "updated";
public static final String Delete = "deleted";
private Object data;
private String operation;
public DefaultFormAdapter(Object data) {
initialize( data );
this.operation = "";
}
public DefaultFormAdapter(Object data,String operation) {
initialize( data );
this.operation = operation;
}
public DefaultFormAdapter(Object data,BindingResult binding) {
initialize(data);
if( binding.hasErrors() ) {
for( FieldError e : binding.getFieldErrors() ) {
this.errorList.add(new ErrorMessage(e.getField(), e.getDefaultMessage()));
}
}
}
private void initialize(Object data) {
this.fieldList = getObjectFields(data.getClass());
Object id = getPrimaryKey( data );
if( id == null ) {
this.operation = Insert;
} else {
this.operation = Update;
}
this.data = data;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
private boolean isWriting() {
return Insert.equalsIgnoreCase( this.operation ) || Update.equalsIgnoreCase( this.operation ) || Delete.equalsIgnoreCase( this.operation ) ;
}
public boolean hasValidData() {
return errorList.size() == 0;
}
public String toErrorXML() {
StringBuffer buffer = new StringBuffer();
String id = getPrimaryKey( data ).toString();
buffer.append("<data>");
for( ErrorMessage e : errorList ) {
buffer.append("<action sid='").append( id == null ? "" : id.toString() ).append( "' ");
buffer.append("type='invalid' ");
buffer.append("field='" + e.getField() + "' ");
buffer.append("message='" ).append( e.getMessage() ).append("'/>");
}
buffer.append("</data>");
return buffer.toString();
}
@Override
public String toXML() {
StringBuffer buffer = new StringBuffer();
if( isWriting() ) {
if( this.errorList.size() == 0 ) {
buffer.append( toStoreXML() );
} else {
buffer.append( toErrorXML() );
}
} else {
buffer.append(toDataXML());
}
return buffer.toString();
}
private String toDataXML() {
StringBuffer buffer = new StringBuffer();
buffer.append("<?xml version='1.0' encoding='UTF-8'?>");
buffer.append("<data>");
try {
if( data != null ) {
for(String field : fieldList) {
Object value = getObjectValue(data, field);
buffer.append("<").append(field).append("><![CDATA[").append( value.toString() ).append( "]]></").append(field).append(">");
}
}
} catch (Exception e) {
log.severe(e.getMessage());
}
buffer.append("</data>");
return buffer.toString();
}
public String toStoreXML() {
StringBuffer buffer = new StringBuffer();
buffer.append("<data>");
buffer.append("<action type='").append( this.operation ).append("' ");
if( data != null ) {
String id = getPrimaryKey( data ).toString();
buffer.append("sid='").append( id ).append("' ");
buffer.append("tid='").append( id ).append("'/>");
} else {
buffer.append("field='id' ");
buffer.append("sid='").append( "0" ).append("' ");
buffer.append("tid='").append( "0" ).append("' ");
buffer.append("message='" ).append( "invalid data " ).append("'/>");
}
buffer.append("</data>");
return buffer.toString();
}
}
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
public class BookController {
@Autowired
private BookService bookService;
@RequestMapping(value = "/books/{id}", method = RequestMethod.GET)
public @ResponseBody DefaultFormAdapter getBook(@PathVariable("id") final String id) {
Book book = bookService.getBook(id);
DefaultFormAdapter adapter = new DefaultFormAdapter( book );
return adapter;
}
@RequestMapping(value = "/book", method = RequestMethod.POST)
public @ResponseBody DefaultFormAdapter storeBook(@Valid @ModelAttribute Book book, BindingResult binding) {
DefaultFormAdapter adapter = new DefaultFormAdapter(book,binding);
if( adapter.hasValidData() ) {
bookService.store(book);
}
return adapter;
}
@RequestMapping(value = "/book/{id}", method = RequestMethod.DELETE)
public @ResponseBody DefaultFormAdapter deleteBook(@PathVariable("id") final String id) {
Book book = bookService.getBook(id);
DefaultFormAdapter adapter = new DefaultFormAdapter(book,DefaultFormAdapter.Delete);
bookService.remove(id);
return adapter;
}
- }
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!