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 library. There 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: 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.
war/WEB-INF/pages/index.gtpl
- <% include '/WEB-INF/includes/header.gtpl' %>
- <% include '/WEB-INF/jsp/list.jsp' %>
- <% include '/WEB-INF/includes/footer.gtpl' %>
war/WEB-INF/pages/edit.gtpl
- <% include '/WEB-INF/includes/header.gtpl' %>
- <% include '/WEB-INF/jsp/form.jsp' %>
- <% 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
- <%@ taglib uri="http://www.mylaensys.com/dhtmlx" prefix="dhtmlx" %>
- <dhtmlx:body name='initializeDHTMLX' imagePath='/imgs/'>
- <dhtmlx:layout name='layout' id='content' pattern='1C'>
- <dhtmlx:layoutcell name='a' text='a' i18n='false' hideHeader='true'>
- <dhtmlx:toolbar name='toolbar'>
- <dhtmlx:toolbarButton id='button_list' text='List'/>
- </dhtmlx:toolbar>
- <dhtmlx:form name='form'>
- <dhtmlx:formInputFieldSet name='fieddset' label='Book Edit'>
- <dhtmlx:formHidden name='id' value="0"/>
- <dhtmlx:formLabel name="message" label=""/>
- <dhtmlx:formInput name='author' label='Author'/>
- <dhtmlx:formInput name='price' label='Price'/>
- <dhtmlx:formInput name='sales' label='Sales'/>
- <dhtmlx:formInput name='title' label='Title'/>
- <dhtmlx:formButton name='button_upd' value='Update'/>
- </dhtmlx:formInputFieldSet>
- </dhtmlx:form>
- </dhtmlx:layoutcell>
- </dhtmlx:layout>
- </dhtmlx:body>
- <script language='JavaScript' type='text/javascript'>
- var errorMessage = "";
- var bookId = '<%= request.getAttribute("id") %>';
- function custom_NotEmpty(value) {
- if (!dhtmlxValidation.isNotEmpty(value)) {
- return " must not be empty";
- }
- return true;
- }
- function custom_ValidNumeric(value) {
- if (!dhtmlxValidation.isValidNumeric(value)) {
- return "must be a numeric";
- }
- return true;
- }
- function custom_ValidInteger(value) {
- if (!dhtmlxValidation.isValidInteger(value)) {
- return "must be an integer";
- }
- return true;
- }
- function initialize() {
- initializeDHTMLX();
- toolbar.attachEvent("onClick", on_click);
- form.attachEvent("onButtonClick", function(id) {
- if (id == "button_upd") {
- if (form.validate()) {
- toolbar.disableItem("button_list");
- form.setItemLabel('message', '<div class="message">Saving...</div>');
- form.save();
- } else {
- form.setItemLabel('message', '<div class="error">' + errorMessage + '</div>');
- }
- }
- });
- form.bindValidator("author", "custom_NotEmpty");
- form.bindValidator("price", "custom_ValidNumeric");
- form.bindValidator("sales", "custom_ValidInteger");
- form.bindValidator("title", "custom_NotEmpty");
- form.attachEvent("onBeforeValidate", function() {
- errorMessage = "";
- return true;
- });
- form.attachEvent("onValidateError", function(obj, value, res) {
- errorMessage += obj.name.toUpperCase() + " : " + res + " ";
- });
- var dp = new dataProcessor("/book/processor");
- dp.setTransactionMode("POST");
- dp.init(form);
- dp.attachEvent("onAfterUpdate", function(sid, action, tid, tag) {
- form.load("/book/show/" + bookId);
- form.setItemLabel('message', '<div class="message">Record Saved</div>');
- toolbar.enableItem("button_list");
- });
- form.load("/book/show/" + bookId);
- }
- function on_click(id) {
- if ("button_list" == id) {
- window.location.href = encodeURI('/');
- }
- }
- dhtmlxEvent(window, 'load', initialize);
- </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
- get "/", forward: "/WEB-INF/pages/index.gtpl"
- get "/book/@task/@id", forward: "/book.groovy?id=@id&task=@task"
- 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.
- import com.google.appengine.api.datastore.Entity
- import com.google.appengine.api.datastore.Key
- import com.google.appengine.api.datastore.KeyFactory
- switch (params."task") {
- case "list":
- log.info "list : getting book list "
- int entityCount = datastore.execute { select count from books }
- params.offset = params.posStart ? Integer.parseInt( params.posStart ) : 0
- params.max = params.count ? Integer.parseInt( params.count) : 20
- params.sort
= params.orderby ? params.orderby : "sales"
- params.order = params.dir ? (params.dir == "des" ? "desc" : "asc") : "asc"
- def books = datastore.execute {
- from books
- limit params.max offset params.offset
- sort params.order by params.sort
- }
- response.setContentType("text/xml")
- html.rows(total_count: entityCount , pos: params.offset ) {
- books.each { book ->
- html.row(id: book.key.id) {
- html.cell( book.sales )
- html.cell( book.title )
- html.cell( book.author )
- html.cell( book.price )
- }
- }
- }
- break
- case "edit":
- log.info "edit: editing a book"
- request.setAttribute 'id', params.id
- forward '/WEB-INF/pages/edit.gtpl'
- break
- case "show":
- log.info "show: getting book data"
- def id = Long.parseLong(params.id )
- Key key = KeyFactory.createKey("books", id)
- def book = datastore.get(key)
- response.setContentType("text/xml")
- html.data {
- author {
- mkp.yieldUnescaped("<![CDATA[" + book.author + "]]>")
- }
- price {
- mkp.yieldUnescaped("<![CDATA[" + book.price + "]]>")
- }
- sales {
- mkp.yieldUnescaped("<![CDATA[" + book.sales + "]]>")
- }
- title {
- mkp.yieldUnescaped("<![CDATA[" + book.title + "]]>")
- }
- }
- break
- default:
- break
- }
- import com.google.appengine.api.datastore.Entity
- import com.google.appengine.api.datastore.Key
- import com.google.appengine.api.datastore.KeyFactory
- response.setContentType("text/xml")
- switch (params."!nativeeditor_status") {
- case "inserted":
- log.info "inserted: inserting a book"
- def book = new Entity("books")
- book << params.subMap(['sales', 'title','author','price'])
- book.save()
- html.data {
- action(type: "insert", sid: book.key.id, tid: book.key.id)
- }
- break
- case "updated":
- log.info "updated: updating a book"
- def id = Long.parseLong( params.id ? params.id : params.gr_id )
- Key key = KeyFactory.createKey("books", id)
- def book = datastore.get(key)
- book << params.subMap(['sales', 'title','author','price'])
- book.save()
- html.data {
- action(type: "update", sid: book.key.id, tid: book.key.id)
- }
- break
- case "deleted":
- log.info "deleted: deleting a book"
- def key = ['books', params.gr_id] as Key
- key.delete()
- html.data {
- action(type: "delete", sid: params.gr_id, tid: params.gr_id)
- }
- break
- default:
- break
- }
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
Enjoy.