gwt-appcache
The HTML5 Appcache specification is a mechanism for enabling offline HTML5 applications. This library provides a simple way to generate the required cache manifests and serve a separate manifest for each separate permutation. The library also provides support for the browser side aspects of the appcache specification. See the appendix section includes further references concerning the appcache spec.
Quick Start
The simplest way to appcache enable a GWT application is to;
- add the following dependencies into the build system. i.e.
<dependency>
<groupId>org.realityforge.gwt.appcache</groupId>
<artifactId>gwt-appcache-client</artifactId>
<version>1.0.7</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.realityforge.gwt.appcache</groupId>
<artifactId>gwt-appcache-linker</artifactId>
<version>1.0.7</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.realityforge.gwt.appcache</groupId>
<artifactId>gwt-appcache-server</artifactId>
<version>1.0.7</version>
</dependency>
- add the following snippet into the .gwt.xml file.
<module rename-to='myapp'>
...
<!-- Enable the client-side library -->
<inherits name="org.realityforge.gwt.appcache.Appcache"/>
<!-- Enable the linker -->
<inherits name="org.realityforge.gwt.appcache.linker.Linker"/>
<!-- enable the linker that generates the manifest -->
<add-linker name="appcache"/>
<!-- configure all the static files not managed by the GWT compiler -->
<extend-configuration-property name="appcache_static_files" value="./"/>
<extend-configuration-property name="appcache_static_files" value="index.html"/>
<!-- configure fallback rules used by the client when offline -->
<extend-configuration-property name="appcache_fallback_files" value="online.png offline.png"/>
<extend-configuration-property name="appcache_fallback_files" value="myDynamicService.jsp myOfflineData.json"/>
</module>
- configure html that launches the application to look for the manifest.
<!doctype html>
<html manifest="myapp.appcache">
...
</html>
- declare the servlet that serves the manifest.
@WebServlet( urlPatterns = { "/myapp.appcache" } )
public class ManifestServlet
extends AbstractManifestServlet
{
public ManifestServlet()
{
addPropertyProvider( new UserAgentPropertyProvider() );
}
}
- interact with the application from within the browser.
final ApplicationCache cache = ApplicationCache.getApplicationCacheIfSupported();
if ( null != cache )
{
cache.addUpdateReadyHandler( new UpdateReadyEvent.Handler()
{
@Override
public void onUpdateReadyEvent( @Nonnull final UpdateReadyEvent event )
{
//Force a cache update if new version is available
cache.swapCache();
}
} );
// Ask the browser to recheck the cache
cache.requestUpdate();
...
This should be sufficient to get your application using the appcache. If you load the application in a modern browser you should see it making use of the cache in the console.
A very simple example of this code is available in the gwt-appcache-example project.
How does it work?
For every permutation generated by the GWT compiler, a separate manifest file
is generated. The manifest includes almost all public resources generated by
GWT with the exception of some used during debugging and development (i.e.
myapp.devmode.js
and compilation-mappings.txt
). The manifest also includes
any additional files declared using the "appcache_static_files
" configuration
setting. (It should be noted that the "appcache_static_files
" files are
relative to the manifest file).
After the GWT compiler has generated all the different permutations, a single
xml descriptor permutations.xml
is generated that lists all the permutations
and the deferred-binding properties that were used to uniquely identify the
permutations. Typically these include values of properties such as "user.agent
".
If the compiler is using soft permutations then it is possible that multiple
deferred-binding properties will be served using a single permutation, in which
case the descriptor will have comma separated values in the permutations.xml
for that permutation.
The manifest servlet is then responsible for reading the permutations.xml
and
inspecting the incoming request and generating properties that enable it to select
the correct permutation and thus the correct manifest file. The selected manifest
file is returned to the requester.
How To: Define a new Selection Configuration
Sometimes it is useful to define a new configuration property in the gwt module descriptors that will define new permutations. A fairly typical example would be to define a configuration property that defines different view modalities. i.e. Is the device phone-like, tablet-like or a desktop. This would drive the ui and workflow in the application.
Step 1 is to define the configuration in the gwt module descriptor. i.e.
<define-property name="ui.modality" values="phone, tablet, desktop"/>
<property-provider name="ui.modality"><![CDATA[
{
var ua = window.navigator.userAgent.toLowerCase();
if ( ua.indexOf('android') != -1 ) { return 'phone'; }
if ( ua.indexOf('iphone') != -1 ) { return 'phone'; }
if ( ua.indexOf('ipad') != -1 ) { return 'tablet'; }
return 'desktop';
}
]]></property-provider>
Step 2 is to use the new configuration property to control the deferred binding rules in gwt modules. For example, the following could be added to a .gwt.xml module file;
<replace-with class="com.biz.client.gin.DesktopInjectorWrapper">
<when-type-is class="com.biz.client.gin.InjectorWrapper"/>
<when-property-is name="ui.modality" value="desktop"/>
</replace-with>
<replace-with class="com.biz.client.gin.TabletInjectorWrapper">
<when-type-is class="com.biz.client.gin.InjectorWrapper"/>
<when-property-is name="ui.modality" value="tablet"/>
</replace-with>
<replace-with class="com.biz.client.gin.PhoneInjectorWrapper">
<when-type-is class="com.biz.client.gin.InjectorWrapper"/>
<when-property-is name="ui.modality" value="phone"/>
</replace-with>
Step 3 is to define a property provider for your new configuration property and add it to the manifest servlet. i.e.
public class UIModalityPropertyProvider
implements PropertyProvider
{
@Override
public String getPropertyValue( final HttpServletRequest request )
{
final String ua = request.getHeader( "User-Agent" ).toLowerCase();
if ( ua.contains( "android" ) || ua.contains( "phone" ) ) { return "phone"; }
else if ( ua.contains( "ipad" ) ) { return "tablet"; }
else { return "desktop"; }
}
@Override
public String getPropertyName()
{
return "ui.modality";
}
}
@WebServlet( urlPatterns = { "/myapp.appcache" } )
public class ManifestServlet
extends AbstractManifestServlet
{
public ManifestServlet()
{
addPropertyProvider( new UIModalityPropertyProvider() );
addPropertyProvider( new UserAgentPropertyProvider() );
}
}
This example demonstrates a simple mechanism for supporting server-side derivable configuration properties to select a permutation. In some cases, the selection property can only be determined on the client. This scenario is more complex and requires a combination of cookies and dynamic host pages to address.
How To: Define a new client-side selection Configuration
Sometimes configuration properties can only be determined on the client. A good example is the device pixel density that can be determined by inspecting the "window.devicePixelRatio" property in the browser.
<define-property name="pixel.density" values="high, low"/>
<property-provider name="pixel.density"><![CDATA[
{
if(window.devicePixelRatio >= 2) { return 'high'; }
return 'low';
}
]]></property-provider>
The gwt-appcache library can defer the selection of the property to the client-side by merging the manifests of the high and low density permutations and returning the merged manifest to the client. This is done by marking the "pixel.density" property as client-side via;
@WebServlet( urlPatterns = { "/myapp.appcache" } )
public class ManifestServlet
extends AbstractManifestServlet
{
public ManifestServlet()
{
addPropertyProvider( new UserAgentPropertyProvider() );
...
addClientSideSelectionProperty( "pixel.density" );
}
}
This will mean that the client ultimately caches extra data that may not be used by the client. This may be acceptable for small applications but a better approach is to detect the pixel density and set a cookie prior to navigating to the page that hosts the application. The server can then attempt to determine the value of the configuration property using the cookie name like;
public class PixelDensityPropertyProvider
implements PropertyProvider
{
@Override
public String getPropertyName() { return "pixel.density"; }
@Override
public String getPropertyValue( HttpServletRequest request )
{
final Cookie[] cookies = request.getCookies();
if ( null != cookies )
{
for ( final Cookie cookie : cookies )
{
if ( "pixel.density".equals( cookie.getName() ) )
{
return cookie.getValue();
}
}
}
return null;
}
}
@WebServlet( urlPatterns = { "/myapp.appcache" } )
public class ManifestServlet
extends AbstractManifestServlet
{
public ManifestServlet()
{
addPropertyProvider( new UserAgentPropertyProvider() );
addPropertyProvider( new PixelDensityPropertyProvider() );
...
addClientSideSelectionProperty( "pixel.density" );
}
}
How To: Integrate into existing framework
The gwt-appcache library was designed to be easy to integrate into any other gwt framework. A good example is the wonderful MGWT library from which this project was initially derived. MGWT selects the permutation based on the following configuration properties;
-
mgwt.os
-iphone
,iphone_retina
,ipad
,ipad_retina
,android
,android_tablet
,blackberry
etc. -
mobile.user.agent
-mobilesafari
vsnot_mobile
. -
user.agent
- A standard gwt configuration property. -
phonegap.env
- Alwaysno
for web applications.
It is important to the MGWT framework to distinguish between retina and non-retina versions of
the iphone and ipad variants. The retina versions inspect the window.devicePixelRatio
browser property
similarly to the above pixel.density
example. Rather than making this a separate configuration
property, MGWT conflates this with operating system. As a result it uses a custom strategy to
merge the multiple permutations manifests as can be observed at AbstractMgwtManifestServlet.
MGWT also defines several property providers.
There is a pull request where you can look at the
work required to re-integrate the functionality back into the MGWT framework. This is a good
example of complex integration of gwt-appcache
.
How To: Configure the url for the Manifest servlet in web.xml
The above examples assume that annotations are used to configure the url for the manifest servlet. It is also possible to explicitly configure the url pattern in web.xml via a snippet similar to the following. This will configure the appcache manifest to be at the path "/somedir/example.appcache" relative to the application root. The manifest servlet will expect to find the permutations.xml file at the path "/somedir/example/permutations.xml" relative to the application root.
<servlet>
<servlet-name>org.realityforge.gwt.appcache.example.server.ManifestServlet</servlet-name>
</servlet>
<servlet-mapping>
<servlet-name>org.realityforge.gwt.appcache.example.server.ManifestServlet</servlet-name>
<url-pattern>/somedir/example.appcache</url-pattern>
</servlet-mapping>
Frequently Asked Questions
Why do I get a 404 from the Manifest servlet when I specify a configuration property in .gwt.xml?
If you specify a configuration property in your .gwt.xml file such as below, the manifest servlet may start returning a 404. Why is this and how do I fix it?
<set-property name="user.agent" value="safari" />
The Manifest servlet uses the permutations.xml to determine how to select the permutation(s) to serve. The properties generated by the PropertyProvider instances added to the manifest must match the set of properties that are in the permutations.xml. If you add a property provider that generates a property that is not present in permutations.xml then the manifest servlet will never be able to find a permutation that matches that property and thus a 404 will be returned.
When you specified a single value for the user.agent property above, the user.agent configuration was made into a constant and thus it could not be used to distinguish between permutations. As a result, the property no longer appears in the permutations.xml file. At this point it is necessary to remove the UserAgentPropertyProvider from the manifest servlet.
Appendix
Credit
This library began as a enhancement of similar functionality in the MGWT project by Daniel Kurka. All credit goes to Daniel for the initial code and idea. The library is also inspired by work done by the rebar project. Thanks goes out to them as well.