Thursday, June 30, 2011

Writing GWT Linker for offline applications – part 2

NOTE: This article follows my previously post about linkers, which you can find HERE.

It is really sad that there is not enough documentation about the GWT Linkers. It is a very powerful feature and gives you a lot of possibility, I do hope that in the next documentation this will change. As I tried to extend my linker, I found also some example into the GWT Code on the current trunk. I was very happy to see that the GWT team works on the offline support as well, so maybe we will see this feature supported natively from the GWT very soon. It works also using Linkers and they plan to have one more linker which generates the cache manifest file which you can add to your <module>.gwt.xml file. For impatient of you, follow the tutorial bellow.

Before we start some few new things I learn doing my tests. The com.google.gwt.core.ext.Linker has two public methods you can override called linker(). If you use the annotation @Shardable when you create your custom linker, then the method link(TreeLogger logger, LinkerContext context, ArtifactSet artifacts, boolean onePermutation) will be called and only this method! The difference here is that you do have the variable onePermutation which true when only one permutation is called. If you use @Shardable then you have to define also your linker in a different way into your <module>.gwt.xml file, you should use this:

1 <define-linker name="manifest" class="com.gwt2go.dev.linker.ManifestLinker" />
2 <add-linker name="manifest" />
3

To save some time I based my code on the SimpleAppCacheLinker which you can find in the GWT trunk code HERE. I don’t wanted to start experiment everything from the beginning, since GWT team work on it as well and you can use the code. You should be also familiar with the HTML5 Offline Web Applications, since this example will vary depending on your project requirements. Also I change some staff in the code not really much, so here the steps.


First I extend my ManifestLinker so that it uses the source code from the SimpleAppCacheLinker and made some modification:


1
2 import java.io.IOException;
3 import java.util.Arrays;
4 import java.util.Date;
5
6 import org.apache.commons.io.IOUtils;
7
8 import com.google.gwt.core.ext.LinkerContext;
9 import com.google.gwt.core.ext.TreeLogger;
10 import com.google.gwt.core.ext.UnableToCompleteException;
11 import com.google.gwt.core.ext.linker.AbstractLinker;
12 import com.google.gwt.core.ext.linker.Artifact;
13 import com.google.gwt.core.ext.linker.ArtifactSet;
14 import com.google.gwt.core.ext.linker.EmittedArtifact;
15 import com.google.gwt.core.ext.linker.LinkerOrder;
16 import com.google.gwt.core.ext.linker.impl.SelectionInformation;
17
18 /**
19 * Manifest linker class creating application manifest for HTML5 offline web
20 * applications supporting browsers, based on code from
21 * com.google.gwt.core.linker.SimpleAppCacheLinker <br>
22 *
23 * @see <a
24 * href="http://code.google.com/p/google-web-toolkit/source/browse/trunk/dev/core/src/com/google/gwt/core/linker/SimpleAppCacheLinker.java?r=10181">SimpleAppCacheLinker</a>
25 *
26 * <br>
27 * ManifestLinker - linker for public path resources in the Application
28 * Cache.
29 * <p>
30 * To use:
31 * <ol>
32 * <li>Add {@code manifest="YOURMODULENAME/appcache.manifest"} to the
33 * {@code <html>} tag in your base html file. E.g.,
34 * {@code <html manifest="mymodule/appcache.manifest">}</li>
35 * <li>Add a mime-mapping to your web.xml file:
36 * <p>
37 *
38 * <pre>
39 * {@code <mime-mapping>
40 * <extension>manifest</extension>
41 * <mime-type>text/cache-manifest</mime-type>
42 * </mime-mapping>
43 * }
44 * </pre>
45 *
46 * </li>
47 * </ol>
48 * <p>
49 * On every compile, this linker will regenerate the appcache.manifest file
50 * with files from the public path of your module.
51 * <p>
52 * To obtain a manifest that contains other files in addition to those
53 * generated by this linker, create a class that inherits from this one and
54 * overrides {@code otherCachedFiles()}, and use it as a linker instead:
55 * <p>
56 *
57 * <pre>
58 * <blockquote>
59 * {@code @Shardable}
60 * public class MyAppCacheLinker extends ManifestLinker {
61 * {@code @Override}
62 * protected String[] otherCachedFiles() {
63 * return new String[] {"/MyApp.html","/MyApp.css"};
64 * }
65 *
66 * {@code @Override}
67 * protected String[] appCacheManifestTemplate() {
68 * return "myappcache.manifest";
69 * }
70 * }
71 * </blockquote>
72 * </pre>
73 *
74 * @author L.Pelov
75 */
76 @LinkerOrder(LinkerOrder.Order.POST)
77 public class ManifestLinker extends AbstractLinker {
78
79 private static final String MANIFEST = "appcache.manifest";
80 private static final String MANIFESTTEMPLATE = "cache.manifest.template";
81
82 @Override
83 public String getDescription() {
84 return "Application manifest linker";
85 }
86
87 @Override
88 public ArtifactSet link(TreeLogger logger, LinkerContext context,
89 ArtifactSet artifacts, boolean onePermutation)
90 throws UnableToCompleteException {
91 ArtifactSet toReturn = new ArtifactSet(artifacts);
92
93 if (onePermutation) {
94 return toReturn;
95 }
96
97 if (toReturn.find(SelectionInformation.class).isEmpty()) {
98 logger.log(TreeLogger.INFO, "Warning: Clobbering " + MANIFEST
99 + " to allow debugging. "
100 + "Recompile before deploying your app!");
101 artifacts = null;
102 }
103
104 // Create the general cache-manifest resource for the landing page:
105 toReturn.add(emitLandingPageCacheManifest(context, logger, artifacts));
106 return toReturn;
107
108 };
109
110 /**
111 * Creates the cache-manifest resource specific for the landing page.
112 *
113 * @param context
114 * the linker environment
115 * @param logger
116 * the tree logger to record to
117 * @param artifacts
118 * {@code null} to generate an empty cache manifest
119 */
120
121 private Artifact<?> emitLandingPageCacheManifest(LinkerContext context,
122 TreeLogger logger, ArtifactSet artifacts)
123 throws UnableToCompleteException {
124 StringBuilder publicSourcesSb = new StringBuilder();
125 StringBuilder staticResoucesSb = new StringBuilder();
126
127 if (artifacts != null) {
128 // Iterate over all emitted artifacts, and collect all cacheable
129 // artifacts
130 for (@SuppressWarnings("rawtypes")
131 Artifact artifact : artifacts) {
132 if (artifact instanceof EmittedArtifact) {
133 EmittedArtifact ea = (EmittedArtifact) artifact;
134 String pathName = ea.getPartialPath();
135 if (pathName.endsWith("symbolMap")
136 || pathName.endsWith(".xml.gz")
137 || pathName.endsWith("rpc.log")
138 || pathName.endsWith("gwt.rpc")
139 || pathName.endsWith("manifest.txt")
140 || pathName.startsWith("rpcPolicyManifest")) {
141 // skip these resources
142 } else {
143 publicSourcesSb.append(pathName + "\n");
144 }
145 }
146 }
147
148 String[] cacheExtraFiles = getCacheExtraFiles();
149 for (int i = 0; i < cacheExtraFiles.length; i++) {
150 staticResoucesSb.append(cacheExtraFiles[i]);
151 staticResoucesSb.append("\n");
152 }
153 }
154
155 String cacheManifestString = createCache(logger, context,
156 publicSourcesSb, staticResoucesSb);
157
158 // Create the manifest as a new artifact and return it:
159 return emitString(logger, cacheManifestString, MANIFEST);
160 }
161
162 /**
163 * Generate the application cache manifest file
164 *
165 * @param logger
166 * - the tree logger to record to
167 * @param context
168 * - the linker environment
169 * @param publicSourcesSb
170 * - contains the public resources, generated by the compilation
171 * @param staticResoucesSb
172 * - contains the static resources, defined by your subclass
173 * @return
174 * @throws UnableToCompleteException
175 */
176 protected String createCache(TreeLogger logger, LinkerContext context,
177 StringBuilder publicSourcesSb, StringBuilder staticResoucesSb)
178 throws UnableToCompleteException {
179
180 // // build cache list
181 // StringBuilder sb = new StringBuilder();
182 // sb.append("CACHE MANIFEST\n");
183 // sb.append("# Unique id #" + (new Date()).getTime() + "."
184 // + Math.random() + "\n");
185 // // we have to generate this unique id because the resources can
186 // change
187 // // but the hashed cache.html files can remain the same.
188 // sb.append("# Note: must change this every time for cache to invalidate\n");
189 // sb.append("\n");
190 // sb.append("CACHE:\n");
191 // sb.append("# Static app files\n");
192 // sb.append(staticResoucesSb.toString());
193 // sb.append("\n# Generated app files\n");
194 // sb.append(publicSourcesSb.toString());
195 // sb.append("\n\n");
196 // sb.append("# All other resources require the user to be online.\n");
197 // sb.append("NETWORK:\n");
198 // sb.append("*\n");
199
200 try {
201 String manifest = IOUtils.toString(getClass().getResourceAsStream(
202 appCacheManifestTemplate()));
203
204 // replace the placeholder with the real data
205 manifest = manifest.replace("$UNIQUEID$",
206 (new Date()).getTime() + "." + Math.random()).toString();
207
208 manifest = manifest.replace("$STATICAPPFILES$",
209 staticResoucesSb.toString());
210
211 manifest = manifest.replace("$GENAPPFILES$",
212 publicSourcesSb.toString());
213
214 logger.log(
215 TreeLogger.INFO,
216 "Be sure your landing page's <html> tag declares a manifest:"
217 + " <html manifest="
218 + context.getModuleFunctionName() + "/" + MANIFEST
219 + "\">");
220
221 return manifest;
222
223 } catch (IOException e) {
224
225 logger.log(TreeLogger.ERROR,
226 "Could not read cache manifest template.", e);
227
228 throw new UnableToCompleteException();
229 }
230 }
231
232 private String[] getCacheExtraFiles() {
233 String[] cacheExtraFiles = otherCachedFiles();
234 return cacheExtraFiles == null ? new String[0] : Arrays.copyOf(
235 cacheExtraFiles, cacheExtraFiles.length);
236 }
237
238 /**
239 * Obtains the extra files to include in the manifest. Ensures the returned
240 * array is not null.
241 */
242 protected String[] otherCachedFiles() {
243 return null;
244 }
245
246 /**
247 * Do not forget to include this file into your <html manifest="" \>
248 *
249 * @return
250 */
251 protected String appCacheManifestTemplate() {
252 return MANIFESTTEMPLATE;
253 }
254 }
255

I add one more function calls appCacheManifestTemplate() which you can override in your subclass. It gives you the possibility to use custom template file for the cache. This means you can have a simple template file with some configurations inside which are not GWT relevant and than just replace the placeholders inside with the generated GWT code. My template files cache.manifest.template looks like this:


1 CACHE MANIFEST
2 # Unique id #$UNIQUEID$
3 # Note: must change this every time for cache to invalidate
4 # L.Pelov
5
6 CACHE:
7 # Static app files
8 $STATICAPPFILES$
9
10 # Generated app files
11 $GENAPPFILES$
12
13 # All other resources require the user to be online.
14 NETWORK:
15 *
16

Inside I place the placeholders $UNIQUEID$, $STATICAPPFILES$ and $GENAPPFILES$, which then I replace in the function createCache(). Inside there is also the original code to see the difference. The template file should be created in the same namespace where your linker class was created.


Next step will be to extend this linker with your application class linker:


1 import com.google.gwt.core.ext.linker.Shardable;
2
3 /**
4 * Application Linker creates the cacheable manifest file
5 *
6 * @author L.Pelov
7 */
8 @Shardable
9 public class AppCacheManifestLinker extends ManifestLinker {
10
11 @Override
12 protected String[] otherCachedFiles() {
13 return new String[] { "/Gwt2go.html", "/Gwt2go.css" };
14 }
15
16 @Override
17 protected String appCacheManifestTemplate() {
18 return "myappcache.manifest";
19 }
20 }
21

As you can see you can defined very easy also a new static files to be cached and also have other custom templates, if you wish. If no custom template was defined then the ManifestLinker will use the default cache.manifest.template from your namespace.


As next you have to define your application manifest file into your main html file. In my case this  the Gwt2go.html where I just had to put this:


<html manifest="gwt2go/appcache.manifest">


This will tell the browser where to read the manifest file. Another configuration you NEED to do, you have to add following into your web.xml file…


1 <mime-mapping>
2 <extension>manifest</extension>
3 <mime-type>text/cache-manifest</mime-type>
4 </mime-mapping>
5

… and at last you have to put this inside your <module>.gwt.xml file…


1 <define-linker name="manifest" class="com.gwt2go.dev.linker.AppCacheManifestLinker" />
2 <add-linker name="manifest" />
3

so that the linker will be called when you compile your application.


Now we are ready to go. Compile your application and open it with a browser which support Offline Web Applications standard.


If you start the application now and open it you should see for example in Firefox this:


image





OK in my case is in GermanSmiley, but you should see the same in English as well, the first button from left allows the browser to cache the content. When you allow the browser to download the content for offline work, you then go and set-up the browser to work offline and then try the application again without the ?gwt.code on the end of the link. If everything was OK then you should be able to work with the application.


Regarding debugging and working with it. If you want to debug or test run the application in Eclipse and use the development mode links as usual. If you want that the browser refresh the cache with the latest modifications you done in your code, you have to rebuild the application. This will change the unique id and the browser knows what to do.


You can find the code from this example HERE.


To go directly to the namespace with the linker code click HERE.


I hope that helps you, cheers!

4 comments:

  1. Hi, the linker itself works great, thanks a lot!

    Unfortunately the linker is called twice: once I click on compile and once again when I open the website in a browser.
    This would not be problem per se but the second time the linker is run it creates an empty (the static files are missing, too) manifest overwriting the manifest that was created the first time.
    The example Application behaves the same way.

    I'm running eclipse JEE 3.6 using GWT 2.3.0 and GAE 1.5.1. I also ran the application on a second system with the same result.

    Has anyone experienced the same behavior and/or does know how to prevent that?

    Thanks and Regards, Roman

    ReplyDelete
  2. Hi Roman, thanks for the feedback, I will check this issue as well

    ReplyDelete
  3. AnonymousJuly 22, 2011

    I am having the exact same problem as Roman. The manifest gets overwritten with no values. Were you able to fix this issue?

    Thanks.

    ReplyDelete
  4. First thank you very much for the feedback! Yesterday I had some time and check on this issue, so here is my post about this:

    http://webcentersuite.blogspot.com/2011/07/writing-gwt-linker-for-offline.html

    ReplyDelete