package org.vaadin.touchkit.gwt; import java.io.File; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeSet; 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.CompilationResult; import com.google.gwt.core.ext.linker.ConfigurationProperty; import com.google.gwt.core.ext.linker.EmittedArtifact; import com.google.gwt.core.ext.linker.LinkerOrder; import com.google.gwt.core.ext.linker.SelectionProperty; import com.google.gwt.core.ext.linker.Shardable; /** * A GWT linker that produces a cache.manifest file describing what to cache in * the application cache. Very useful for specifying which resources need to be * available when in offline mode. */ @LinkerOrder(LinkerOrder.Order.POST) @Shardable public class CacheManifestLinker extends AbstractLinker { private final HashSet<String> cachedArtifacts = new HashSet<String>(); private static Set<String> allArtifacts = Collections .synchronizedSet(new HashSet<String>()); private static Map<String, Set<String>> generatedManifestResources = Collections .synchronizedMap(new HashMap<String, Set<String>>()); public CacheManifestLinker() { addCachedResource("/"); addCachedResource("../../../VAADIN/vaadinBootstrap.js"); // Add the empty fake file we need add to keep Vaadin happy addCachedResource("../../../VAADIN/themes/touchkit/styles.css"); } @Override public String getDescription() { return "Touchkit cache manifest generator"; } @Override @SuppressWarnings("rawtypes") public ArtifactSet link(TreeLogger logger, LinkerContext context, ArtifactSet artifacts, boolean onePermutation) throws UnableToCompleteException { loadTouchKitWidgetSetResources(context); ArtifactSet newArtifacts = new ArtifactSet(artifacts); if (onePermutation) { Set<String> userAgents = new HashSet<String>(); for (CompilationResult result : artifacts .find(CompilationResult.class)) { SortedSet<SortedMap<SelectionProperty, String>> propertiesMap = result .getPropertyMap(); for (SortedMap<SelectionProperty, String> sm : propertiesMap) { Set<SelectionProperty> keySet = sm.keySet(); for (SelectionProperty selectionProperty : keySet) { if ("user.agent".equals(selectionProperty.getName())) { userAgents.add(sm.get(selectionProperty)); } } } } // This happens when we compile the widgetset for one browser only. if (userAgents.size() == 0) { userAgents.add("safari"); } SortedSet<String> hashSet = new TreeSet<String>(); for (Artifact artifact : artifacts) { if (artifact instanceof EmittedArtifact) { EmittedArtifact ea = (EmittedArtifact) artifact; String pathName = ea.getPartialPath(); if (acceptCachedResource(pathName)) { hashSet.add(pathName); allArtifacts.add(pathName); } } } // Create manifest file names based on UA values Set<String> manifestNames = new HashSet<String>(); for (String ua : userAgents) { manifestNames.add(ua); if ("safari".equals(ua)) { // custom manifest for pre-KitKat Android manifestNames.add("aosp"); } } for (String name : manifestNames) { Set<String> manifestResources = generatedManifestResources .get(name); if (manifestResources == null) { manifestResources = new TreeSet<String>(hashSet); generatedManifestResources.put(name, manifestResources); } else { manifestResources.addAll(hashSet); } manifestResources.addAll(getManifestSpecificResources(name)); } } else { for (Artifact artifact : artifacts) { if (artifact instanceof EmittedArtifact) { EmittedArtifact ea = (EmittedArtifact) artifact; String pathName = ea.getPartialPath(); if (acceptCachedResource(pathName)) { if (!allArtifacts.contains(pathName)) { // common stuff like kickstart script, included // scripts, styles, images etc.. cachedArtifacts.add(pathName); } } } } for (Entry<String, Set<String>> e : generatedManifestResources .entrySet()) { e.getValue().addAll(cachedArtifacts); newArtifacts.add(createCacheManifest(context, logger, e.getValue(), e.getKey())); } } return newArtifacts; } private Set<String> getManifestSpecificResources(String name) { SortedSet<String> resources = new TreeSet<String>(); String fontAwesome = "../../../VAADIN/themes/base/fonts/fontawesome-webfont"; String fontExtension = "aosp".equals(name) ? ".ttf" : ".woff"; resources.add(fontAwesome + fontExtension); return resources; } /** * Traverses directories specified in gwt modules to be added to cache * manifests. E.g. themes. * * @param context */ private void loadTouchKitWidgetSetResources(LinkerContext context) { synchronized (cachedArtifacts) { SortedSet<ConfigurationProperty> configurationProperties = context .getConfigurationProperties(); for (ConfigurationProperty configurationProperty : configurationProperties) { if (configurationProperty.getName().equals( "touchkit.manifestlinker.additionalCacheRoot")) { List<String> values = configurationProperty.getValues(); for (String root : values) { addResourcesRecursively(root); } break; } } } } private void addResourcesRecursively(String root) { String[] split = root.split(":"); String sourcePath = split[0]; String relativeRoot = split[1]; File file = new File(sourcePath); if (file.isDirectory()) { String[] list = file.list(); for (String child : list) { doAdd(file, child, relativeRoot); } } } private void doAdd(File f, String relativePath, final String relativeRoot) { File file2 = new File(f, relativePath); if (file2.isDirectory()) { String[] list = file2.list(); for (String string : list) { doAdd(file2, string, relativeRoot + "/" + file2.getName()); } } else { addCachedResource(relativeRoot + "/" + relativePath); } } protected void addCachedResource(Artifact<?> artifact) { String string = artifact.toString(); addCachedResource("" + string); } protected void addCachedResource(String filename) { if (acceptCachedResource(filename)) { cachedArtifacts.add(filename); } } List<String> acceptedFileExtensions = Arrays.asList(".html", ".js", ".css", ".png", ".jpg", ".gif", ".ico", ".woff"); protected boolean acceptCachedResource(String filename) { if (filename.startsWith("compile-report/")) { return false; } for (String acceptedExtension : acceptedFileExtensions) { if (filename.endsWith(acceptedExtension)) { return true; } } return false; } protected String getCacheManifestFileName() { return "cache.manifest"; } private Artifact<?> createCacheManifest(LinkerContext context, TreeLogger logger, Set<String> artifacts, String userAgent) throws UnableToCompleteException { StringBuilder cm = new StringBuilder(); cm.append("CACHE MANIFEST\n"); cm.append("# Build time" + new Date()); cm.append("\n\nCACHE:\n"); for (String fn : artifacts) { cm.append(fn); cm.append("\n"); } cm.append("\nNETWORK:\n"); cm.append("*\n\n"); String manifest = cm.toString(); String manifestName = userAgent + ".manifest"; return emitString(logger, manifest, manifestName); } }