Showing posts with label ajax. Show all posts
Showing posts with label ajax. Show all posts

Saturday, December 06, 2008

The Properties Pattern and Functors

This week I have nothing to write about - I am in various stages of completion on multiple things I started, but none complete enough to post. Accordingly, in true blogging tradition, I do the next best thing - find someone else's blog, and comment on it, thereby creating a post of my own :-). This week's lucky winner is Steve Yegge's "The Universal Design Pattern" post. Granted, the post is long, but it is very interesting and informative - and entertaining. I strongly suggest you go read it first.

If you did as I suggested, you would know that Steve's Universal Design Pattern is the Properties Pattern, or an approach of modeling your object's data as Maps of name-value pairs instead of member variables. I was quite enamored with DynaBeans at one point, so much so that I built a very flexible (but somewhat difficult to maintain) content generation system around it, so this post was a major source of validation for me. I went with DynaBeans and DynaClasses because I wanted an inheritance structure, which I could not figure out how to model with maps at that time - Steve's post describes how to do this, with a special _parent key pointing to the Map that the current map extends.

So here is my take on the Properties Pattern. Each object has a Map of name-value pairs instead of traditional member variables and getters and setters. Instead, there is a get(String) and a set(String, Object) method which get and set the property named by the String argument. A get() will recursively climb the inheritance tree using the _parent key until it finds the value for the key before giving up and returning null.

I think it may be possible to take this one step further. If you look at a Prototype Ajax.Request call, it looks something like this. I choose Javascript because Javascript uses the Properties Pattern very extensively and this particular example illustrates that.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  var request = new Ajax.Request(
    "/path/to/service/url",
    {
      method: 'get', 
      parameters: { 
        a : $F('a'),
        b : $F('b')
      }, 
      asynchronous: true,
      onLoading: function(transport) {
        // do something while the request is processing
      },
      onSuccess: function(transport) {
        // do something when the request is complete
      }
    });

In our example above, the second argument is a Map of name-value pairs. To our Properties Pattern enabled Java application object, this would like like a map with the keys {"parameters", "asynchronous", "onLoading", onSuccess"} with corresponding values hanging off them. All but the last two are simply data, but the last two are really function objects.

Although Java does not provide us ways to instantiate functions directly, we can still attach standard functor classes, such as a Transformer, from commons-collections. The onLoading and onSuccess methods are listeners that get invoked when the state of the enclosing object changes. To emulate that behavior, we could have a check to see if any of the "on*" methods should be invoked before we set the key in our set() call. Something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  private Map<String,Object>> map = new HashMap<String,Object>();
  ...
  public void set(String key, Object value) {
    for (String key : map.keySet()) {
      if (key.startsWith("on")) {
        // shouldFire is application or object specific
        if (shouldFire(key)) {
          Object value = fire(key, data);
          // if value is returned, do something specific with the value.
          // But most of the time (at least in the event handler case)
          // it would just be null.
        }
      }
    }
    map.put(key, value);
  }

  private Object fire(String eventName, Map<String,Object> data) {
    Transformer<Map<String,Object>,Object> handler = 
      (Transformer<Map<String,Object>,Object>) map.get(eventName);
    return handler.transform(data);
  }

It is quite possible that being able to set a function as a property may not be all that helpful, since the behavior of a prototype in most situations is directed through code -- the behavior may be slightly modifiable using data. Allowing functions to be specified in the property map means that the new instance may have completely different (overridden) behavior from its parent prototype. While this may not be the desired behavior in most cases, there can be situations, where you want to have different behavior in the child than in the parent, and being allowed to override or add functionality via function objects can be helpful.

The problem of serialization and user-friendliness can be tackled together by "allowing" the user to specify the functions as scripts in interpreted languages such as Jython or Javascript, which also run in the JVM through the ScriptEngine interface available since Java 6. Since they are scripts, they can be serialized and deserialized as text or JSON if needed.

Saturday, February 03, 2007

Three Autocomplete implementations compared

Many web sites are now offering forms which suggest completions as you type. For example, in a form to hold the name of a US state, typing in "C" will pop up a list box containing ["California", "Colorado"]. Subsequently typing in an "a" will decrease the options in the list box to only ["California"]. Hitting the ENTER key will populate the field with "California". One of the most popular (although probably not the first) implementations is Google Suggest.

This feature is certainly very helpful from the user's perspective, since it saves keystrokes and enables him to get his job done faster. A side effect is that the list of completions aids in the process of discovery. For the user, it could mean that he gets to pages which he would not have looked at otherwise. For the site owner, it means that the site is more "sticky", thus translating into more page views and advertising dollars for sites that depend on advertising.

I have been curious about how auto-complete works, although the curiosity did not translate into actual code till recently. Obviously, AJAX is part of the equation, since each keystroke event in the form needs to be captured and sent back to the server and the possible completions returned and displayed in the scope of a single request. I was more interested, however, in how the server-side component can be built to efficiently return the results it needed to.

Over the last week, I came up with three possible implementations to do auto-completions on the file names in my ~/tmp directory. There are about 280 files in there, so this is nothing compared to what production quality auto-completion components will have to serve on real websites, but it could be a starting point for better ideas. I enumerate them here, with code, and some relative performance numbers.

In-Memory Trie

Tries are specialized data structures where a word can be stored as a sequence of characters. Reading the word involves traversing down the branch of the tree. At each node, the possible completions of the partial word can be found by traversing down all possible paths to the leaf level. It seemed ideal for modeling auto-completions, which is why I chose it. A Trie is modelled as a collection of TrieNode objects. A TrieNode is basically the current character and a Map of completions. Here is the code:

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
// Trie.java
public class Trie {

  private TrieNode rootNode;

  public Trie() {
    super();
    rootNode = new TrieNode(' ');
  }

  public void load(String phrase) {
    loadRecursive(rootNode, phrase + "$");
  }

  private void loadRecursive(TrieNode node, String phrase) {
    if (StringUtils.isBlank(phrase)) {
      return;
    }
    char firstChar = phrase.charAt(0);
    node.add(firstChar);
    TrieNode childNode = node.getChildNode(firstChar);
    if (childNode != null) {
      loadRecursive(childNode, phrase.substring(1));
    }
  }

  public boolean matchPrefix(String prefix) {
    TrieNode matchedNode = matchPrefixRecursive(rootNode, prefix);
    return (matchedNode != null);
  }

  private TrieNode matchPrefixRecursive(TrieNode node, String prefix) {
    if (StringUtils.isBlank(prefix)) {
      return node;
    }
    char firstChar = prefix.charAt(0);
    TrieNode childNode = node.getChildNode(firstChar);
    if (childNode == null) {
      // no match at this char, exit
      return null;
    } else {
      // go deeper
      return matchPrefixRecursive(childNode, prefix.substring(1));
    }
  }

  public List<String> findCompletions(String prefix) {
    TrieNode matchedNode = matchPrefixRecursive(rootNode, prefix);
    List<String> completions = new ArrayList<String>();
    findCompletionsRecursive(matchedNode, prefix, completions);
    return completions;
  }

  private void findCompletionsRecursive(TrieNode node, String prefix, List<String> completions) {
    if (node == null) {
      // our prefix did not match anything, just return
      return;
    }
    if (node.getNodeValue() == '$') {
      // end reached, append prefix into completions list. Do not append
      // the trailing $, that is only to distinguish words like ann and anne
      // into separate branches of the tree.
      completions.add(prefix.substring(0, prefix.length() - 1));
      return;
    }
    Collection<TrieNode> childNodes = node.getChildren();
    for (TrieNode childNode : childNodes) {
      char childChar = childNode.getNodeValue();
      findCompletionsRecursive(childNode, prefix + childChar, completions);
    }
  }

  public String toString() {
    return "Trie:" + rootNode.toString();
  }
}

// TrieNode.java
public class TrieNode {

  private Character character;
  private HashMap<Character,TrieNode> children;

  public TrieNode(char c) {
    super();
    this.character = new Character(c);
    children = new HashMap<Character,TrieNode>();
  }

  public char getNodeValue() {
    return character.charValue();
  }

  public Collection<TrieNode> getChildren() {
    return children.values();
  }

  public Set<Character> getChildrenNodeValues() {
    return children.keySet();
  }

  public void add(char c) {
    if (children.get(new Character(c)) == null) {
      // children does not contain c, add a TrieNode
      children.put(new Character(c), new TrieNode(c));
    }
  }

  public TrieNode getChildNode(char c) {
    return children.get(new Character(c));
  }

  public boolean contains(char c) {
    return (children.get(new Character(c)) != null);
  }

  public int hashCode() {
    return character.hashCode();
  }

  public boolean equals(Object obj) {
    if (!(obj instanceof TrieNode)) {
      return false;
    }
    TrieNode that = (TrieNode) obj;
    return (this.getNodeValue() == that.getNodeValue());
  }

  public String toString() {
    return ReflectionToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
  }
}

In-Memory Relational Database

Although the previous implementation works fine, it is not very readable. This got me thinking. All we are doing are prefix queries on the passed in word, so a possible implementation could be database based. We use an in-memory database such as HSQLDB to ensure similar performance to the Trie implementation. Here is the code for this implementation.

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
// DbTrie.java
public class DbTrie {

  private static final String DB_NAME = "/tmp/lsdb";

  private static final String LOAD_SQL = "insert into ls(name) values (?)";
  private static final String MATCH_SQL = "select count(*) from ls where name like '%prefix%%'";
  private static final String FIND_SQL = "select name from ls where name like '%prefix%%'";

  private JdbcTemplate jdbcTemplate;
  private boolean loadData = false;

  public DbTrie() throws Exception {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
    dataSource.setUrl("jdbc:hsqldb:file:" + DB_NAME);
    dataSource.setUsername("sa");
    dataSource.setPassword("");
    jdbcTemplate = new JdbcTemplate(dataSource);
    if (! (new File(DB_NAME + ".properties").exists())) {
      jdbcTemplate.execute("create table ls(name varchar(64) not null, primary key(name));");
      loadData = true;
    }
  }

  public void load(String line) {
    if (loadData) {
      jdbcTemplate.update(LOAD_SQL, new String[] {line});
    }
  }

  public boolean matchPrefix(String prefix) {
    int numMatches = jdbcTemplate.queryForInt(MATCH_SQL.replaceAll("%prefix%", prefix));
    return (numMatches > 0);
  }

  @SuppressWarnings("unchecked")
  public List<String> findCompletions(String prefix) {
    List<String> completions = new ArrayList<String>();
    List<Map<String,String>> rows = jdbcTemplate.queryForList(
      FIND_SQL.replaceAll("%prefix%", prefix));
    for (Map<String,String> row : rows) {
      completions.add(row.get("NAME"));
    }
    return completions;
  }
}

Java Set

My final implementation is a plain old java.util.TreeSet. Given that we need a way to quickly jump down to the position where the rest of the entries are lexicographically greater than or equal to our input, then iterate until we reach a point where the entries no longer start with our input, a TreeSet seemed to be the ideal data structure. Here is the code:

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
// SetTrie.java
public class SetTrie {

  private TreeSet<String> lines;

  public SetTrie() {
    lines = new TreeSet<String>();
  }

  public void load(String line) {
    lines.add(line);
  }

  public boolean matchPrefix(String prefix) {
    Set<String> tailSet = lines.tailSet(prefix);
    for (String tail : tailSet) {
      if (tail.startsWith(prefix)) {
        return true;
      }
    }
    return false;
  }

  public List<String> findCompletions(String prefix) {
    List<String> completions = new ArrayList<String>();
    Set<String> tailSet = lines.tailSet(prefix);
    for (String tail : tailSet) {
      if (tail.startsWith(prefix)) {
        completions.add(tail);
      } else {
        break;
      }
    }
    return completions;
  }
}

Performance

Each implementation was tested with the standard call sequence and the timing information captured. The call sequence is shown below:

1
 2
 3
 4
 5
 6
 7
 8
 9
10
Trie trie = new Trie();
  //DbTrie trie = new DbTrie();
  //SetTrie trie = new SetTrie();
  for (String line : lines) { // contains filenames from ~/tmp/
    trie.load(line);
  }
  for (int i = 1; i < TEST_STRING_LENGTH; i++) {
    String prefix = TEST_STRING.substring(0, i);
    List<String> completions = trie.findCompletions(prefix);
  }

Here are some performance numbers for each of the three implementations. As expected, the Trie implementation (being a specialized data structure) is the fastest, and the HSQLDB implementation is the slowest (because of the overhead of an SQL engine). The Set based implementation is the easiest to understand, but is not as quick as the Trie based implementation.

Implementation Setup (ms) Average Lookup time (ms)
Trie 82 0.03
HSQLDB 192 1.87
Set 6 0.06

Conclusion

All the implementations above are in-memory implementations. For large data sets, loading these data sets into memory could impact startup times significantly. There is also the concern with huge memory footprints of the application servers. While these may be possible implementations for small sites, these would probably not be suitable for large sites, even though they do have response times that can be measured in the order of fractions of milliseconds.

Sunday, October 01, 2006

GWT: AJAX framework for the Javascript averse

I have been meaning to check out GWT (Google Web Toolkit) since JavaOne this year, when I overheard someone tell someone else how "absolutely freakin' wonderful" it was. But since most of the AJAX related work I did was concerned with returning data from the server for AJAX-ified front end components, the opportunity did not come up. Recently though, based on something I had to do at work, I decided to use GWT to try and build a test component with two dropdown lists for my personal book reviews application. The objective was to check out how easy/hard it would be to do, and to create a template for the stuff I had to do at work. This article describes the code for this widget and the things I had to do to get it working.

Very briefly, GWT allows you to build your Javascript widgets using Java and the GWT widget class library. It allows you to generate Javascript code from the Java code you write. Only the Javascript code will be shipped to your production server.

Problem definition

So here's the problem. I have a bunch of reviews on books on various subjects, so I would like to be able to categorize the books by type, then by title. There are 2 dropdowns. The first dropdown lists the various categories of books I have reviewed, such as "Linux", "Java", "Databases", etc. The second dropdown lists the book titles in the category selected in the first dropdown. Once I select a category from the first dropdown and a title from the second dropdown, and then hit the Go button, the review should show up in a TextArea below the dropdowns.

Download GWT

The first step was to download the GWT SDK from the GWT site. GWT class libraries (the widgets) are available under an Apache 2.0 license, but the toolkit itself is covered under a slightly different license. However, that should not bother most developers, since GWT is free for both commercial and non-commercial use, and Google's license does not assert any rights to the code you create with GWT, so its yours, free and clear. GWT is currently available for Windows and Linux. I downloaded the Linux version.

Create project

GWT comes with some scripts to build an Eclipse application. There is also scripts to build an Ant application, for those who do not use Eclipse, but I did not look at that. There are three scripts that need to be run, the first to create the Eclipse project, the second to create an empty GWT application skeleton, and the third to create a JUnit test case for the application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[sujit@cyclone gwt-linux-1.0.21]$ ./projectCreator -eclipse bookshelf -out /home/sujit/src/bookshelf
Created directory /home/sujit/src/bookshelf/src
Created file /home/sujit/src/bookshelf/.project
Created file /home/sujit/src/bookshelf/.classpath

[sujit@cyclone gwt-linux-1.0.21]$ ./applicationCreator -eclipse bookshelf -out /home/sujit/src/bookshelf org.sujit.bookshelf.client.BookShelfViewer
Created directory /home/sujit/src/bookshelf/src/org/sujit/bookshelf
Created directory /home/sujit/src/bookshelf/src/org/sujit/bookshelf/client
Created directory /home/sujit/src/bookshelf/src/org/sujit/bookshelf/public
Created file /home/sujit/src/bookshelf/src/org/sujit/bookshelf/BookShelfViewer.gwt.xml
Created file /home/sujit/src/bookshelf/src/org/sujit/bookshelf/public/BookShelfViewer.html
Created file /home/sujit/src/bookshelf/src/org/sujit/bookshelf/client/BookShelfViewer.java
Created file /home/sujit/src/bookshelf/BookShelfViewer.launch
Created file /home/sujit/src/bookshelf/BookShelfViewer-shell
Created file /home/sujit/src/bookshelf/BookShelfViewer-compile

[sujit@cyclone gwt-linux-1.0.21]$ ./junitCreator -junit /usr/java/eclipse/plugins/org.junit_3.8.1/junit.jar -eclipse BookShelfViewer -out /home/sujit/src/bookshelf org.sujit.bookshelf.test.BookShelfViewerTest
Created directory /home/sujit/src/bookshelf/src/org/sujit/bookshelf/test
Created file /home/sujit/src/bookshelf/src/org/sujit/bookshelf/BookShelfViewerTest.gwt.xml
Created file /home/sujit/src/bookshelf/src/org/sujit/bookshelf/test/BookShelfViewerTest.java
Created file /home/sujit/src/bookshelf/BookShelfViewerTest-hosted.launch
Created file /home/sujit/src/bookshelf/BookShelfViewerTest-web.launch
Created file /home/sujit/src/bookshelf/BookShelfViewerTest-hosted
Created file /home/sujit/src/bookshelf/BookShelfViewerTest-web

Notice that this means that you have a project for each GWT component that you decide to build. For those who prefer developing within a monolithic environment, where there is a single web project, and we just keep adding new functionality into it, this may not be the best thing, but having your web project built off a number of tiny projects does lead to more maintainability, and the ability to release code with the confidence that you haven't broken something somewhere else.

XML Configuration

Configuration is quite simple. There is a ${projectname}.gwt.xml that provides the information about the entry point to our GWT module. The entry-point element provides the class name for our main widget, and the servlet-path provides information about the backend service.

Note that the configuration is all for development. In production, you will simply ship the generated Javascript code on the front end, and the servlet on the backend.

1
2
3
4
5
6
7
<module>
  <!-- Inherit the core Web Toolkit stuff.                  -->
  <inherits name='com.google.gwt.user.User'/>
  <!-- Specify the app entry point class.                   -->
  <entry-point class='org.sujit.bookshelf.client.BookShelfViewer'/>
  <servlet path="/bookshelf-service" class="org.sujit.bookshelf.server.BookShelfServiceServlet" />
</module>

The Widget Code

The Widget Code is built off the components available in the GWT Widget class library. The style should be familiar to anyone who has written Java Swing code. The onModuleLoad() method is called when the module loads, and is the place where the widget is defined. The onChange() method is called whenever the selected item changes in either ListBox object, and the onClick() method is called when the Go button is clicked. The constructory specifies the location of the backend service. The getXXX() and setXXX() methods provide accessors and mutators for the internal variables which store the contents of the ListBox and the TextArea objects. The loadXXX() methods are responsible for pulling data off the backend component (more on that below), and the refreshXXX() methods are responsible for taking the backend data and populating the ListBox and TextArea widgets.

  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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
package org.sujit.bookshelf.client;

import java.util.Iterator;
import java.util.List;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.ChangeListener;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

public class BookShelfViewer implements EntryPoint, ChangeListener, ClickListener {

  private static final String WAIT_MESSAGE = "Retrieving, please wait...";

  // data to be shown in widget
  private List categories;
  private List titlesInCategory;
  private String review;

  // internal tracking indexes
  private int selectedCategoryIndex;
  private int selectedTitleIndex;

  // we are listening on events raised by these widgets.
  private ListBox categoryListBox;
  private ListBox titleListBox;
  private Button goButton;
  private TextArea reviewTextArea;

  // Async interface of a service
  private BookShelfServiceAsync bookShelfService;

  public BookShelfViewer() {
    super();
    bookShelfService = (BookShelfServiceAsync) GWT.create(BookShelfService.class);
    ServiceDefTarget serviceEndPoint = (ServiceDefTarget) bookShelfService;
    serviceEndPoint.setServiceEntryPoint("/bookshelf-service");
  }

  public List getCategories() {
    return this.categories;
  }

  public void setCategories(List categories) {
    this.categories = categories;
  }

  public String getReview() {
    return this.review;
  }

  public void setReview(String review) {
    this.review = review;
  }

  public int getSelectedCategoryIndex() {
    return this.selectedCategoryIndex;
  }

  public void setSelectedCategoryIndex(int selectedCategoryIndex) {
    this.selectedCategoryIndex = selectedCategoryIndex;
  }

  public int getSelectedTitleIndex() {
    return this.selectedTitleIndex;
  }

  public void setSelectedTitleIndex(int selectedTitleIndex) {
    this.selectedTitleIndex = selectedTitleIndex;
  }

  public List getTitlesInCategory() {
    return this.titlesInCategory;
  }

  public void setTitlesInCategory(List titlesInCategory) {
    this.titlesInCategory = titlesInCategory;
  }

  /**
   * This is the entry point method.
   */
   public void onModuleLoad() {

     loadCategories();
     loadTitlesInCategory("Databases");

     Label label = new Label("Choose your poison:");

     categoryListBox = new ListBox();
     categoryListBox.setVisibleItemCount(1);
     categoryListBox.setSelectedIndex(0);
     refreshCategoryListBox(categories);
     categoryListBox.addChangeListener(this);
     setSelectedCategoryIndex(0);

     titleListBox = new ListBox();
     titleListBox.setVisibleItemCount(1);
     titleListBox.setSelectedIndex(0);
     refreshTitleListBox(titlesInCategory);
     titleListBox.addChangeListener(this);
     setSelectedTitleIndex(0);

     goButton = new Button();
     goButton.setText("Go!");
     goButton.addClickListener(this);

     reviewTextArea = new TextArea();
     reviewTextArea.setCharacterWidth(50);
     reviewTextArea.setVisibleLines(10);
     loadReviews(getSelectedCategoryIndex(), getSelectedTitleIndex());
     refreshReviewTextArea(review);

     HorizontalPanel toolbarPanel = new HorizontalPanel();
     toolbarPanel.add(label);
     toolbarPanel.add(categoryListBox);
     toolbarPanel.add(titleListBox);
     toolbarPanel.add(goButton);

     VerticalPanel componentPanel = new VerticalPanel();
     componentPanel.add(toolbarPanel);
     componentPanel.add(reviewTextArea);

     RootPanel.get().add(componentPanel);
   }

   /**
    * Event raised by selections in the category list box and title list boxes
    * will trigger this method.
    * @see com.google.gwt.user.client.ui.ChangeListener#onChange(com.google.gwt.user.client.ui.Widget)
    * @param sender the widget that raised the event.
    */
   public void onChange(Widget sender) {
     if (sender == categoryListBox) {
       // update the title list box
       int selectedIndex = categoryListBox.getSelectedIndex();
       setSelectedCategoryIndex(selectedIndex);
       String categoryName = (String) categories.get(selectedIndex);
       loadTitlesInCategory(categoryName);
       refreshTitleListBox(titlesInCategory);
     } else if (sender == titleListBox) {
       int selectedIndex = titleListBox.getSelectedIndex();
       setSelectedTitleIndex(selectedIndex);
     }
   }

   /**
    * Events raised by clicking the "Go" button will trigger this method.
    * @see com.google.gwt.user.client.ui.ClickListener#onClick(com.google.gwt.user.client.ui.Widget)
    * @param sender the widget that raised this event.
    */
   public void onClick(Widget sender) {
     if (sender == goButton) {
       loadReviews(getSelectedCategoryIndex(), getSelectedTitleIndex());
       refreshReviewTextArea(review);
     }
   }

   private void loadCategories() {
     bookShelfService.getCategories(new AsyncCallback() {
       public void onFailure(Throwable caught) {
         reviewTextArea.setText(caught.toString());
       }
       public void onSuccess(Object result) {
         setCategories((List) result);
         refreshCategoryListBox(getCategories());
       }
     });
   }

   private void refreshCategoryListBox(List categories) {
     categoryListBox.clear();
     if (categories != null) {
       for (Iterator it = categories.iterator(); it.hasNext();) {
         String category = (String) it.next();
         categoryListBox.addItem(category);
       }
     } else {
       categoryListBox.addItem(WAIT_MESSAGE);
     }
   }

   private void loadTitlesInCategory(String categoryName) {
     bookShelfService.getTitles(categoryName, new AsyncCallback() {
      public void onFailure(Throwable caught) {
        reviewTextArea.setText(caught.toString());
      }
      public void onSuccess(Object result) {
        setTitlesInCategory((List) result);
        refreshTitleListBox(getTitlesInCategory());
      }
     });
   }

   private void refreshTitleListBox(List titlesByCategory) {
     titleListBox.clear();
     if (titlesByCategory != null) {
       for (Iterator it = titlesByCategory.iterator(); it.hasNext();) {
         String title = (String) it.next();
         titleListBox.addItem(title);
       }
     } else {
       titleListBox.addItem(WAIT_MESSAGE);
     }
   }

   private void loadReviews(int selectedCategoryIndex, int selectedTitleIndex) {
     String categoryName = categoryListBox.getItemText(selectedCategoryIndex);
     String titleName = titleListBox.getItemText(selectedTitleIndex);
     bookShelfService.getReviewText(categoryName, titleName, new AsyncCallback() {
       public void onFailure(Throwable caught) {
         setReview(caught.toString());
       }
       public void onSuccess(Object result) {
         setReview((String) result);
         refreshReviewTextArea(getReview());
       }
     });
   }

   private void refreshReviewTextArea(String review) {
     if (review != null) {
       reviewTextArea.setText(review);
     } else {
       reviewTextArea.setText(WAIT_MESSAGE);
     }
   }
}

Communicating with the Server

In order to communicate with the server, we need two interfaces on the client side, and an implementing class on the server side. The implementing class is a Servlet, and subclasses the RemoteServiceServlet class provided with the GWT toolkit. The first interface on the client side is an interface specifying what services are exposed by the Servlet, and the second one is an Async version of the service interface that is used by the GWT callback mechanism. Notice that the Async version always returns void and has an extra AsyncCallback parameter for each corresponding method. The AsynCallback is where the results are passed back to the client.

1
2
3
4
5
6
7
package org.sujit.bookshelf.client;

public interface BookShelfService extends RemoteService {
  public List getCategories();
  public List getTitles(String category);
  public String getReviewText(String category, String title);
}
1
2
3
4
5
6
7
8
9
package org.sujit.bookshelf.client;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface BookShelfServiceAsync {
  public void getCategories(AsyncCallback callback);
  public void getTitles(String category, AsyncCallback callback);
  public void getReviewText(String category, String title, AsyncCallback callback);
}

On the server side, there is a simple Servlet which implements the BookShelfService interface and extends the GWT RemoteServiceServlet. Although there are no guidelines on where to place it, I decided to put it in a server directory, a sibling of the client directory, and that seems to work fine.

 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
package org.sujit.bookshelf.server;

import javax.servlet.ServletException;

import org.sujit.bookshelf.client.BookShelfService;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class BookShelfServiceServlet extends RemoteServiceServlet implements BookShelfService {

  public void init() throws ServletException {
    super.init();
    // database setup
  }

  public List getCategories() {
    // database call
  }

  public String getReviewText(String category, String title) {
    // database call
  }

  public List getTitles(String category) {
    // database call
  }
}

Handling Asynchronicity

If you look at any of the loadXXX() methods in the BookShelfViewer.java widget code, you will notice that calls to the BookShelfService servlet instantiate an anonymous AsyncCallback inner class. The result object in onSuccess() contains the object returned by the corresponding call to the server, so you will need to cast it appropriately.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
   private void loadReviews(int selectedCategoryIndex, int selectedTitleIndex) {
     String categoryName = categoryListBox.getItemText(selectedCategoryIndex);
     String titleName = titleListBox.getItemText(selectedTitleIndex);
     bookShelfService.getReviewText(categoryName, titleName, new AsyncCallback() {
       public void onFailure(Throwable caught) {
         setReview(caught.toString());
       }
       public void onSuccess(Object result) {
         setReview((String) result);
         refreshReviewTextArea(getReview());
       }
     });
   }

Another thing that took me some time to get used to was the fact that because AsyncCallback is asynchronous, the widget can call for data after it is requested from the server but before it is returned. As a result you are likely to get a NullPointerException on the client. The trick is to check for whether the result is null, and if so, to replace it with a generic "Waiting for server..." message, like the refreshXXX() methods here. As a user, you will probably never notice the message, but your application will not get the NullPointer.

1
2
3
4
5
6
7
   private void refreshReviewTextArea(String review) {
     if (review != null) {
       reviewTextArea.setText(review);
     } else {
       reviewTextArea.setText(WAIT_MESSAGE);
     }
   }

Running the shell

To run the application in the GWT shell, we need to start the ${projectName}-shell script that was generated when we ran the applicationCreator script. This gives us two windows, one to show the log messages, as shown below. The shell starts an embedded Tomcat server on port 8888.

Testing

Its great that GWT provides a script to create a JUnit test case, but in my opinion this is of limited use, since it does not support events. To fire events, you presumably have to call these methods in your test, and verify that the ListBoxes get populated as you intend. It may be more useful to test the server side thorougly, and verify manually that some of the cases are adequately handled on the front end. But that may just be true for this component, not for more complicated ones.

Deployment

The Javascript files are deployed into www/${componentClassName} directory. For those of you who did not particularly fancy my plain old HTML component, GWT offers you the ability to style the component from within Java by linking in a CSS Style sheet. Tweaking the Javascript is really not an option, and is counter-productive, since now you have to apply the same tweaks each time you change your Java code. And you have only to look at each cache file to realize how difficult it is going to be to tweak the Javascript. From what I have found, GWT supports Netscape, Firefox, MSIE, Opera and Safari, by generating different versions of the Javascript code and using browser signature sniffing to direct to the right Javascript.

Conclusion

The best part about GWT is that the developer does not have to know or write Javascript. Many expert Java developers trivialize Javascript, as if it were beneath them to have to write Javascript, but speaking for myself, I just don't know Javascript as well as I know Java. Also, Javascript is a more forgiving language, so it allows you to get away with incorrect syntax, but will manifest itself as runtime errors which are very hard to debug. Tool support for Javascript is also not as good as for Java. Personally, therefore, I found it easier to work with the GWT's Swing style code, even though I haven't done much desktop Java programming. One of the deal breakers with GWT is the quality of the generated Javascript, which comes back in one big block without indentation and such, so its very hard to tweak. However, since GWT works by generating Javascript, tweaking the Javascript will actually lead to a maintenance nightmare in any case, so the correct approach would be to fix the Java code to do what you want.

Overall, I found GWT to be quite flexible and easy to work with. The scripts to create an project make it very easy to get up and running with GWT. The GWT shell is also a real time saver since all you have to do to push your Java code changes out to Javascript is to click the Refresh button. Working with a server component does not work quite so well in the shell, especially if you have to use JDBC libraries and such. I got around this by mocking out my server component to not use JDBC, but looking back, I could have either put my server component outside on another application server (a good move in any case, since that way I could test it independently), or by adding the JDBC JAR files in the BookShelfViewer.lanch file.

GWT seems to be most suitable for AJAX applications that are standalone, in the sense that they do not interact with other components (AJAX or non-AJAX) on the web page. For example, GWT may not be the best choice if my Go button needed to send a form action request back to the server with a consequent page turn. However, if the component controls its own life cycle, such as something like Google maps, which are pretty much full page AJAX rich clients, then GWT would definitely be the way to go.