Since this iteration has been stretched over several posts now, let's take a quick look at the puzzle pieces we have so far:

PasswordData(pojo) that represents the data that this application is working with.- A
Display(interface + implementation) that represents the GWT widget classes and knows how to display the data. - A
Display.Controller(interface, not implemented yet) that can react to UI events and knows "what to do." - A
Scrambler(abstract utility class, partially implemented) that knows how to scramble data before it is sent to the server. - A
Store(utility class, implemented) that caches password data on the client, and also handles the conversion between scrambling and unscrambling of data.
Looks like a lot of our puzzle pieces are still missing, doesn't it? Fear not -- by the end of this post, we will have filled in all the blanks in the design and will be done with a first in-memory implementation of the server side (source code is available here.). Sounds like a lot of work? Fortunately, GWT makes it really easy for us, as I will show during the rest of this article.
Let's forget for a moment that we are building a web-based application that's Java on the server and Javascript on the client. What if this was all one coherent piece of "Desktop software"? We have a UI layer of objects that need to talk to some sort of backend that is occasionally slow to respond, but we can share code between both layers as much as we want. A common way of modelling such a relationship is the Command pattern, so let's give that a shot. How could our architecture look like if we were to code it all in a single language?

- A stub object (let's call it
Backend) exposes a set of RPC calls that can be invoked asynchronously by theController - Whenever the
Backendneeds to communicate one or more changes to the client, it issues a list ofServerClientCommandobjects to the client. These commands contain both the data and the execution logic necessary to make the client-side modifications. - Whenever the
Controllerreceives a list of commands, it will allow these commands to execute in the client layer. Proper execution of these commands perform all changes to the UI that are necessary.
Here is the cool thing I learned about GWT recently: what I just described for a single-language system actually also works across the client/server boundary! In other words, I can create a Java object on my server, send it as serialized data (within certain limitations) to the client, and the compiled Javascript code will recognize the object, deserialize it properly and execute on the browser! How awesome is that?
Now, to put my money where my mouth is, let's talk implementation. Here is the basic
ServerClientCommand interface:package com.appenginefan.schluesselmeister.client;
import java.io.Serializable;
/**
* Represents a command that gets generated on the server
* (Java) side but executed on the client (Javascript) side.
* Will it blend?
*/
public interface ServerToClientCommand extends Serializable {
/**
* Apply a command that we received from the server to the
* display or the store. Do not provide any access to the
* backend of the Controller to this command, so that we
* cannot accidentally leak back any unscrambled data to
* the server.
*
* @param display
* the display that should be updated by this
* command
* @param store
* the store that should be updated by this
* command
*/
public void apply(Display display, Store store);
}
Note that each command works directly on the
Display and the Store, nothing else. In other words, a command from the server cannot accidentally send unscrambled PasswordData to App Engine, which is a nice side effect of this design.For the rest of the iteration, I'd like to keep my server implementation as simple as possible, so I will only need two commands. The first one updates the list of categories in the store and the display.
package com.appenginefan.schluesselmeister.client;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class SetCategoriesCommand implements
ServerToClientCommand {
private static final long serialVersionUID = 1L;
private List<String> categories = new ArrayList<String>();
public SetCategoriesCommand() {
}
public SetCategoriesCommand(Collection<String> categories) {
this();
this.categories.addAll(categories);
}
public SetCategoriesCommand addCategory(
String scrambledCategory) {
categories.add(scrambledCategory);
return this;
}
@Override
public void apply(Display display, Store store) {
store
.updateListOfCategoriesWithScrambledData(categories);
display.setCategories(store
.getListOfCategoriesUnscrambled());
}
}
The second command is invoked when a new series of scrambled password information arrives at the client. It will put that data into the
Store and also update the Display, if the latter happens to have the category in question selected.package com.appenginefan.schluesselmeister.client;
import java.util.ArrayList;
import java.util.List;
public class SetDataCommand implements
ServerToClientCommand {
private static final long serialVersionUID = 1L;
private String category;
private List<PasswordData> data =
new ArrayList<PasswordData>();
protected SetDataCommand() {
}
public SetDataCommand(String category) {
this();
this.category = category;
}
public SetDataCommand addData(PasswordData data) {
this.data.add(data);
return this;
}
@Override
public void apply(Display display, Store store) {
store.updateCacheWithScrambledData(category, data);
if (category
.equals(store.getCurrentCategoryScrambled())) {
display.setPasswordData(store
.getListOfPasswordDataUnscrambled(category));
}
}
}
So, how does our server-side code look like? The following is an in-memory implementation, good enough for some first tests (remember -- the App Engine based servlet is not scheduled to arrive for another iteration):
package com.appenginefan.schluesselmeister.server;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.appenginefan.schluesselmeister.client.Backend;
import com.appenginefan.schluesselmeister.client.PasswordData;
import com.appenginefan.schluesselmeister.client.ServerToClientCommand;
import com.appenginefan.schluesselmeister.client.SetCategoriesCommand;
import com.appenginefan.schluesselmeister.client.SetDataCommand;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
/**
* In-memory implementation of the backend. Do not use in
* real App Engine.
*/
@SuppressWarnings("serial")
public class InMemoryBackendImpl
extends RemoteServiceServlet implements Backend {
private final Map<Long, PasswordData> allData =
new HashMap<Long, PasswordData>();
private long counter = 0;
private static <T> List<T> singleton(T t) {
ArrayList<T> result = new ArrayList<T>();
result.add(t);
return result;
}
private SetCategoriesCommand getCategoriesCommand() {
Set<String> scrambled = new HashSet<String>();
for (PasswordData data : allData.values()) {
scrambled.add(data.getCategory());
}
return new SetCategoriesCommand(scrambled);
}
private SetDataCommand getDataCommand(String category) {
SetDataCommand result = new SetDataCommand(category);
for (PasswordData data : allData.values()) {
if (data.getCategory().equals(category)) {
result.addData(data);
}
}
return result;
}
@Override
public List<? extends ServerToClientCommand> requestCategories() {
return singleton(getCategoriesCommand());
}
@Override
public List<? extends ServerToClientCommand> requestData(
String category) {
return singleton(getDataCommand(category));
}
@Override
public List<? extends ServerToClientCommand> save(
PasswordData data) {
List<ServerToClientCommand> result =
new ArrayList<ServerToClientCommand>();
if (data.getId() == null) {
data.setId(++counter);
}
allData.put(data.getId(), data);
result.add(getCategoriesCommand());
result.add(getDataCommand(data.getCategory()));
return result;
}
}
By the way, this is the first class (not counting unit tests) that I built that does not also live on the client side -- everything else will also be compiled into Javascript by GWT. The servlet's superclass,
RemoteServiceServlet takes care of pretty much all the rest for us. The only thing noteworthy about serialization (besides that is "just works", of course) is that I had to build my own singleton() tool-method, because the Singleton list produces by java.util.Collections does not seem to be in the whitelist of classes that GWT knows how to handle.So, now that our first server is done, let's get our client-side
Controller working. The major function of the Controller is now going to be to decide what server-side methods to call, to receive incoming commands and to call their apply method:package com.appenginefan.schluesselmeister.client;
import java.util.ArrayList;
import java.util.List;
import com.google.gwt.user.client.rpc.AsyncCallback;
/**
* Controls all UI and client/server interactions in this
* application
*/
public class Controller implements Display.Controller {
private final Display display;
private final BackendAsync backend;
private final Store store;
private final Scrambler scrambler;
private final AsyncCallback<List<? extends ServerToClientCommand>> callback =
new AsyncCallback<List<? extends ServerToClientCommand>>() {
@Override
public void onFailure(Throwable caught) {
// do nothing
}
@Override
public void onSuccess(
List<? extends ServerToClientCommand> result) {
onCallback(result);
}
};
public Controller(Display display, BackendAsync backend,
Store store, Scrambler scrambler) {
this.display = display;
this.backend = backend;
this.store = store;
this.scrambler = scrambler;
}
/**
* Executed whenever commands arrive from the server.
*
* @param commands
* the list of commands that came in.
*/
public void onCallback(
List<? extends ServerToClientCommand> commands) {
if (commands != null) {
display.lockup(null);
for (ServerToClientCommand command : commands) {
if (command != null) {
command.apply(display, store);
}
}
}
}
@Override
public void saveData(PasswordData data) {
backend.save(scrambler.scramble(data), callback);
}
@Override
public void selectCategory(
String unscrambledCategoryOrNull) {
store.setCurrentCategory(unscrambledCategoryOrNull);
if (unscrambledCategoryOrNull != null) {
display.setPasswordData(store
.getListOfPasswordDataUnscrambled(store
.getCurrentCategoryScrambled()));
backend.requestData(store
.getCurrentCategoryScrambled(), callback);
} else {
display
.setPasswordData(new ArrayList<PasswordData>());
}
}
@Override
public void start() {
display.setController(this);
display.lockup("Connecting to server...");
backend.requestCategories(callback);
}
@Override
public void switchScrambler(String secret) {
scrambler.setScramblingSecret(secret);
display.setCategories(store
.getListOfCategoriesUnscrambled());
display.setPasswordData(store
.getListOfPasswordDataUnscrambled(store
.getCurrentCategoryScrambled()));
}
}
And believe it or not -- that's it. The only thing remaining is to plug the pieces on the client side together (this part would be even shorter if I had already implemented the
Scrambler algorithm):package com.appenginefan.schluesselmeister.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.ui.RootPanel;
/**
* Entry point classes define <code>onModuleLoad()</code>.
*/
public class Schluesselmeister implements EntryPoint {
@Override
public void onModuleLoad() {
// Wire all classes together
final MainScreen display = new MainScreen();
// TODO: use a real encryption algorithm
final Scrambler scrambler = new Scrambler() {
@Override
public String scramble(String s) {
return s;
}
@Override
public String unscramble(String s) {
return s;
}
};
final Store store = new Store(scrambler);
final BackendAsync backend =
(BackendAsync) GWT.create(Backend.class);
final Controller controller =
new Controller(display, backend, store, scrambler);
// Show the UI and start the application
RootPanel.get().add(display);
controller.start();
}
}
I invite you to take a look at the source and let me know if there are things I can improve. Bear in in mind, that I am a GWT novice myself. I have to say that I am pretty excited about this way of development though -- one IDE for client and server, reusable code throughout the different application layers, a Java compiler that tells me when I screwed up my client code, and the power of all the good Java unit testing tools to improve code quality, up to the client-side javascript logic. I am looking forward to see how well the App Engine integration is going to work out in the next iteration. If it's only half as smooth as the work so far, typing it up for the blog will be the biggest of all efforts.
0 comments:
Post a Comment