Saturday, July 29, 2006

AJAX Component with DWR and Velocity

I have been meaning to give the DWR (Direct Web Remoting) AJAX toolkit a shot for some time now. I consider myself a first generation AJAX programmer (as someone who has used XmlHttpRequest), but since I dont know any of the newer AJAX toolkits such as Prototype and DOJO, I just dont get no respect from the hotshot AJAX types (that was a joke BTW). Apart from that, considering that any session remotely related to AJAX played to overflowing crowds in this year's JavaOne, this was something I should have looked at quite some time ago. But since I dont do too much front end development, this was not something I needed to know, so I let it slide. So I finally got around to looking at DWR, and in this article, I describe an AJAX component using DWR and Velocity that can be served up within a portal-style page.

The component provides CRUD (Create, Retrieve, Update and Delete) functionality for a business object. The component is modelled as a state machine. The state diagram is shown below, where the nodes are views provided by the component, and the edges are the operations that are permitted on it.

DWR works by creating Javascript proxies for Java beans that are available to the servlet context. Methods can be called on the proxies in Javascript just as if they were regular Java objects. Each Javascript method call needs to provide an additional callback method parameter, which defines what to do with the result once it is available from the backend. This is because the calls are asynchronous (the A in AJAX) and the Javascript method call does not wait for the backend to respond. The callback method typically parses the return value of the method call and pops it into a span tag in the page.

In the example, I have used a BookReview bean (since I had some test data from one of my previous projects) but this strategy can be extended to allow any object to be exposed. Also, in my example, the service that provides data to the state machine is a local JDBC service, but could just as well have been a client talking to a remote webservice to get data.

I used Spring for the MVC framework, integrating DWR with it based on the instructions in Bram Smeet's weblog, and the DWR-Spring integration page on the DWR site. Unlike Bram Smeet's example however, where he uses Javascript to pull apart the bean returned from the backend service and populate the span tag, I went with the approach of using Velocity templates on the server to create HTML snippets and return the HTML, which the callback function then popped into the span tag. You would probably guess that I am no hotshot Javascript coder (and you'll be right), but the reason for this is more than just to avoid writing Javascript. So the reasons are, in no particular order:

  • Avoid having to write any more Javascript than absolutely necessary.
  • It is easier to unit test at the Java layer than the Javascript layer.
  • Java has had better tool support than Javascript (although thats changing).
  • Ability to cache Velocity templates on the server for performance.

Why Velocity? Well, since we are bypassing the standard request-response cycle using DWR, I could not use JSPs, since JSPs need to have a pageContext populated by the controller at the end of the request-response cycle. The other option was to have generated the HTML directly in the service classes using System.out.println() calls, but that would have taken us back to the dark ages of web programming. Velocity templates provide a clean separation of the view from the model without forcing us to participate in the HTTP request-response cycle. In the case of the webservice setup, the templates can live on the front end application, and the component look and feel can be tweaked without any changes to the webservice client. Even in the case of the basic setup, having templates is more maintainable, since we can change the presentation without affecting the underlying service layer.

Configuration

The configuration is based on information in Bram Smeet's weblog and the DWR-Spring integration pages, so there is nothing new here, I am just including it in here for completeness. I list below the contents of the web.xml, dwr.xml and the Spring comp-servlet.xml.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" >
<!-- WEB-INF/web.xml -->

<web-app>
  <display-name>DWR/Velocity Component Test</display-name>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <servlet>
    <servlet-name>comp</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet>
    <servlet-name>dwr-invoker</servlet-name>
    <servlet-class>uk.ltd.getahead.dwr.DWRServlet</servlet-class>
    <init-param>
      <param-name>debug</param-name>
      <param-value>true</param-value>
    </init-param>
  </servlet>

  <servlet-mapping>
    <servlet-name>comp</servlet-name>
    <url-pattern>*.do</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>dwr-invoker</servlet-name>
    <url-pattern>/dwr/*</url-pattern>
  </servlet-mapping>

</web-app>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE dwr PUBLIC "-//GetAhead Limited//DTD Direct Web Remoting 1.0//EN" "http://www.getahead.ltd.uk/dwr/dwr10.dtd">
<!-- WEB-INF/dwr.xml -->
<dwr>
  <allow>
    <create creator="new" javascript="JDate">
      <param name="class" value="java.util.Date" />
    </create>
    <create creator="spring" javascript="BookReviewService">
      <param name="beanName" value="bookReviewService" />
      <param name="location" value="classpath:comp-servlet.xml" />
    </create>
  </allow>
</dwr>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd" >
<!-- WEB-INF/comp-servlet.xml -->

  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/bookshelfdb" />
    <property name="username" value="root" />
    <property name="password" value="mysql" />
  </bean>
   <bean id="bookReviewService" class="org.component.services.BookReviewService">
    <property name="dataSource" ref="dataSource" />
  </bean>
   <bean id="bookReviewController" class="org.component.controllers.BookReviewController">
  <property name="service" ref="bookReviewService" />
  </bean>
   <bean id="simpleUrlHandlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
      <props>
        <prop key="main.do">bookReviewController</prop>
      </props>
    </property>
  </bean>

</beans>

The Service

The code for the BookReviewService class is shown below. It consists of a set of public methods that the Javascript proxy can call, all of which return a String. The mergeContent() method takes a bean and a template name and renders the bean into the template. The BookReview bean is a simple JavaBean holder of properties, and the BookReviewCollection is a wrapper over a List<BookReview> which also contains the current page number and the total number of pages that can be displayed. In the interests of keeping this blog post to a manageable size, neither of these beans are shown, but they are trivial to implement.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// BookReviewService.java
package org.component.services;

import java.io.StringWriter;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.component.beans.BookReview;
import org.component.beans.BookReviewCollection;
import org.springframework.jdbc.core.ColumnMapRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;

public class BookReviewService {

    public static final String DEFAULT_ORDER_BY = "name";

    private static final Logger log = Logger.getLogger(BookReviewService.class);
    private static final int NUM_ROWS_PER_PAGE = 5;
    private static final String TEMPLATE_DIR = "src/main/resources/templates";

    private DataSource dataSource;

    public BookReviewService() {
        super();
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public String getAllReviews(int page, String orderBy, boolean isOrderAscending) {
        log.debug("getAllReviews(page=" + page + ", orderBy=" + orderBy + ", isOrderAscending=" + isOrderAscending + ")");
        return getAllReviewsAndMergeToTemplate(page, orderBy, isOrderAscending, "all_reviews");
    }

    public String getAllReviewsAndMergeToTemplate(int page, String orderBy, boolean isOrderAscending, String templateFile) {
        log.debug("getAllReviewsAndMergeToTemplate(page=" + page + ", orderBy=" + orderBy + ", isOrderAscending=" + isOrderAscending + ", templateFile=" + templateFile + ")");
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        String limitStr = String.valueOf(page * NUM_ROWS_PER_PAGE) + "," + String.valueOf(NUM_ROWS_PER_PAGE);
        if (orderBy == null) {
            orderBy = "name";
        }
        List list = jt.queryForList(
            "select id, name, author, review from books order by " +
            (StringUtils.isEmpty(orderBy) ? DEFAULT_ORDER_BY : orderBy) +
            (isOrderAscending ? " ASC" : " DESC") +
            " limit " + limitStr, new Object[0]);
        BookReview[] reviews = new BookReview[list.size()];
        for (int i = 0; i < reviews.length; i++) {
            Map row = (Map) list.get(i);
            reviews[i] = new BookReview();
            reviews[i].setId((Long) row.get("id"));
            reviews[i].setBookTitle((String) row.get("name"));
            reviews[i].setReviewer((String) row.get("author"));
            reviews[i].setReviewText((String) row.get("review"));
        }
        int numReviews = jt.queryForInt("select count(*) from books");
        int lastPage = (int) Math.ceil((double) numReviews / NUM_ROWS_PER_PAGE);        BookReviewCollection collection = new BookReviewCollection();
        collection.setCurrentPage(page);
        collection.setLastPage(lastPage);
        collection.setReviews(reviews);
        return mergeContent(collection, templateFile);
    }

    public String getReview(int id) {
        log.debug("getReview(id=" + id + ")");
        BookReview review = getBookReview(id);
        return mergeContent(review, "single_review");
    }

    public String addOrEditReviewForm(int id, String bookTitle, String reviewer, String text) {
        log.debug("addOrEditReviewForm(id=" + id + ", bookTitle=" + bookTitle + ", reviewer=" + reviewer + ", text=" + text + ")");
        BookReview review = new BookReview();
        review.setId((long) id);
        review.setBookTitle(bookTitle);
        review.setReviewer(reviewer);
        review.setReviewText(text);
        return mergeContent(review, "add_edit_review");
    }

    public String previewReview(int id, String bookTitle, String reviewer, String text) {
        log.debug("previewReview(id=" + id + ", bookTitle=" + bookTitle + ", reviewer=" + reviewer + ", text=" + text + ")");
        BookReview review = new BookReview();
        review.setId((long) id);
        review.setBookTitle(bookTitle);
        review.setReviewer(reviewer);
        review.setReviewText(text);
        return mergeContent(review, "preview_review");
    }

    public String saveReview(int id, String bookTitle, String reviewer, String text) {
        log.debug("saveReview(id=" + id + ", bookTitle=" + bookTitle + ", reviewer=" + reviewer + ", text=" + text + ")");
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        if (id == 0) {
            jt.update("insert into books (id, name, author, review) values (0, ?, ?, ?)",
                new Object[] {bookTitle, reviewer, text});
        }
        return getAllReviews(0, DEFAULT_ORDER_BY, true);
    }

    public String deleteReview(int id) {
        log.debug("deleteReview(id=" + id + ")");
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        if (id != 0) {
            jt.update("delete from books where id=?", new Object[] {new Long(id)});
        }
        return getAllReviews(0, DEFAULT_ORDER_BY, true);
    }

    // for package access by test class
    protected BookReview getBookReview(int id) {
        BookReview review = new BookReview();
        if (id != 0) {
            JdbcTemplate jt = new JdbcTemplate(dataSource);
            Map row = (Map) jt.queryForObject("select id, name, author, review from books where id=?", new Object[] {id}, new ColumnMapRowMapper());
            review.setId((Long) row.get("id"));
            review.setBookTitle((String) row.get("name"));
            review.setReviewer((String) row.get("author"));
            review.setReviewText((String) row.get("review"));
        } else {
            review.setId(0L);
        }
        return review;
    }

    private String mergeContent(Object bean, String templateFile) {
        try {
            Velocity.init();
            VelocityContext vc = new VelocityContext();
            vc.put("bean", bean);
            Template t = Velocity.getTemplate(TEMPLATE_DIR + "/" + templateFile + ".vm");
            StringWriter writer = new StringWriter();
            t.merge(vc, writer);
            writer.flush();
            writer.close();
            return writer.getBuffer().toString();
        } catch (Exception e) {
            log.error("Error merging content", e);
            return "";
        }
    }
}

The Velocity Templates

The Velocity templates have a 1:1 correspondence with the nodes in our state diagram above. The main.vm template represents the component as it will first appear when the containing page is invoked. Notice the span tag named "component". This is where all the subsequent content pulled from method calls on the BookReviewService bean will be put.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
## main.vm
<!--
  test page: http://localhost:8080/smart-component/dwr/index.html
  this page: http://localhost:8080/smart-component/test.html
-->
<html>
  <head>
    <title>BookReviews</title>
    <script type="text/javascript" src="/smart-component/dwr/interface/BookReviewService.js"></script>
    <script type="text/javascript" src="/smart-component/dwr/engine.js"></script>
    <script type="text/javascript" src="/smart-component/dwr/util.js"></script>
  </head>
  <body>
    <script type="text/javascript">
    var callback = function(contents) {
        document.getElementById('component').innerHTML = contents;
    }
    </script>
    <span id="component">
      <table cellspacing="2" cellpadding="2" border="1">
        <tr>
          <td><b>Book Title</b></td>
          <td><b>Reviewer</b></td>
          <td><b>Review</b></td>
          <td><b>Edit</b></td>
          <td><b>Delete</b></td>
        </tr>
#foreach ($review in ${bean.reviews})
        <tr>
          <td>${review.bookTitle}</td>
          <td>${review.reviewer}</td>
          <td>${review.reviewText}</td>
          <td><input type="button" name="edit" value="Edit" onClick="BookReviewService.addOrEditReviewForm('${review.id}', '${review.bookTitle}', '${review.reviewer}', '${review.reviewText}', callback);" /></td>
          <td><input type="button" name="delete" value="Delete" onClick="BookReviewService.deleteReview('${review.id}', callback);" /></td>
        </tr>
#end
      </table>
      <input type="button" name="add" value="Add Review" onClick="BookReviewService.addOrEditReviewForm('0', '', '', '', callback);" />
      &nbsp;|
#if (${bean.currentPage} > 0 && ${bean.currentPage} < ${bean.lastPage})
#set ($prevPage = ${bean.currentPage} - 1)
      &nbsp;
      <input type="button" name="prevPage" value="Previous Page" onClick="BookReviewService.getAllReviews('${prevPage}', '', 'true', callback);" />
#end
#if (${bean.currentPage} == 0)
#set ($nextPage = ${bean.currentPage} + 1)
      &nbsp;
      <input type="button" name="nextPage" value="Next Page" onClick="BookReviewService.getAllReviews('${nextPage}', '', 'true', callback);" />
#end
    </span>
  </body>
</html>

The other pages are all_reviews.vm, add_edit_review.vm, single_review.vm and preview_review.vm. The all_reviews.vm contains the template for the list view, the add_edit_review.vm is the form template, and the single_review.vm and preview_review.vm are templates for the single book review view and the preview view (before saving). One thing to notice in the add_edit_review.vm is that it is not enclosed in a form tag. Enclosing the form in a form tag will make it do a request on submit, which we don't want.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
## all_reviews.vm
<table cellspacing="2" cellpadding="2" border="1">
  <tr>
    <td><b>Book Title</b></td>
    <td><b>Reviewer</b></td>
    <td><b>Review</b></td>
    <td><b>Edit</b></td>
    <td><b>Delete</b></td>
  </tr>
#foreach ($review in ${bean.reviews})
  <tr>
    <td>${review.bookTitle}</td>
    <td>${review.reviewer}</td>
    <td>${review.reviewText}</td>
    <td><input type="button" name="edit" value="Edit" onClick="BookReviewService.addOrEditReviewForm('${review.id}', '${review.bookTitle}', '${review.reviewer}', '${review.reviewText}', callback);" /></td>
    <td><input type="button" name="delete" value="Delete" onClick="BookReviewService.deleteReview('${review.id}', callback);" /></td>
  </tr>
#end
</table>
<input type="button" name="add" value="Add Review" onClick="BookReviewService.addOrEditReviewForm('0', '', '', '', callback)" />
&nbsp;|
#if (${bean.currentPage} > 0 && ${bean.currentPage} < ${bean.lastPage})
#set ($prevPage = ${bean.currentPage} - 1)
&nbsp;
<input type="button" name="prevPage" value="Previous Page" onClick="BookReviewService.getAllReviews('${prevPage}', '', 'true', callback);" />
#end
#if (${bean.currentPage} == 0)
#set ($nextPage = ${bean.currentPage} + 1)
&nbsp;
<input type="button" name="nextPage" value="Next Page" onClick="BookReviewService.getAllReviews('${nextPage}', '', 'true', callback)" />
#end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
## add_edit_review.vm
<input id="add_edit.id" type="hidden" name="id" value="$!{bean.id}" />
<table cellspacing="3" cellpadding="0" border="0">
  <tr>
    <td><b>Title:</b></td>
    <td><input id="add_edit.bookTitle" type="text" name="name" value="$!{bean.bookTitle}" /></td>
  </tr>
  <tr>
    <td><b>Your name:</b></td>
    <td><input id="add_edit.reviewer" type="text" name="author" value="$!{bean.reviewer}" /></td>
  </tr>
  <tr><td colspan="2"><b>Comment</td></tr>
  <tr>
    <td colspan="2"><textarea id="add_edit.reviewText" cols="80" rows="10" name="text">$!{bean.reviewText}</textarea></td>
  </tr>
</table>
<input type="button" name="preview" value="Preview" onClick="BookReviewService.previewReview(document.getElementById('add_edit.id').value, document.getElementById('add_edit.bookTitle').value, document.getElementById('add_edit.reviewer').value, document.getElementById('add_edit.reviewText').value, callback);" />
&nbsp;
<input type="button" name="cancel" value="Cancel" onClick="BookReviewService.getAllReviews('0', '', 'true', callback);" />
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
## single_review.vm
<table>
  <tr>
    <td><b>Title:</b>&nbsp;${bean.bookTitle}</td>
  </tr>
  <tr>
    <td><b>Reviewed by:</b>&nbsp;${bean.reviewer}</td>
  </tr>
  <tr>
    <td><b>Review:</b>&nbsp;${bean.reviewText}</td>
  </tr>
</table>
<input type="button" name="edit" value="Edit" onClick="BookReviewService.addOrEditReviewForm('${bean.id}', '${bean.bookTitle}', '${bean.reviewer}', '${bean.reviewText}', callback);" />&nbsp;
<input type="button" name="delete" value="Delete" onClick="BookReviewService.deleteReview('${bean.id}', callback);" />&nbsp;
<input type="button" name="list" value="Back to List" onClick="BookReviewService.getAllReviews('0', '', 'true', callback);" />
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
## preview_review.vm
<table>
  <tr>
    <td><b>Title:</b>
    <td>${bean.bookTitle}</td>
  </tr>
  <tr>
    <td><b>Reviewed by:</b>
    <td>${bean.reviewer}</td>
  </tr>
  <tr>
    <td colspan="2">${bean.reviewText}</td>
  </tr>
  <tr>
  </tr>
</table>
<input type="button" name="save" value="Save" onClick="BookReviewService.saveReview('${bean.id}', '${bean.bookTitle}', '${bean.reviewer}', '${bean.reviewText}', callback);" />
&nbsp;
<input type="button" name="edit" value="Edit" onClick="BookReviewService.addOrEditReviewForm('${bean.id}', '${bean.bookTitle}', '${bean.reviewer}', '${bean.reviewText}', callback);" />
&nbsp;
<input type="button" name="cancel" value="Cancel" onClick="BookReviewService.getAllReviews('0', '', 'true', callback);" />

Bootstrapping the Component

Since Javascript is event based, there has to be some event to start it up. I tried starting the list view with an onLoad event, but that was getting very confusing, since I could not populate the same span tag for all subsequent events. So I decided to bootstrap the component with a standard Spring Controller. So when you type this URL into your browser,

1
http://localhost:8080/comp/main.do

The main.vm template is used to provide an initial listing of BookReview objects in the database. The code for the Controller is straightforward, it just invokes the BookReviewService.getAllReviewsAndMergeToTemplate() method and writes directly to the ServletOutputStream.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package org.component.controllers;

import java.io.OutputStream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.component.services.BookReviewService;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class BookReviewController implements Controller {

    private static final Logger log = Logger.getLogger(BookReviewController.class);

    private BookReviewService service;

    public BookReviewController() {
        super();
    }

    public void setService(BookReviewService service) {
        this.service = service;
    }

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String htmlOutput = service.getAllReviewsAndMergeToTemplate(0, BookReviewService.DEFAULT_ORDER_BY, true, "main");
        OutputStream ostream = response.getOutputStream();
        ostream.write(htmlOutput.getBytes());
        ostream.flush();
        ostream.close();
        return null;
    }
}

Possible DWR Bug

I could not make method calls on onClick events on links work with DWR and Firefox 1.5. It looks like it may be a bug in DWR since the Javascript error message points to engine.js, a DWR supplied file. That is why the templates have so many buttons, since onClick events are triggered correctly if the link is replaced with a button. If anybody has made it work, please let me know.

Conclusion

The combination of Velocity templates to generate HTML on the server and DWR clients to consume it makes for very readable and maintainable code, compared to using XmlHttpRequest calls from Javascript. AJAX is definitely here to stay, and opens up lots of possibilities for partitioning application functionality.

22 comments (moderated to prevent spam):

Sujit Pal said...

I posted this link to the DWR User's list, and got back a lot of useful comments. Summarizing them here:

Maarten Bosteels suggested a way round having to duplicate the contents of all_reviews.vm. Always a good thing.

3) It looks as if all_reviews.vm is identical to the contents of <span id="component"> in main.vm ?
Then I read your comment about bootstrapping...

Maybe you could change main.vm like this:
<html>
...
<span id="component"> $all_reviews </span>
...
</html>

and change your controller like this: (using two Velocity merge operations)
--
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String all_reviews = service.getAllReviewsAndMergeToTemplate(
0, BookReviewService.DEFAULT_ORDER_BY, true, "all_reviews");
Velocity.init();
VelocityContext vc = new VelocityContext();
vc.put("all_reviews", all_reviews);
Template t = Velocity.getTemplate(TEMPLATE_DIR + "/main.vm");
StringWriter writer = new StringWriter();
t.merge(vc, writer);
OutputStream ostream = response.getOutputStream();
ostream.write(writer.getString().getBytes());
ostream.flush();
ostream.close();
return null;
}
--

Joe Walker was kind enough to put a link to this page from the "Tutorials and Articles about DWR" page on Getahead. Thanks Joe, I am honored.

Lance Semmers had an interesting idea of configuring the velocity template in the dwr.conf itself, and even being able to configure the type of template (velocity/jsp/something else) something like this:

<create creator="new" javascript="BookReviewService">
<param name="class"
value="org.component.services.BookReviewService" />
<template
method="addOrEditReviewForm"
type="velocity"
file="templates/add_edit_review.vm"
beanName="bean" />

<!-- or you could use ${0} for generic declaration -->
<template
method="save${0}"
type="velocity"
file="templates/save${0}.vm"
beanName="bean" />

<!-- JSP template -->
<template
method="add${0}"
type="jsp"
file="jsps/add${0}.jsp"
beanName="bean" />
</create>

This way your BookReviewService methods could continue to return Beans rather than strings and DWR could handle the template glue.

I do think that this would be a more generic way of doing things, returning beans and utilizing the power of DWR. I had toyed with the idea of JSP templates (since it is the dominant presentation approach), but I found it difficult to pull it out of the request-response cycle, so I went with velocity.

Anonymous said...

sohbet

Sujit Pal said...

The above comment (with the single hyperlink) appears to be spam, leads to a Turkish (I think) site which seems to be a link farm. Posted it in error, this should probably be removed, but I cannot find a way to unpublish comments in blogger...

Salil Kalia said...

Hi Sujit - Its really a great information about DWR and velocity. But I have a question - Is there any way to get request/session through DWR framework. I just have started reading on DWR, so couldn't get any chance to dig up the source code.
My main motive is to use internationalization or other dynamic things which I may generate on the basis of some information stored in session.
Please let me know if something clicks you.

Moreover I am looking for the lightest/fastest way to achieve AJAX. Just the minimal javascript. Even in DWR there's a lot of javascript code which I won't use, perhaps. So in that case I will take a rid of extra javascript from the source files.

Thanks
Salil Kalia

Sujit Pal said...

Thanks, Salil.

To answer your first question about getting the Session from the Request, I can't say for sure, since I've haven't had a need to do it. But since DWR allows you to define proxies for your Java beans that are defined on the server, wouldn't it be possible for one of these beans to access the ServletContext and return the Session (or session attribute) as a property to the Javascript layer?

To answer your second question, some time back, I was playing around with Prototype and Dojo in an attempt to build some simple AJAX clients against our JSON/JSONP API. They are both lightweight and fast and are well documented - you may want to look at them. Both of them will require you to write some Javascript, however. What I liked about DWR is that there is no Javascript, you write Java, and Javascript proxies are created by DWR.

Salil Kalia said...

Thanks Sujit,
I was looking into DWR documentation and I found that framework automatically pass the corresponding HttpRequest/HttpResponse/HttpSession instances if we mark these in the method signature. Also DWR provides utility classes to achieve the same. For more information please read the following..
http://directwebremoting.org/dwr/server/javaapi

DWR is powerful framework to generate javascript on the fly. And on my local machine it takes 0(negligible) to 16 milliseconds to create corresponding JS files. But the thing is engine.js and utill.js are around 90 kb as for combined size. Which might take some time to transfer to the client machine depending upon the internet speed.

If I use DWR, probably I need to cut down this size of these files either by removing unwanted js functions or by compressing them through some tool.

Thanks
Salil

Sujit Pal said...

Thanks for the great info, this will definitely be useful to others who come here and have similar questions. You probably know about this already, but for compressing javascript there are quite a few utilities available on the net, they work by removing extra whitespace from the javascript code,

Unknown said...

i could do all set up but its not working for me there is no error whils building the application. But when i access the URL its saying page not available .

priya

Sujit Pal said...

Hi Priya, did you take a look at the DWR debug page? Running your call through the debug page usually tells you whats going wrong with it.

Unknown said...

yes now i could display the first page that is main.vm but when i click on Add Review button . itsgetting resource not Found Exception . Can u pls tell the folder structure . I am using eclipse 3.3 DWr 2.0 and Spring 2.0.5 i could display the main.vm by setting the viewResolver in springapp-servlet.xml to org.springframework.web.servlet.view.velocity.VelocityViewResolver .

Unknown said...

In DWr debug page i could see BookReviewService working

Anonymous said...

thanks

Sujit Pal said...

@krishna: sorry about the delay in responding, good to know that everything is working now.

@netlog: you are welcome.

Anonymous said...

Thanks for the great info, this will definitely be useful to others who come here and have similar questions. You probably know about this already, but for compressing javascript there are quite a few utilities available on the net, they work by removing extra whitespace from the javascript code,

Sujit Pal said...

Thanks for the comment Ask. And yes, I knew that tools for compressing Javascript exist, but nothing beyond that, which is kind of shameful...I did ask around to find out what we used here, its YUI.

aşk mesajları said...

Thanks for the great info, this will definitely be useful to others who come here and have similar questions. You probably know about this already, but for compressing javascript there are quite a few utilities available on the net, they work by removing extra whitespace from the javascript code,

Sujit Pal said...

Hi, thanks for the comment. BTW, apparently you have a namesake (2 comments up) who thinks exactly like you :-).

Anonymous said...

I posted this link to the DWR User's list, and got back a lot of useful comments. Summarizing them here:
thank you...

Sujit Pal said...

Hi efe, looks like the blogger comment widget may have eaten up your summary.

Mary said...

Hello, How do I get in touch with you? There is no email or contact info listed .. please advise .. thanks .. Mary. Please contact me maryregency at gmail dot com

Sujit Pal said...

Hi Mary, you can get in touch with me through the blog comments like you did already. If you have a specific question, please include it in your message and I will try to answer it (although I am not an expert in either DWR or Velocity).

Sujit Pal said...

Teşekkür ederim (google translate :-) ile)