Adds HTML5 App Cache support to MobileWebApp sample.
Uses a custom linker to figure out which files were generated by GWTC.

Review at http://gwt-code-reviews.appspot.com/1428811


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10136 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/linker/SimpleAppCacheLinker.java b/dev/core/src/com/google/gwt/core/linker/SimpleAppCacheLinker.java
new file mode 100644
index 0000000..d8da7c3
--- /dev/null
+++ b/dev/core/src/com/google/gwt/core/linker/SimpleAppCacheLinker.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2011 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.core.linker;
+
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.AbstractLinker;
+import com.google.gwt.core.ext.linker.Artifact;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.EmittedArtifact;
+import com.google.gwt.core.ext.linker.LinkerOrder;
+import com.google.gwt.core.ext.linker.LinkerOrder.Order;
+import com.google.gwt.core.ext.linker.impl.SelectionInformation;
+
+import java.util.Arrays;
+import java.util.Date;
+
+/**
+ * AppCacheLinker - linker for public path resources in the Application Cache.
+ * <p>
+ * To use:
+ * <ol>
+ * <li>Add {@code manifest="YOURMODULENAME/appcache.nocache.manifest"} to the
+ * {@code <html>} tag in your base html file. E.g.,
+ * {@code <html manifest="mymodule/appcache.nocache.manifest">}</li>
+ * <li>Add a mime-mapping to your web.xml file:
+ * <p>
+ * <pre>{@code <mime-mapping>
+ * <extension>manifest</extension>
+ * <mime-type>text/cache-manifest</mime-type>
+ * </mime-mapping>
+ * }</pre>
+ * </li>
+ * </ol>
+ * <p>
+ * On every compile, this linker will regenerate the appcache.nocache.manifest
+ * file with files from the public path of your module.
+ * <p>
+ * To obtain a manifest that contains other files in addition to those
+ * generated by this linker, create a class that inherits from this one
+ * and overrides {@code otherCachedFiles()}, and use it as a linker instead:
+ * <p>
+ * <pre><blockquote>
+ * {@code @Shardable}
+ * public class MyAppCacheLinker extends AbstractAppCacheLinker {
+ *   {@code @Override}
+ *   protected String[] otherCachedFiles() {
+ *     return new String[] {"/MyApp.html","/MyApp.css"};
+ *   }
+ * }
+ * </blockquote></pre>
+ */
+@LinkerOrder(Order.POST)
+public class SimpleAppCacheLinker extends AbstractLinker {
+
+  private static final String MANIFEST = "appcache.nocache.manifest";
+
+  @Override
+  public String getDescription() {
+    return "AppCacheLinker";
+  }
+
+  @Override
+  public ArtifactSet link(TreeLogger logger, LinkerContext context, ArtifactSet artifacts,
+      boolean onePermutation)
+      throws UnableToCompleteException {
+    
+    ArtifactSet toReturn = new ArtifactSet(artifacts);
+    
+    if (onePermutation) {
+      return toReturn;
+    }
+
+    if (toReturn.find(SelectionInformation.class).isEmpty()) {
+      logger.log(TreeLogger.INFO, "devmode: generating empty " + MANIFEST);
+      artifacts = null;
+    }
+    
+    // Create the general cache-manifest resource for the landing page:
+    toReturn.add(emitLandingPageCacheManifest(context, logger, artifacts));
+    return toReturn;
+  }
+
+  /**
+   * Override this method to force the linker to also include more files
+   * in the manifest. 
+   */
+  protected String[] otherCachedFiles() {
+    return null;
+  }
+
+  /**
+   * Creates the cache-manifest resource specific for the landing page.
+   * 
+   * @param context the linker environment
+   * @param logger the tree logger to record to 
+   * @param artifacts {@code null} to generate an empty cache manifest
+   */
+  private Artifact<?> emitLandingPageCacheManifest(LinkerContext context, TreeLogger logger,
+      ArtifactSet artifacts)
+      throws UnableToCompleteException {
+    StringBuilder publicSourcesSb = new StringBuilder();
+    StringBuilder staticResoucesSb = new StringBuilder();
+
+    if (artifacts != null) {
+      // Iterate over all emitted artifacts, and collect all cacheable artifacts
+      for (@SuppressWarnings("rawtypes") Artifact artifact : artifacts) {
+        if (artifact instanceof EmittedArtifact) {
+          EmittedArtifact ea = (EmittedArtifact) artifact;
+          String pathName = ea.getPartialPath();
+          if (pathName.endsWith("symbolMap") 
+              || pathName.endsWith(".xml.gz") 
+              || pathName.endsWith("rpc.log")
+              || pathName.endsWith("gwt.rpc")
+              || pathName.endsWith("manifest.txt")
+              || pathName.startsWith("rpcPolicyManifest")) {
+            // skip these resources
+          } else {
+            publicSourcesSb.append(pathName + "\n");
+          }
+        }
+      }
+      
+      
+      String[] cacheExtraFiles = getCacheExtraFiles(); 
+      for (int i = 0; i < cacheExtraFiles.length; i++) {
+        staticResoucesSb.append(cacheExtraFiles[i]);
+        staticResoucesSb.append("\n");
+      }
+    }
+
+    // build cache list
+    StringBuilder sb = new StringBuilder();
+    sb.append("CACHE MANIFEST\n");
+    sb.append("# Unique id #" + (new Date()).getTime() + "." + Math.random() + "\n");
+    // we have to generate this unique id because the resources can change but
+    // the hashed cache.html files can remain the same.
+    sb.append("# Note: must change this every time for cache to invalidate\n");
+    sb.append("\n");
+    sb.append("CACHE:\n");
+    sb.append("# Static app files\n");
+    sb.append(staticResoucesSb.toString());
+    sb.append("\n# Generated app files\n");
+    sb.append(publicSourcesSb.toString());
+    sb.append("\n\n");
+    sb.append("# All other resources require the user to be online.\n");
+    sb.append("NETWORK:\n");
+    sb.append("*\n");
+
+    logger.log(TreeLogger.INFO, "Make sure you have the following"
+        + " attribute added to your landing page's <html> tag: <html manifest=\""
+        + context.getModuleFunctionName() + "/" + MANIFEST + "\">");
+
+    // Create the manifest as a new artifact and return it:
+    return emitString(logger, sb.toString(), MANIFEST);
+  }
+
+  /**
+   * Obtains the extra files to include in the manifest. Ensures the returned
+   * array is not null. 
+   */
+  private String[] getCacheExtraFiles() {
+    String[] cacheExtraFiles = otherCachedFiles();
+    return cacheExtraFiles == null ? 
+        new String[0] : Arrays.copyOf(cacheExtraFiles, cacheExtraFiles.length);
+  }
+}
diff --git a/dev/core/test/com/google/gwt/core/linker/SimpleAppCacheLinkerTest.java b/dev/core/test/com/google/gwt/core/linker/SimpleAppCacheLinkerTest.java
new file mode 100644
index 0000000..0861d14
--- /dev/null
+++ b/dev/core/test/com/google/gwt/core/linker/SimpleAppCacheLinkerTest.java
@@ -0,0 +1,178 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.google.gwt.core.linker;
+
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.ConfigurationProperty;
+import com.google.gwt.core.ext.linker.SelectionProperty;
+import com.google.gwt.core.ext.linker.SyntheticArtifact;
+import com.google.gwt.core.ext.linker.impl.SelectionInformation;
+
+import junit.framework.TestCase;
+
+import java.io.InputStream;
+import java.util.Scanner;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * Tests {@link SimpleAppCacheLinker}
+ */
+public class SimpleAppCacheLinkerTest extends TestCase {
+  private ArtifactSet artifacts;
+  private TreeLogger logger;
+  
+  @Override
+  public void setUp() {
+    artifacts = new ArtifactSet();
+    artifacts.add(new SelectionInformation("foo", 0, new TreeMap<String, String>()));
+    logger = TreeLogger.NULL;
+  }
+
+  public void testAddCachableArtifacts() throws UnableToCompleteException {
+    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
+
+    // Some cacheable artifact
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.bar", new byte[0]));
+
+    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, false);
+
+    assertEquals(3, result.size());
+    assertHasOneManifest(result);
+    assertTrue(getManifestContents(result).contains("foo.bar"));
+  }
+
+  public void testNoNonCachableArtifacts() throws UnableToCompleteException {
+    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
+
+    // Some non-cacheable artifacts
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.symbolMap", new byte[0]));
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.xml.gz", new byte[0]));
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.rpc.log", new byte[0]));
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.gwt.rpc", new byte[0]));
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "rpcPolicyManifest.bar", new byte[0]));
+
+    ArtifactSet result = linker.link(TreeLogger.NULL, new MockLinkerContext(), artifacts, false);
+
+    assertEquals(7, result.size());
+    assertHasOneManifest(result);
+    assertFalse(getManifestContents(result).contains("symbolMap"));
+    assertFalse(getManifestContents(result).contains("xml.gz"));
+    assertFalse(getManifestContents(result).contains("rpc.log"));
+    assertFalse(getManifestContents(result).contains("gwt.rpc"));
+    assertFalse(getManifestContents(result).contains("rpcPolicyManifest"));
+  }
+
+  public void testAddStaticFiles() throws UnableToCompleteException {
+    SimpleAppCacheLinker linker = new OneStaticFileAppCacheLinker();
+
+    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, false);
+
+    assertEquals(2, result.size());
+    assertHasOneManifest(result);
+    assertTrue(getManifestContents(result).contains("aStaticFile"));
+  }
+
+  public void testEmptyManifestDevMode() throws UnableToCompleteException {
+    // No SelectionInformation artifact
+    artifacts = new ArtifactSet();
+    
+    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
+
+    // Some cacheable artifact
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.bar", new byte[0]));
+
+    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, false);
+
+    assertHasOneManifest(result);
+    assertFalse(getManifestContents(result).contains("foo.bar"));
+  }
+
+  public void testManifestOnlyOnLastPass() throws UnableToCompleteException {
+    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
+
+    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, true);
+
+    assertEquals(artifacts, result);
+    
+    result = linker.link(logger, new MockLinkerContext(), artifacts, false);
+    
+    assertEquals(2, result.size());
+    assertHasOneManifest(result);
+  }
+  
+  private void assertHasOneManifest(ArtifactSet artifacts) {
+    int manifestCount = 0;
+    for (SyntheticArtifact artifact : artifacts.find(SyntheticArtifact.class)) {
+      if ("appcache.nocache.manifest".equals(artifact.getPartialPath())) {
+        assertEquals("appcache.nocache.manifest", artifact.getPartialPath());
+        manifestCount++;
+      }
+    }
+    assertEquals(1, manifestCount);
+  }
+  
+  private SyntheticArtifact getManifest(ArtifactSet artifacts) {
+    for (SyntheticArtifact artifact : artifacts.find(SyntheticArtifact.class)) {
+      if ("appcache.nocache.manifest".equals(artifact.getPartialPath())) {
+        assertEquals("appcache.nocache.manifest", artifact.getPartialPath());
+        return artifact;
+      }
+    }
+    fail("Manifest not found");
+    return null;
+  }
+  
+  private String getManifestContents(ArtifactSet artifacts) throws UnableToCompleteException {
+    return getArtifactContents(getManifest(artifacts));
+  }
+
+  private String getArtifactContents(SyntheticArtifact artifact) throws UnableToCompleteException {
+    InputStream is = artifact.getContents(logger);
+    String contents = new Scanner(is).useDelimiter("\\A").next();
+    return contents;
+  }
+
+  public static class OneStaticFileAppCacheLinker extends SimpleAppCacheLinker {
+    
+    @Override
+    protected String[] otherCachedFiles() {
+      return new String[] {"aStaticFile"};
+    }
+  }
+  
+  private static class MockLinkerContext implements LinkerContext {
+
+    public SortedSet<ConfigurationProperty> getConfigurationProperties() {
+      return new TreeSet<ConfigurationProperty>();
+    }
+
+    public String getModuleFunctionName() {
+      return null;
+    }
+
+    public long getModuleLastModified() {
+      return 0;
+    }
+
+    public String getModuleName() {
+      return null;
+    }
+
+    public SortedSet<SelectionProperty> getProperties() {
+      return new TreeSet<SelectionProperty>();
+    }
+
+    public boolean isOutputCompact() {
+      return true;
+    }
+
+    public String optimizeJavaScript(TreeLogger logger, String jsProgram) {
+      return jsProgram;
+    }
+  }
+}
diff --git a/samples/mobilewebapp/src/dev/com/google/gwt/sample/mobilewebapp/linker/AppCacheLinker.java b/samples/mobilewebapp/src/dev/com/google/gwt/sample/mobilewebapp/linker/AppCacheLinker.java
new file mode 100644
index 0000000..c09d502
--- /dev/null
+++ b/samples/mobilewebapp/src/dev/com/google/gwt/sample/mobilewebapp/linker/AppCacheLinker.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2011 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.mobilewebapp.linker;
+
+import com.google.gwt.core.ext.linker.Shardable;
+import com.google.gwt.core.linker.SimpleAppCacheLinker;
+
+/**
+ * A custom linker that generates an app cache manifest with the
+ * files generated by the GWT compiler and the static files used
+ * by this application.
+ * 
+ * @see SimpleAppCacheLinker
+ */
+@Shardable
+public class AppCacheLinker extends SimpleAppCacheLinker {
+  @Override
+  protected String[] otherCachedFiles() {
+    return new String[] {
+        "/MobileWebApp.html",
+        "/MobileWebApp.css",
+        "/audio/error.mp3",
+        "/audio/error.ogg",
+        "/audio/error.wav",
+        "/video/tutorial.mp4",
+        "/video/tutorial.ogv"
+    };
+  }
+}
diff --git a/samples/mobilewebapp/src/main/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml b/samples/mobilewebapp/src/main/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml
index 2d7bf7f..63b42cc 100644
--- a/samples/mobilewebapp/src/main/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml
+++ b/samples/mobilewebapp/src/main/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml
@@ -29,6 +29,9 @@
     <when-property-is name="formfactor" value="tablet"/>
   </replace-with>
 
+  <define-linker name="appcachelinker" class="com.google.gwt.sample.mobilewebapp.linker.AppCacheLinker" />
+  <add-linker name="appcachelinker" />
+
   <!-- Specify the paths for translatable code                    -->
   <source path='client'/>
   <source path='shared'/>
diff --git a/samples/mobilewebapp/user-build.xml b/samples/mobilewebapp/user-build.xml
index 810a54e..d29bd8b 100644
--- a/samples/mobilewebapp/user-build.xml
+++ b/samples/mobilewebapp/user-build.xml
@@ -4,6 +4,7 @@
   <!-- Arguments to gwtc and devmode targets -->
   <property name="gwt.args" value="" />
   <property name="src.dir" value="src/main" />
+  <property name="src.dev.dir" value="src/dev" />
   <property name="war.dir" value="war" />
   <property name="gwt.sdk" value="../.." />
 
@@ -74,9 +75,20 @@
     </copy>
   </target>
 
-  <target name="gwtc" depends="javac" description="GWT compile to JavaScript (production mode)">
+  <target name="javac-dev" description="Compile gwtc related classes">
+    <mkdir dir="build/dev-classes"/>
+    <javac srcdir="${src.dev.dir}" includes="**" encoding="utf-8"
+        destdir="build/dev-classes"
+        source="1.6" target="1.6" nowarn="true"
+        debug="true" debuglevel="lines,vars,source">
+      <classpath refid="project.class.path"/>
+    </javac>
+  </target>
+
+  <target name="gwtc" depends="javac,javac-dev" description="GWT compile to JavaScript (production mode)">
     <java failonerror="true" fork="true" classname="com.google.gwt.dev.Compiler">
       <classpath>
+        <path location="build/dev-classes"/>
         <pathelement location="${src.dir}/"/>
         <path refid="project.class.path"/>
       </classpath>
@@ -95,9 +107,10 @@
     <enhance_war war="${war.dir}/" />
   </target>
 
-  <target name="devmode" depends="javac,datanucleusenhance" description="Run development mode">
+  <target name="devmode" depends="javac,javac-dev,datanucleusenhance" description="Run development mode">
     <java failonerror="true" fork="true" classname="com.google.gwt.dev.DevMode">
       <classpath>
+        <path location="build/dev-classes"/>
         <pathelement location="${src.dir}/"/>
         <path refid="project.class.path"/>
         <path refid="tools.class.path"/>
@@ -131,6 +144,7 @@
     <delete dir="war/WEB-INF/deploy/" failonerror="false" />
     <delete dir="war/WEB-INF/lib/" failonerror="false" />
     <delete dir="war/mobilewebapp/" failonerror="false" />
+    <delete dir="build/" failonerror="false" />
   </target>
 
 </project>
diff --git a/samples/mobilewebapp/war/MobileWebApp.html b/samples/mobilewebapp/war/MobileWebApp.html
index d8508cc..d7efd8c 100644
--- a/samples/mobilewebapp/war/MobileWebApp.html
+++ b/samples/mobilewebapp/war/MobileWebApp.html
@@ -5,7 +5,7 @@
 <!-- with a "Quirks Mode" doctype may lead to some -->
 <!-- differences in layout.                        -->
 
-<html>
+<html manifest="mobilewebapp/appcache.nocache.manifest">
   <head>
     <meta http-equiv="content-type" content="text/html; charset=UTF-8">
 
diff --git a/samples/mobilewebapp/war/WEB-INF/web.xml b/samples/mobilewebapp/war/WEB-INF/web.xml
index 144664f..9a45312 100644
--- a/samples/mobilewebapp/war/WEB-INF/web.xml
+++ b/samples/mobilewebapp/war/WEB-INF/web.xml
@@ -43,4 +43,10 @@
     <mime-type>video/mp4</mime-type>
   </mime-mapping>
 
+  <!-- Set the HTML5 appcache mime type -->
+  <mime-mapping>
+    <extension>manifest</extension>        
+    <mime-type>text/cache-manifest</mime-type>        
+  </mime-mapping>
+
 </web-app>
diff --git a/user/test/com/google/gwt/core/ext/LinkerSuite.java b/user/test/com/google/gwt/core/ext/LinkerSuite.java
index 7e67af1..556f5dc 100644
--- a/user/test/com/google/gwt/core/ext/LinkerSuite.java
+++ b/user/test/com/google/gwt/core/ext/LinkerSuite.java
@@ -20,6 +20,7 @@
 import com.google.gwt.core.ext.test.CrossSiteIframeLinkerTest;
 import com.google.gwt.core.ext.test.IFrameLinkerTest;
 import com.google.gwt.core.ext.test.XSLinkerTest;
+import com.google.gwt.core.linker.SimpleAppCacheLinkerTest;
 import com.google.gwt.junit.tools.GWTTestSuite;
 
 import junit.framework.Test;
@@ -40,6 +41,7 @@
     suite.addTestSuite(SelectionScriptJavaScriptTest.class);
     suite.addTestSuite(SelectionScriptLinkerUnitTest.class);
     suite.addTestSuite(XSLinkerTest.class);
+    suite.addTestSuite(SimpleAppCacheLinkerTest.class);
     /*
      * Note: Single-script linking is disabled by default, because it only works
      * when the test is run for a single permutation.