During some recent consulting, a client needed to enable offline data
collection for an existing web application. End users, who, when on
assignment, were often out of range of internet services, needed some
way to fill in forms on their laptops. Later, when they regained
connectivity, those forms needed to be automatically uploaded to a
central server for further processing.
For a couple of reasons, we decided that Google Gears would be the
best tool for implementing these requirements. First, the online version
of the forms had already been developed in HTML, and Gears would allow
to simply extend those forms with offline support and use the browser
for delivery, rather than to rewrite them for some other platform.
Needless to say, having only one version of a form to maintain drives
down development time and maintenance costs greatly. In comparison, two
another approaches: to provide offline support using Eclipse RCP or
Microsoft InfoPath, would have required reimplementation of the offline
forms in SWT or an InfoPath-specific XML lingo, respectively.
Secondly, once you've extended forms to include offline support, it's
only a short reach to enable advanced features like auto-save and crash
recovery into the online forms (if you use GMail, consider how much time
and stress it's auto-save drafts have undoubtedly saved you).
Problems
Unfortunately, there was one requirement for the offline forms that
proved difficult to implement in Gears. In addition to completing text
forms offline, users needed the ability to take pictures (using a
web-camera or some other DirectShow device) and associate them with a
particular form. For example, if a social worker was making a home
visit, he had to be able to attach pictures documenting abusive living
conditions to his report.
Ideally, we would launch an applet to capture images, and, using a
library like dsj
doing so in Java wouldn't be a problem, but saving that image so that it
can be accessed from the forms via Gears is tricky, and this short
article will show an approach for doing that.
Understanding Gears
Before discussing the solution in depth, let's review the
architecture of Gears. Remember that Gears is a browser extension that
runs on an end-user's computer, and, therefore, that the context of the
subsequent discussion is the user's computer, not a server.
First, every Gears-enabled web application has its own private SQLite
database on the client machine, to which the application is free to
create, delete, and update both tables and data. In our application,
this is where offline forms are stored. The
Gears Database API provides detailed information on where these
databases are stored, though suffice it to say that the exact location
is dependent on both the operating system and browser being used.
Secondly, a single SQLite database manages the LocalServer component
for all Gears-enabled websites. The local server hosts managed and
unmanaged resource stores for an application, which the application
generally uses for caching remote data, specified by URL, for offline
use. Applications can only interact with this database through the
LocalServer API. One key, oft-misunderstood, feature of the local server is
that it automatically intercepts and serves requests for any URLs that
it has cached, transparently to the application.
Solution Sketch
Now, coming back to our problem, after the applet captures the image
it needs to be pulled in by the offline application and saved to the
private database along with the rest of the form data. Unfortunately, a
straightforward solution is not workable in Gears for at least three
reasons:
There is no way to pass captured image data from an applet to
the enclosing HTML page.
Gears has very limited Blob support,
so, critically, there is no way to save Blob data into the private
database.
If, somehow, issues one and two were both resolved,
Gears does not provide a way to set the source of an image to data
stored in an application variable (it may be possible to set a data
src for an image, but this would require base64 encoding binary data
in the browser, which seems out-of-reach with the current APIs).
The trick is to leverage, or rather, commandeer, the LocalServer for
our ends. Like the alcon blue butterfly, which fools ants into raising
its young as their own, our applet, after capturing an image, deposits
it into the stewardship of the LocalServer under the guise of a
fictitious, unique URL. By way of collusion, the offline application
queries the applet for the URL, and can then not only associate it with
a form saved in the database, but can actually define it as the source
(i.e., src) for an image. The trusty LocalServer intercepts the request
and unwittingly serves up the saved image from its cache.
In the section below, we'll break down the solution,
step-by-step.
Detailed Blueprints
After an image is captured by the applet, most of the initial action
takes place there, in Java code.
Initialization: Computing Output Locations
Before the applet can begin writing to Gears' databases it first
needs to compute their locations. To begin we need the
home directory of the Gears installation:
Compute the Gears home directory
if ("Windows Vista".equals(osName)) {
if ("Firefox".equals(browserName)) {
gearsHome = computeFirefoxOutputLocation(new File(userHome
+ "\\AppData\\Local\\Mozilla\\Firefox\\Profiles"));
5 } else
if ("Explorer".equals(browserName)) {
gearsHome = new File(userHome
+ "\\AppData\\LocalLow\\Google\\Google Gears for "
+ "Internet Explorer\\");
10 } else
if ("Chrome".equals(browserName)) {
gearsHome = new File(userHome
+ "\\AppData\\Local\\Google\\Chrome\\User Data\\Default"
+ "\\Plugin Data\\Google Gears");
15 }
}
if ("Windows XP".equals(osName)) {
if ("Firefox".equals(browserName)) {
gearsHome = computeFirefoxOutputLocation(new File(userHome
20 + "\\Local Settings\\Application Data\\Mozilla"
+ "\\Firefox\\Profiles"));
} else
if ("Explorer".equals(browserName)) {
gearsHome = new File(userHome
25 + "\\Local Settings\\Application Data\\Google\\Google "
+ "Gears for Internet Explorer");
} else
if ("Chrome".equals(browserName)) {
gearsHome = new File(userHome
30 + "\\Local Settings\\Application Data\\Google\\Chrome"
+ "\\User Data\\Default\\Plugin Data\\Google Gears");
}
}
Above, I've included code for Windows XP and Vista for each of
Firefox, IE, and Chrome. Computing the exact location for Firefox, which
supports multiple browsing profiles, is delegated to the method
computeFirefoxOutputLocation, reproduced below:
Compute the output location
private File computeFirefoxOutputLocation(File file) {
File result = null;
for (File f: file.listFiles()) {
if (f.getName().endsWith(".default")) {
5 if (result == null)
result = f;
else
return null;
}
10 }
File[] files = result.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return (name.equals("Google Gears for Firefox"));
}
15 });
return files.length != 1 ? null: files[0];
}
Of all aspects of the solution, this is probably the least desirable
because it supports a rather limited set of platforms. Of course, this
code could easily be extended for wider support, but a better solution
would be for Gears to expose the home directory location in a JavaScript
object on the page, where it could be queried by the applet.
In fact, we use a very similar technique to compute the image output
location which, in Gears, is dependent on the hostname, protocol, and
port number of the application.
Compute the image output location
String browserName=null, browserVersion=null, hostname=null,
port=null, protocol=null;
try {
JSObject win = JSObject.getWindow(this);
5 browserName = String.valueOf(win.eval("getBrowserName()"));
browserVersion = String.valueOf(win.eval("getBrowserVersion()"));
hostname = String.valueOf(win.eval("getHostname()"));
port = String.valueOf(win.eval("getPort()"));
protocol = String.valueOf(win.eval("getProtocol()"));
10
securityOriginUrl = protocol + "//" + hostname
+ (port.length() == 0 ? "" : ":" + port);
entriesURL = securityOriginUrl + "/publisher/";
15 if (protocol.endsWith(":"))
protocol = protocol.substring(0, protocol.length() - 1);
if (port.length() == 0 && "http".equals(protocol)) {
port = "80";
}
20 } catch (JSException e) {
}
// Now we need to compute the actual image output location.
25 //
if (gearsHome != null) {
localserverDB = new File(gearsHome, "localserver.db");
File f = find(gearsHome, hostname);
30 if (f != null) {
f = find(f, protocol + "_" + port);
if (f != null) {
f = findOrCreate(f, "com.ahmadsoft.sacwis.frame.Desktop"
+ "[publisher]#localserver");
35 if (f != null) {
imageOutputLocation = f;
}
}
}
40 }
In lines 4-9 the applet accesses methods on the Window
object to get the required information.
Saving the Image
When the image needs to be saved, the applet uses the information
computed during initialization.
The call to generateImageFilename in line 3 generates a
unique filename formatted as
publisher_{yyyyMMddHHmmssSSS}_{Math.abs(random.nextLong())}
and with the extension .jpg.
Line 5 registers the image with Gears so that it becomes "visible" to
the local server. In line 8, the URL associated with the image during
registration is saved to the main page so that our application can read
it later on.
Below, I've reproduced the registerPayload method.
There's a lot involved in registering a resource in the Gears
LocalServer, and if you're not interested in the details, you can simply
copy and paste the code below.
Register the payload
/**
* Responsible for registering the payload with the Gears Database after it
* has been created.
* @param outputFile
5 */
private void registerPayload(File outputFile) {
int versionId = -1, payloadId = -1, entryId = -1;
try {
10 Class.forName("org.sqlite.JDBC");
Connection conn = DriverManager.getConnection("jdbc:sqlite:"
+ localserverDB.getAbsolutePath());
java.sql.Statement s = conn.createStatement();
final String vIdQry =
15 "SELECT VersionID from VERSIONS WHERE ServerID IN "
+ "( SELECT ServerID FROM SERVERS WHERE Name=\'publisher\' "
+ " AND SecurityOriginUrl=\'" + getSecurityOriginUrl() + "\')";
ResultSet r = s.executeQuery(vIdQry);
if (r.next()) { // should not fail.
20 versionId = r.getInt(1);
}
r.close();
SimpleDateFormat headerFormat =
25 new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z");
Date now = new Date();
Date expiration = new Date(Long.MAX_VALUE);
final String httpHeader =
30 "Date: " + headerFormat.format(now) + "\r\n" +
"Last-Modified: " + headerFormat.format(now) + "\r\n" +
"Expires: " + headerFormat.format(expiration) + "\r\n" +
"Content-Type: img/jpeg\r\n" +
"Content-Length: " + outputFile.length() + "\r\n" +
35 "Server: VX Puffer/3.1\r\n";
final String payQry = "INSERT INTO PAYLOADS "
+ "(CreationDate, Headers, StatusCode, StatusLine) VALUES (?,?,?,?)";
PreparedStatement payQryStmt = conn.prepareStatement(payQry);
40 payQryStmt.setLong(1, System.currentTimeMillis());
payQryStmt.setString(2, httpHeader);
payQryStmt.setInt(3, 200);
payQryStmt.setString(4, "HTTP/1.1 200 OK");
int rowsUpdated = payQryStmt.executeUpdate();
45 payQryStmt.close();
r = s.executeQuery("SELECT last_insert_rowid();");
if (r.next())
payloadId = r.getInt(1);
50 r.close();
capturedImageUrl = getEntriesURL() + outputFile.getName();
final String entQry = "INSERT INTO ENTRIES "
+ "(VersionID, Url, PayloadID, IgnoreQuery) "
55 + "VALUES (" + versionId + ",?," + payloadId + ",0)";
PreparedStatement entQryStmt = conn.prepareStatement(entQry);
entQryStmt.setString(1, capturedImageUrl);
rowsUpdated = entQryStmt.executeUpdate();
entQryStmt.close();
60
r = s.executeQuery("SELECT last_insert_rowid();");
if (r.next())
entryId = r.getInt(1);
r.close();
65
final String bdQry = "INSERT INTO RESPONSEBODIES (BodyID, FilePath) "
+ "VALUES (" + payloadId + ", ?)";
PreparedStatement bdQryStmt = conn.prepareStatement(bdQry);
70 String filePath =
imageOutputLocation.getParentFile().getParentFile().getName() + "\\" +
imageOutputLocation.getParentFile().getName() + "\\" +
imageOutputLocation.getName() + "\\" +
outputFile.getName();
75
System.out.println(payloadId + " - " + filePath);
bdQryStmt.setString(1, filePath);
rowsUpdated = bdQryStmt.executeUpdate();
80 System.out.println(rowsUpdated);
bdQryStmt.close();
conn.close();
} catch (ClassNotFoundException e) {
85 e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
The publisher resource store referenced in line 13 is actually
initialized by the offline application through the Gears API. Sample
code, written using GWT, is below:
Create the resource store
LocalServer server = Factory.getInstance().createLocalServer();
ResourceStore store = server.createStore("publisher");
This concludes a detailed look at the applet-side implementation of
the image capture, which involves saving the captured image and
registering it with the local server. Next we turn attention to the
offline application's side of things.
Working With the Image
After the applet has closed, the offline application associates the
captured image with the current form and displays it to the user. (A
note to readers: the offline application is developed with GWT, which is why all the
code is written in Java even though it runs in the browser.)
Below is the method used to retrieve the captured image's URL:
Using this method, saveImage associates the URL with the
form.
Associate the image with a form
public void saveImage() {
String captureUrl = getCaptureUrl();
...
FamilyVisitResource result = DatabaseHelper
.INSTANCE.create(database, model.getId(), captureUrl);
// notify model listeners.
//
for (FamilyVisitForm.ModelChangeListener l: listeners)
l.onImageAdded(result);
...
}
Later, when the user opens the image, the URL is loaded from the database
into a model bean, and displaying it is a snap:
Display the image
Grid pnlImage = new Grid(4, 1);
pnlImage.setCellPadding(0);
pnlImage.setCellSpacing(0);
pnlImage.setWidth("100%");
{
Label l = new Label("Captured Image");
l.addStyleName("as-fv-narrative-lbl");
pnlImage.setWidget(0, 0, l);
pnlImage.setWidget(1, 0, new Image(model.getUrl()));
}
formFamilyVisit.add(pnlImage);
Conclusion
After reading this article you've hopefully gained insights into
novel uses for Google Gears. I've included a
screencast that shows the
entire image capture process in an early prototype of the said
application. We later enhanced image capture even further with time and
GPS stamping, all of which was integrated into our Gears application
using the principles outlined above.
Resources
Google Gears
is a browser plugin that enables offline browser applications.