Revision: 10136
Author:   rchan...@google.com
Date:     Wed May  4 09:12:17 2011
Log:      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

http://code.google.com/p/google-web-toolkit/source/detail?r=10136

Added:
 /trunk/dev/core/src/com/google/gwt/core/linker/SimpleAppCacheLinker.java
/trunk/dev/core/test/com/google/gwt/core/linker/SimpleAppCacheLinkerTest.java
 /trunk/samples/mobilewebapp/src/dev
 /trunk/samples/mobilewebapp/src/dev/com
 /trunk/samples/mobilewebapp/src/dev/com/google
 /trunk/samples/mobilewebapp/src/dev/com/google/gwt
 /trunk/samples/mobilewebapp/src/dev/com/google/gwt/sample
 /trunk/samples/mobilewebapp/src/dev/com/google/gwt/sample/mobilewebapp
/trunk/samples/mobilewebapp/src/dev/com/google/gwt/sample/mobilewebapp/linker /trunk/samples/mobilewebapp/src/dev/com/google/gwt/sample/mobilewebapp/linker/AppCacheLinker.java
Modified:
/trunk/samples/mobilewebapp/src/main/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml
 /trunk/samples/mobilewebapp/user-build.xml
 /trunk/samples/mobilewebapp/war/MobileWebApp.html
 /trunk/samples/mobilewebapp/war/WEB-INF/web.xml
 /trunk/user/test/com/google/gwt/core/ext/LinkerSuite.java

=======================================
--- /dev/null
+++ /trunk/dev/core/src/com/google/gwt/core/linker/SimpleAppCacheLinker.java Wed May 4 09:12:17 2011
@@ -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);
+  }
+}
=======================================
--- /dev/null
+++ /trunk/dev/core/test/com/google/gwt/core/linker/SimpleAppCacheLinkerTest.java Wed May 4 09:12:17 2011
@@ -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;
+    }
+  }
+}
=======================================
--- /dev/null
+++ /trunk/samples/mobilewebapp/src/dev/com/google/gwt/sample/mobilewebapp/linker/AppCacheLinker.java Wed May 4 09:12:17 2011
@@ -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"
+    };
+  }
+}
=======================================
--- /trunk/samples/mobilewebapp/src/main/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml Tue May 3 10:43:13 2011 +++ /trunk/samples/mobilewebapp/src/main/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml Wed May 4 09:12:17 2011
@@ -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'/>
=======================================
--- /trunk/samples/mobilewebapp/user-build.xml  Tue May  3 10:43:13 2011
+++ /trunk/samples/mobilewebapp/user-build.xml  Wed May  4 09:12:17 2011
@@ -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>
=======================================
--- /trunk/samples/mobilewebapp/war/MobileWebApp.html Wed Apr 20 09:27:14 2011 +++ /trunk/samples/mobilewebapp/war/MobileWebApp.html Wed May 4 09:12:17 2011
@@ -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">

=======================================
--- /trunk/samples/mobilewebapp/war/WEB-INF/web.xml     Mon May  2 09:33:23 2011
+++ /trunk/samples/mobilewebapp/war/WEB-INF/web.xml     Wed May  4 09:12:17 2011
@@ -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>
=======================================
--- /trunk/user/test/com/google/gwt/core/ext/LinkerSuite.java Tue Aug 10 07:06:57 2010 +++ /trunk/user/test/com/google/gwt/core/ext/LinkerSuite.java Wed May 4 09:12:17 2011
@@ -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.

--
http://groups.google.com/group/Google-Web-Toolkit-Contributors

Reply via email to