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