Wednesday, September 21, 2011

DHTMLX Tags on Gaelyk


About a month ago the final release of Gaelyk 1.0, the lightweight Groovy toolkit for Google App Engine, has been released. I'm a fan of the Groovy programming language, even if it is challenging to involve customers in this paradigm shift. This post illustrates a simple CRUD  application, (available here), written with Gaelyk and the DHTMLX Tag libraryThere is a school of thought that considers tag libraries to be evil and another one which does not. Between the two schools there are those who believe that tag libraries can be useful, if you don't turn them into a monster. In a discussion about Gaelyk and tag libraries support, Guillaume Laforge, made a very smart point about this subject (see here):
Gaelyk templates are really just one view option. You can still continue using Groovlets, but delegate to JSP views, using JSP taglibs. Gaelyk doesn't try to reinvent the wheel here
Gaelyk provides templates (GTPL)  similar to JSPs, which are pages containing scriptlets of code. In the same way in which you can mix Java and Groovy code for classes, Gaelyk let you use a combination of GTPL and JSP pages. 
In this sample application there are two pages, the first one based on a dhtmlxGrid,  which presents the list of records, and second uses a dhtmlxForm to display and edit the data.See screenshot here
Both pages have been created using the DHTMLX Tag Designer, which makes it pretty simple to prototype the layout. 
To take the advantage of Gaelyk templates the JSPs are included within the GTPL: 


war/WEB-INF/pages/index.gtpl
  1. <% include '/WEB-INF/includes/header.gtpl' %>
  2. <% include '/WEB-INF/jsp/list.jsp' %>
  3. <% include '/WEB-INF/includes/footer.gtpl' %>


war/WEB-INF/pages/edit.gtpl
  1. <% include '/WEB-INF/includes/header.gtpl' %>
  2. <% include '/WEB-INF/jsp/form.jsp' %>
  3. <% include '/WEB-INF/includes/footer.gtpl' %>


The section about the grid page is missing, because it is  very similar to a previous post, and the focus here is on the form based edit page. The source code is below:

war/WEB-INF/jsp/form.jsp
  1. <%@ taglib uri="http://www.mylaensys.com/dhtmlx" prefix="dhtmlx" %>
  2. <dhtmlx:body name='initializeDHTMLX' imagePath='/imgs/'>
  3.   <dhtmlx:layout name='layout' id='content' pattern='1C'>
  4.      <dhtmlx:layoutcell name='a' text='a' i18n='false' hideHeader='true'>
  5.         <dhtmlx:toolbar name='toolbar'>
  6.            <dhtmlx:toolbarButton id='button_list' text='List'/>
  7.             </dhtmlx:toolbar>
  8.             <dhtmlx:form name='form'>
  9.               <dhtmlx:formInputFieldSet name='fieddset' label='Book Edit'>
  10.                   <dhtmlx:formHidden name='id' value="0"/>
  11.                   <dhtmlx:formLabel name="message" label=""/>
  12.                   <dhtmlx:formInput name='author' label='Author'/>
  13.                   <dhtmlx:formInput name='price' label='Price'/>
  14.                   <dhtmlx:formInput name='sales' label='Sales'/>
  15.                   <dhtmlx:formInput name='title' label='Title'/>
  16.                   <dhtmlx:formButton name='button_upd' value='Update'/>
  17.               </dhtmlx:formInputFieldSet>
  18.             </dhtmlx:form>
  19.         </dhtmlx:layoutcell>
  20.     </dhtmlx:layout>
  21. </dhtmlx:body>
  22. <script language='JavaScript' type='text/javascript'>
  23.     var errorMessage = "";
  24.     var bookId = '<%= request.getAttribute("id") %>';
  25.     function custom_NotEmpty(value) {
  26.         if (!dhtmlxValidation.isNotEmpty(value)) {
  27.             return " must not be empty";
  28.         }
  29.         return true;
  30.     }
  31.     function custom_ValidNumeric(value) {
  32.         if (!dhtmlxValidation.isValidNumeric(value)) {
  33.             return "must be a numeric";
  34.         }
  35.         return true;
  36.     }
  37.     function custom_ValidInteger(value) {
  38.         if (!dhtmlxValidation.isValidInteger(value)) {
  39.             return "must be an integer";
  40.         }
  41.         return true;
  42.     }
  43.     function initialize() {
  44.         initializeDHTMLX();
  45.         toolbar.attachEvent("onClick", on_click);
  46.         form.attachEvent("onButtonClick", function(id) {
  47.             if (id == "button_upd") {
  48.                 if (form.validate()) {
  49.                     toolbar.disableItem("button_list");
  50.                     form.setItemLabel('message', '<div class="message">Saving...</div>');
  51.                     form.save();
  52.                 } else {
  53.                     form.setItemLabel('message', '<div class="error">' + errorMessage + '</div>');
  54.                 }
  55.             }
  56.         });
  57.         form.bindValidator("author", "custom_NotEmpty");
  58.         form.bindValidator("price", "custom_ValidNumeric");
  59.         form.bindValidator("sales", "custom_ValidInteger");
  60.         form.bindValidator("title", "custom_NotEmpty");
  61.         form.attachEvent("onBeforeValidate", function() {
  62.             errorMessage = "";
  63.             return true;
  64.         });
  65.         form.attachEvent("onValidateError", function(obj, value, res) {
  66.             errorMessage += obj.name.toUpperCase() + " : " + res + " ";
  67.         });
  68.   
  69.         var dp = new dataProcessor("/book/processor");
  70.         dp.setTransactionMode("POST");
  71.         dp.init(form);
  72.         dp.attachEvent("onAfterUpdate", function(sid, action, tid, tag) {
  73.             form.load("/book/show/" + bookId);
  74.             form.setItemLabel('message', '<div class="message">Record Saved</div>');
  75.             toolbar.enableItem("button_list");
  76.         });
  77.         form.load("/book/show/" + bookId);
  78.     }
  79.     function on_click(id) {
  80.         if ("button_list" == id) {
  81.             window.location.href = encodeURI('/');
  82.         }
  83.     }
  84.     dhtmlxEvent(window, 'load', initialize);
  85. </script>


The initialization attaches a handler to the onButtonClick form button event, which updates the message on top of the form, and posts the save operation to the application. The second step of the code sets up the validation, binding  validators to the object form fields, and provides the onBeforeValidate and onValidateError event handlers to update the text message box.
The final step invokes the load method on the form, it triggers an Ajax call to the book Groovlet to retrieve the data and fill the form. 
The routes.groovy defines the URL mapping :

war/WEB-INF/routes.groovy
  1. get  "/", forward: "/WEB-INF/pages/index.gtpl"
  2. get  "/book/@task/@id", forward: "/book.groovy?id=@id&task=@task"
  3. post "/book/processor", forward: "/processor.groovy"


In the case above, path variables are translated in order to route different task to a single Groovlet. The book.groovy Groovlet performs the list, show and edit operations.

war/WEB-INF/groovy/book.groovy
  1. import com.google.appengine.api.datastore.Entity
  2. import com.google.appengine.api.datastore.Key
  3. import com.google.appengine.api.datastore.KeyFactory
  4. switch (params."task") {
  5.   case "list":
  6.    log.info "list : getting book list "
  7.    int entityCount = datastore.execute { select count from books }
  8.    params.offset = params.posStart ? Integer.parseInt( params.posStart ) : 0
  9.    params.max = params.count ? Integer.parseInt( params.count) : 20
  10.    params.sort
  11. = params.orderby ? params.orderby : "sales"
  12.    params.order = params.dir ? (params.dir == "des" ? "desc" : "asc") : "asc"
  13.         def books = datastore.execute {
  14.             from books
  15.             limit params.max offset params.offset
  16.             sort params.order by params.sort
  17.         }
  18.         response.setContentType("text/xml")
  19.         html.rows(total_count: entityCount , pos: params.offset ) {
  20.             books.each { book ->
  21.                 html.row(id: book.key.id) {
  22.                     html.cell( book.sales )
  23.                     html.cell( book.title )
  24.                     html.cell( book.author )
  25.                     html.cell( book.price )
  26.                 }
  27.             }
  28.         }
  29.         break
  30.     case "edit":
  31.         log.info "edit: editing a book"
  32.         request.setAttribute 'id', params.id
  33.         forward '/WEB-INF/pages/edit.gtpl'
  34.         break
  35.     case "show":
  36.         log.info "show: getting book data"
  37.         def id = Long.parseLong(params.id )
  38.         Key key = KeyFactory.createKey("books", id)
  39.         def book = datastore.get(key)
  40.         response.setContentType("text/xml")
  41.         html.data {
  42.               author {
  43.                 mkp.yieldUnescaped("<![CDATA[" + book.author + "]]>")
  44.               }
  45.               price {
  46.                 mkp.yieldUnescaped("<![CDATA[" + book.price + "]]>")
  47.               }
  48.               sales {
  49.                 mkp.yieldUnescaped("<![CDATA[" + book.sales + "]]>")
  50.               }
  51.               title {
  52.                 mkp.yieldUnescaped("<![CDATA[" + book.title + "]]>")
  53.               }
  54.             }
  55.         break
  56.     default:
  57.         break
  58. }


  59. The data processor POST requests are routed to the Groovlet named processor.groovy, which handles the insert, update, delete operations, and persist changes to the entity. Gaelyk’s abstractions for the datastore make them quite straight forward to implement.


    war/WEB-INF/groovy/processor.groovy
    1. import com.google.appengine.api.datastore.Entity
    2. import com.google.appengine.api.datastore.Key
    3. import com.google.appengine.api.datastore.KeyFactory
    4. response.setContentType("text/xml")
    5. switch (params."!nativeeditor_status") {
    6.     case "inserted":
    7.         log.info "inserted: inserting a book"
    8.         def book  = new Entity("books")
    9.         book << params.subMap(['sales', 'title','author','price'])
    10.         book.save()
    11.         html.data {
    12.             action(type: "insert", sid: book.key.id, tid: book.key.id)
    13.         }
    14.         break
    15.     case "updated":
    16.         log.info "updated: updating a book"
    17.         def id = Long.parseLong( params.id ? params.id : params.gr_id )
    18.         Key key = KeyFactory.createKey("books", id)
    19.         def book = datastore.get(key)
    20.         book << params.subMap(['sales', 'title','author','price'])
    21.         book.save()
    22.         html.data {
    23.             action(type: "update", sid: book.key.id, tid: book.key.id)
    24.         }
    25.         break
    26.     case "deleted":
    27.         log.info "deleted: deleting a book"
    28.         def key = ['books', params.gr_id] as Key
    29.         key.delete()
    30.         html.data {
    31.             action(type: "delete", sid: params.gr_id, tid: params.gr_id)
    32.         }
    33.         break
    34.      default:
    35.         break
    36.  }




    Enjoy.



Tuesday, September 6, 2011

Rich web Interfaces with DHTMLX and Google App Engine


Developers building applications on top of the Google App Engine for Java can choose between several frameworks to build rich user interfaces. This post illustrates how to create a simple CRUD application, using DHTMLX Java Tag Library and JPA. The sample provided starts by creating a UI skeleton, which incorporates the DHTMLX widgets, with the use of JavaScript, to integrate the widgets with server side code.


The User Interface 



The application major features are:
  • Dynamic Loading 
    Keeping thousand of records in a data grid is a common requirement for most applications. Smart Rendering increases overall performance with big amounts of data, activating a dynamic loading to fetch data from the server when needed.

  • Edit in place
    In the great book Designing Web Interface, written by Bill Scott and Theresa Neil, they underline the value of the Make It Direct principle, allowing the user to directly edit content in place.
  • Right Click Context Menu
    The Fitts's Law highlights the value to keep the tools close, to improve the user interaction. This principle has been applied providing a  context menu, so that the user can select a row and access the related functions (delete and insert in this case) using the right click.

Client 
To create the user interface the DHTMLX Java Tag Designer has been used (useful but not mandatory). You can find step by step instructions here. Below the HTML code of  the page.


  1. <%@ taglib uri="http://www.mylaensys.com/dhtmlx" prefix="dhtmlx" %>
  2. <html>
  3.  <head>
  4.   <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
  5.   <title></title>
  6.   <link href="dhtmlx.css" rel="stylesheet" type="text/css" />
  7.   <link href="dhtmlx_custom.css" rel="stylesheet" type="text/css" />
  8.   <style></style>          
  9.  </head>
  10.  <script type="text/javascript" src="dhtmlx.js"/>
  11.  <body>
  12.      <!-- body -->
  13.  </body>
  14. </html>
  15. <dhtmlx:body name='initializeDHTMLX' imagePath='imgs/'>
  16.   <dhtmlx:layout name='layout' id='content'  pattern='1C' >
  17.     <dhtmlx:layoutcell name='a' text='a' hideHeader='true'>
  18.       <dhtmlx:toolbar  name='toolbar'>
  19.         <dhtmlx:toolbarButton  id='button_ins' text='Insert Row'/>
  20.         <dhtmlx:toolbarButton  id='button_del' text='Delete Row'/>
  21.       </dhtmlx:toolbar>
  22.       <dhtmlx:grid   name='grid'>
  23.         <dhtmlx:column  name='sales' header='Sales' type='ed'/>
  24.         <dhtmlx:column  name='title' header='Title' type='ed'/>
  25.         <dhtmlx:column  name='author' header='Author' type='ed'/>
  26.         <dhtmlx:column  name='price' header='Price' type='ed'/>
  27.         <dhtmlx:menu name='grid_menu'>
  28.           <dhtmlx:menuChild  id='button_ins' text='Insert Row'/>
  29.           <dhtmlx:menuChild  id='button_del' text='Delete Row'/>
  30.         </dhtmlx:menu >
  31.      </dhtmlx:grid>
  32.      <dhtmlx:statusbar name="status"/>
  33.     </dhtmlx:layoutcell>
  34.   </dhtmlx:layout>
  35. </dhtmlx:body>
  36. <script language='JavaScript' type='text/javascript'>
  37.     function initialize() {
  38.         initializeDHTMLX();
  39.     }
  40.     dhtmlxEvent(window,'load', initialize);
  41. </script>

The user interface is declared within the <dhtmlx:body> tags, using a Layout component as container for the Toolbar,  Status Bar, and Grid. 


  1. var busy = false,sort_c = "",sort_d = "";
  2. function initialize() {
  3.    initializeDHTMLX();
  4.    toolbar.attachEvent("onClick", on_click );
  5.    grid_menu.attachEvent("onClick", on_click );
  6.    grid.attachEvent("onBeforeSorting", function(ind,type,direction){
  7.       if(!busy) {
  8.          sort_c = this.getColumnId(ind);
  9.          sort_d = ((sort_d == "des") ? "asc": "des");
  10.          load_data();
  11.          grid.setSortImgState(true,ind,direction);
  12.       }
  13.       return false;
  14.    });
  15.    grid.enableSmartRendering(true);
  16.    grid.enableValidation(true, true, true, true);
  17.    grid.setColValidators("ValidInteger,NotEmpty,NotEmpty,ValidInteger");
  18.    load_data();
  19.    dp = new dataProcessor("controller");
  20.    dp.setTransactionMode("POST");
  21.    dp.setUpdateMode("cell");
  22.    dp.enableDataNames(true);
  23.    dp.init(grid);
  24. }

In first step, the event handlers for toolbar and menu are attached to the components. The initialization proceed with the grid setup, enabling the SmartRendering, setting up the validation, and loading the data. OnBeforeSorting event handler attached to the grid  provides the support for server side sort processing.   
Last step of initialization, is the data processor configuration, which takes care to send back to the server updates that occurred on the grid; calling enableDataNames ensures that the column names will be included as parameters in the POST request. 
The two additional functions defined for toolbar/menu command handling  (on_click) and data loading (load_data) are visible below :

  1. function load_data() {
  2.    if( !busy ) {
  3.       grid.clearAll();
  4.       grid.loadXML("controller?orderby="+sort_c+"&dir="+sort_d);
  5.    }
  6. }
  7. function on_click(id) {
  8.    var selected = grid.getSelectedRowId();
  9.    if( null != selected) {
  10.       if( "button_ins" == id ) {
  11.   grid.addRow((new Date()).valueOf(),[0,'','',0],grid.getRowIndex(selected));
  12.       } else if( "button_del" == id ) {
  13.          var answer = confirm("Are you sure ?")
  14.          if (answer){
  15.             grid.deleteRow(selected);
  16.          }
  17.       }
  18.    }
  19. }


The load_data function resets the grid component  and sends an ajax  request (GET) to the server to retrieve the data. The on_click detects which button or menu item has been selected by the user and performs the corresponding operation.


Server
On the server side, the Java class Book is annotated for persistence, getter and setter omitted for short. You can see that the names of the attributes match the names of the columns.
  1. @PersistenceCapable(detachable = "true")
  2. public class Book {
  3.     @PrimaryKey
  4.     @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  5.     private Long id;
  6.     @Persistent
  7.     private Integer sales;
  8.     @Persistent
  9.     private String title;
  10.     @Persistent
  11.     private String author;
  12.     @Persistent
  13.     private BigDecimal price;
  14. }


The doGet method of the servlet retrieves the data and feeds the grid. 

  1. public class ControllerServlet extends HttpServlet {
  2.  @Override
  3.  public void doGet(HttpServletRequest request,
                       HttpServletResponse response) throws IOException {
  4.       EntityManager em = EMF.get().createEntityManager();
  5.       try {
  6.          Integer start = new Integer(0);
  7.          Integer count = new Integer(maxrows);
  8.          Integer total = new Integer(0);
  9.          if (!isEmpty(request.getParameter("posStart"))) {
  10.           start = Integer.parseInt(request.getParameter("posStart"));
  11.          }
  12.          if (!isEmpty(request.getParameter("count"))) {
  13.            count = Integer.parseInt(request.getParameter("count"));
  14.            count = count > maxrows ? maxrows : count;
  15.          }
  16.          if (start.intValue() == 0) {
  17.            Query query = em.createQuery("select count(b) from " +
               Book.class.getName() + " b");
  18.            total = (Integer) query.getSingleResult();
  19.          }
  20.          String orderBy = getOrderBy(
             request.getParameter("orderby"), request.getParameter("dir")
             );
  21.          Query query = em.createQuery("select from " +
             Book.class.getName() + orderBy );
  22.          query.setFirstResult(start);
  23.          query.setMaxResults(count);
  24.          List<Book> books = query.getResultList();
  25.          response.setContentType("text/xml");
  26.          response.getWriter().print( toXML(total, start, books) );
  27.          response.getWriter().close();
  28.      } finally {
  29.          em.close();
  30.      }
  31.   }
  32.  }
  33. }


The smart rendering option, enabled during grid initialization, adds as parameters the starting position of the record (posStart) and the number of records to be returned (count). The doGet method processes these parameters plus sort parameters, if any, executes the query on the data store, and returns the retrieved rows as XML. 
Update operations are performed in the doPost method of the ControllerServlet :


  1. public class ControllerServlet extends HttpServlet {
  2.   @Override
  3.   protected void doPost(HttpServletRequest request,
                            HttpServletResponse response)
  4.      EntityManager em = EMF.get().createEntityManager();
  5.      try {
  6.         String action = "";
  7.         String id = request.getParameter("gr_id");
  8.         String type = request.getParameter("!nativeeditor_status");
  9.         Book book = new Book();
  10.         if ("inserted".equalsIgnoreCase(type)) {
  11.            action = "insert";
  12.            BeanUtils.populate(book, request.getParameterMap());
  13.            em.persist(book);
  14.            em.refresh(book);
  15.         } else {
  16.            Query query = em.createQuery("select from " + Book.class.getName()
                             + " where id = " + id);
  17.            book = (Book) query.getSingleResult();
  18.            if ("updated".equalsIgnoreCase(type)) {
  19.                action = "update";
  20.                BeanUtils.populate(book, request.getParameterMap());
  21.                em.persist(book);
  22.            } else if ("deleted".equalsIgnoreCase(type)) {
  23.                action = "delete";
  24.                em.remove(book);
  25.            }
  26.       }
  27.       if (!isEmpty(action)) {
  28.           response.setContentType("text/xml");
  29.           response.getWriter().print("<data><action type='" + action + "' sid='" + id + "' tid='" + book.getId() + "' /></data>");
  30.           response.getWriter().close();
  31.       }
  32.    } catch (Exception e) {
  33.        e.printStackTrace();
  34.    } finally {
  35.         em.close();
  36.    }
  37.  }


DHTMLX data processor component has its own protocol to exchange information with the server (additional information is available on DHTMLX website). This sample implementation, detects the operation triggered by the data processor and performs the appropriate data store operation.