/gwt-appcache

GWT AppCache Support Library

Primary LanguageJavaApache License 2.0Apache-2.0

Project Archived: The browsers are discontinuing the Appcache support so this library no longer needs to exist and users should move to a ServiceWorker architecture instead.


gwt-appcache

Build Status

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.12</version>
   <scope>provided</scope>
</dependency>
<dependency>
   <groupId>org.realityforge.gwt.appcache</groupId>
   <artifactId>gwt-appcache-linker</artifactId>
   <version>1.0.12</version>
   <scope>provided</scope>
</dependency>
<dependency>
   <groupId>org.realityforge.gwt.appcache</groupId>
   <artifactId>gwt-appcache-server</artifactId>
   <version>1.0.12</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.

Quick Start with Elemental2

Elemental2 is Is a library from Google that provides type checked access to all browser APIs for Java code. It is generated from externs files and is a much more up to date library.

This is the recommended approach for modern applications. To use it you need to follow the above quick start with the following modifications.

  • Remove the gwt-appcache-client artifact from your build system.
  • Remove <inherits name="org.realityforge.gwt.appcache.Appcache"/> from the .gwt.xml file.
  • Interact with the applicationCache using Elemental2 APIs.
import static elemental2.dom.DomGlobal.*;

...

if ( null != applicationCache )
{
  //Force a cache update if new version is available
  applicationCache.addEventListener( "updateready", e -> applicationCache.swapCache() );

  // Ask the browser to recheck the cache
  applicationCache.update();

  ...

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 vs not_mobile.
  • user.agent - A standard gwt configuration property.
  • phonegap.env - Always no 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.