The first one will be building a GWT application with a simple UI that has no server logic behind it (just to learn how layout in GWT works). Step two will be adding a fake servlet backend (not app engine, just in memory). While not exactly App Engine yet, I should have a completely specified client-server API by the end of this process that I can subsequently implement on App Engine (iteration 3). Iteration four will handle deployment, CSS and whatever I may screw up in iterations one and two.
With this post, "Schlüsselmeister" is now entering the fourth and last of these stages. At the end of iteration three (which can be downloaded as source from here), I identified the following TODOs:
- The url entry in the table should be a hyperlink.
- Add a way to select a row with an existing password entry, to show the password, and to edit it.
- Add a new field for the username.
- Add a delete button for passwords.
- Convert the landing page into a JSP that hides the app if no user is logged in. Right now, you have to press the login button, which is not quite intuitive.
- Making the app pretty using CSS (anyone know a good template I can use? Please post to this blog!)
The first two items are simple modifications in the GWT UI code, so that's not very interesting to write about. Adding a new field is not rocket science, either. First, I extend my protocol buffer definition by adding an optional field
user_name:package schluesselmeister;
option java_package = "com.appenginefan.schluesselmeister.server";
option java_outer_classname = "DataModel";
option optimize_for = SPEED;
message KeyData {
required int64 id = 1;
required string key = 2;
required string password = 3;
optional string description = 4;
optional string url = 5;
required string category_name = 6;
optional string user_name = 7;
}
message KeyDatabase {
required int64 last_id = 1;
repeated KeyData data = 2;
}
I rebuilt the Java files so that the field is also represented in the server code. Since protocol buffers are very good regarding compatibilty, the new version can easily read the old data and vice versa (try it out: create a new password entry in this version of the app that contains the new field that already contains the new field. Then go back iteration 3: your new entry will still be readable.) Word of warning though: making any changes in version three and saving them to the store will nuke all username entries, since the old app is not aware of the newly created field.
After extending the data model, I do the same to my pojo-class
PasswordData and add an additional input field to the EditPasswordDialog -- and that's pretty much it. Adding a delete button is not much harder either: the biggest work is extending the backend service with a new delete method (and adding enough unit tests to feel confident that it works fine): @Override
public List<? extends ServerToClientCommand> delete(
Long id) {
// Deny access if no user is logged in
final String user = getUserEmail();
if (user == null) {
return getMissingUserResponse();
}
// Do nothing if the client did not pass along an id
if (id == null) {
return getResponseList();
}
final long asPrimitive = id;
// Modify the datastore. Keep track of all categories
// that changed
final List<String> categoriesToUpdate =
Lists.newArrayList();
final Set<String> allRemainingCategories =
Sets.newHashSet();
KeyDatabase database =
store.mutate(user,
new Function<KeyDatabase, KeyDatabase>() {
@Override
public KeyDatabase apply(KeyDatabase db) {
if (db == null) {
return null;
}
Builder builder =
KeyDatabase.newBuilder().mergeFrom(db);
for (int i = builder.getDataCount() - 1; i >= 0; i--) {
final KeyData data = builder.getData(i);
if (data.getId() == asPrimitive) {
ArrayList<KeyData> changeThis =
Lists.newArrayList(builder
.getDataList());
changeThis.remove(i);
builder.clearData();
builder.addAllData(changeThis);
categoriesToUpdate.add(data
.getCategoryName());
} else {
allRemainingCategories.add(data
.getCategoryName());
}
}
return builder.build();
}
});
boolean listChanged = false;
final List<ServerToClientCommand> result =
getResponseList();
for (String category : categoriesToUpdate) {
result.add(getDataCommand(category, database));
if (!allRemainingCategories.contains(category)) {
listChanged = true;
}
}
if (listChanged) {
result.add(getCategoriesCommand(database));
}
return result;
}
With this final modification to the Java code, my GWT client and App Engine backend are pretty much complete. What remains is focusing on the presentation. Since I am completely dreadful with regards to making UIs look pretty, I am going to push out the CSS stuff into another post ;-). Today, it's all going to be about JSP.
So far, I have used a static html landing page for my application. That was fine so far, but I have a small additional requirement that static html cannot solve that easily: since Schlüsselmeister has to know the user's email address to persist data, I want to display the GWT application only if the user is already logged in. Rather than trying anything fancy in my Javascript, I figure I could just tie the application's fate to the
div that the code is displayed at: if it's missing, my main class will simply not show anything: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() {
// Only create an app if the right div is there.
// This way, a JSP can be smart on whether or not to
// show the program
RootPanel panel = RootPanel.get("program");
if (panel != null) {
// Wire all classes together
final MainScreen display = new MainScreen();
final Scrambler scrambler = new BlockTEAScrambler();
final Store store = new Store(scrambler);
final BackendAsync backend =
(BackendAsync) GWT.create(Backend.class);
final Controller controller =
new Controller(display, backend, store, scrambler);
// Make the display visible and start event processing
panel.add(display);
controller.start();
}
}
}
The landing page can now simply turn the program on or off by including (or not including) the
div element. The following example page uses the user API to determine if someone is currently logged in. If that is the case, it will display the application. Otherwise, it will render a link to a login page instead:<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@page import="com.google.appengine.api.users.UserServiceFactory"%>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link type="text/css" rel="stylesheet" href="Schluesselmeister.css">
<title>Schlüsselmeister</title>
<script type="text/javascript" language="javascript"
src="schluesselmeister/schluesselmeister.nocache.js"></script>
</head>
<body>
<h1>App Engine Fan's <i>Schlüsselmeister</i></h1>
<h2>Password management software for the internets</h2>
<p>This password manager application is a piece of software
[...]
framework come with. Use at your own risk, and don't try to hold me
liable for anything. That being said -- I hope you enjoy the program and
find it useful.</p>
<% if (UserServiceFactory.getUserService().getCurrentUser() != null) { %>
<div id="program" />
<% } else { %>
<p>To use this program, you have to <a
href="<%= UserServiceFactory.getUserService().createLoginURL("/") %>">log
in with your Google account.</a></p>
<% } %>
</body>
</html>
That's it for today :-) You can find this version of the application at https://4.latest.vinz-clortho.appspot.com/. Let me know if there are any problems using it.
The last and final development step will have to be to make the page look nice. I have not started that work yet and (as mentioned earlier) I am not particularily good at it either. If anyone has a good suggestion for a stylesheet, just post it to this blog entry. I'll be more than happy to try it out and incorporate it into the application...