/* * Copyright 2013 cruxframework.org. * * 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 org.cruxframework.crux.core.rebind.offline; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicBoolean; import org.cruxframework.crux.core.rebind.screen.OfflineScreen; import org.cruxframework.crux.core.rebind.screen.OfflineScreenFactory; import org.cruxframework.crux.core.utils.FilePatternHandler; import org.cruxframework.crux.tools.scanner.offline.OfflineScreens; import org.w3c.dom.Document; 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.EmittedArtifact; import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility; import com.google.gwt.core.ext.linker.LinkerOrder; import com.google.gwt.core.ext.linker.SelectionProperty; import com.google.gwt.core.ext.linker.Shardable; import com.google.gwt.core.ext.linker.impl.PermutationsUtil; import com.google.gwt.util.tools.Utility; /** * A GWT linker that produces an offline.appcache file describing what to cache in the application cache. It produces one appcache file for * each permutation. * * @author Thiago da Rosa de Bustamante */ @LinkerOrder(LinkerOrder.Order.POST) @Shardable public class AppCacheLinker extends AbstractLinker { private final HashSet<String> cachedArtifacts = new HashSet<String>(); private static Map<ArtifactsGroup, Set<String>> artifactsByGroup = Collections .synchronizedMap(new HashMap<ArtifactsGroup, Set<String>>()); private static Set<String> allArtifacts = Collections.synchronizedSet(new HashSet<String>()); private static final List<String> acceptedFileExtensions = Arrays.asList(".html", ".js", ".css", ".png", ".jpg", ".gif", ".ico"); private PermutationsUtil permutationsUtil; private static AtomicBoolean analyzed = new AtomicBoolean(false); @Override public String getDescription() { return "HTML5 appcache manifest generator"; } @Override public ArtifactSet link(TreeLogger logger, LinkerContext context, ArtifactSet artifacts, boolean onePermutation) throws UnableToCompleteException { ArtifactSet artifactset = new ArtifactSet(artifacts); if (onePermutation) { aggroupPermutationArtifacts(artifactset); analyzed.set(true); } else { if (analyzed.get()) { try { Set<String> offlinePages = OfflineScreens.getOfflineIds(context.getModuleName()); if (offlinePages != null) { for (String offlineScreenID : offlinePages) { Document screen = OfflineScreens.getOfflineScreen(offlineScreenID); OfflineScreen offlineScreen = OfflineScreenFactory.getInstance().getOfflineScreen(offlineScreenID, screen); emitOfflineArtifacts(logger, context, artifactset, offlineScreen); } } } catch (Exception e) { logger.log(TreeLogger.ERROR, "Unable to create offline files", e); throw new UnableToCompleteException(); } } } return artifactset; } private void emitOfflineArtifacts(TreeLogger logger, LinkerContext context, ArtifactSet artifacts, OfflineScreen offlineScreen) throws UnableToCompleteException { String screenID = getTargetScreenId(context, logger, offlineScreen.getRefScreen()); emitMainAppCache(logger, context, artifacts); emitPermutationsAppCache(logger, context, artifacts, screenID, offlineScreen); emitOfflinePage(logger, context, artifacts, offlineScreen.getId()); } private static void aggroupPermutationArtifacts(ArtifactSet artifacts) { String userAgent = getUserAgent(artifacts); String deviceFeatures = getDeviceFeatures(artifacts); SortedSet<String> hashSet = new TreeSet<String>(); for (EmittedArtifact emitted : artifacts.find(EmittedArtifact.class)) { if (emitted.getVisibility() == Visibility.Private) { continue; } String pathName = emitted.getPartialPath(); if (acceptCachedResource(pathName)) { hashSet.add(pathName); allArtifacts.add(pathName); } } ArtifactsGroup group = new ArtifactsGroup(userAgent, deviceFeatures); if (!artifactsByGroup.containsKey(group)) { artifactsByGroup.put(group, new TreeSet<String>()); } artifactsByGroup.get(group).addAll(hashSet); } /************************************************** * manifest file manipulation methods **************************************************/ private void emitPermutationsAppCache(TreeLogger logger, LinkerContext context, ArtifactSet artifacts, String startScreenId, OfflineScreen offlineScreen) throws UnableToCompleteException { for (EmittedArtifact emitted : artifacts.find(EmittedArtifact.class)) { if (emitted.getVisibility() == Visibility.Private) { continue; } String pathName = emitted.getPartialPath(); if (acceptCachedResource(pathName)) { if (!allArtifacts.contains(pathName)) { // common stuff like clear.cache.gif, *.nocache.js, etc cachedArtifacts.add(pathName); } } } Set<ArtifactsGroup> keySet = artifactsByGroup.keySet(); for (ArtifactsGroup group : keySet) { Set<String> set = artifactsByGroup.get(group); set.addAll(cachedArtifacts); artifacts.add(createCacheManifest(context, logger, set, group.getGroupId(), startScreenId, offlineScreen)); artifacts.add(createCacheManifestLoader(context, logger, group.getGroupId(), startScreenId)); } } private void emitMainAppCache(TreeLogger logger, LinkerContext context, ArtifactSet artifacts) throws UnableToCompleteException { String moduleName = context.getModuleName(); StringBuilder builder = new StringBuilder("CACHE MANIFEST\n"); builder.append("# Build Time [" + getCurrentTimeTruncatingMiliseconds() + "]\n"); builder.append("\nCACHE:\n"); for (String fn : cachedArtifacts) { builder.append("/{context}/" + moduleName + "/" + fn + "\n"); } Set<ArtifactsGroup> keySet = artifactsByGroup.keySet(); for (ArtifactsGroup group : keySet) { builder.append("/{context}/" + moduleName + "/" + getManifestLoaderName(group.getGroupId()) + "\n"); } builder.append("\nNETWORK:\n"); builder.append("*\n"); EmittedArtifact manifest = emitString(logger, builder.toString(), getManifestName()); artifacts.add(manifest); } private Artifact<?> createCacheManifest(LinkerContext context, TreeLogger logger, Set<String> artifacts, String artifactGroupId, String startScreenId, OfflineScreen offlineScreen) throws UnableToCompleteException { String moduleName = context.getModuleName(); StringBuilder builder = new StringBuilder("CACHE MANIFEST\n"); builder.append("# Build Time [" + getCurrentTimeTruncatingMiliseconds() + "]\n"); builder.append("\nCACHE:\n"); if (startScreenId != null) { builder.append("/{context}/" + moduleName + "/" + startScreenId + "\n"); } FilePatternHandler pattern = new FilePatternHandler(offlineScreen.getIncludes(), offlineScreen.getExcludes()); for (String fn : artifacts) { if (!fn.endsWith("hosted.html") && pattern.isValidEntry(moduleName + "/" + fn)) { String path = fn.contains("\\") ? fn.replaceAll("\\\\", "/") : fn; builder.append("/{context}/" + moduleName + "/" + path + "\n"); } } builder.append("\nNETWORK:\n"); builder.append("*\n\n"); return emitString(logger, builder.toString(), getManifestName(artifactGroupId)); } private Artifact<?> createCacheManifestLoader(LinkerContext context, TreeLogger logger, String artifactGroupId, String startScreenId) throws UnableToCompleteException { // try // { // ViewProcessor.setForceIndent(true); // ViewProcessor.setOutputCharset("UTF-8"); // ByteArrayOutputStream out = new ByteArrayOutputStream(); // Document screen = ScreenResourceResolverInitializer.getScreenResourceResolver().getRootView(startScreenId, // context.getModuleName(), null); // if (screen == null) // { // logger.log(TreeLogger.ERROR, "Error generating offline app page. Can not found target screen. ScreenID[" + startScreenId // + "]"); // throw new UnableToCompleteException(); // } // screen.getDocumentElement().setAttribute("manifest", getManifestName(artifactGroupId)); // ViewProcessor.generateHTML(startScreenId, screen, out); // return emitString(logger, out.toString("UTF-8"), getManifestLoaderName(artifactGroupId)); // } // catch (Exception e) // { // logger.log(TreeLogger.ERROR, "Error generating offline app page", e); // throw new UnableToCompleteException(); // } return null; } /********************************************************** * javascript manipulation methods **********************************************************/ private void emitOfflinePage(TreeLogger logger, LinkerContext context, ArtifactSet artifacts, String offlineScreenId) throws UnableToCompleteException { permutationsUtil = new PermutationsUtil(); permutationsUtil.setupPermutationsMap(artifacts); StringBuffer buffer = readFileToStringBuffer(getOfflinePageTemplate(logger, context), logger); int startPos = buffer.indexOf("// __OFFLINE_SELECTION_END__"); if (startPos != -1) { String ss = generateSelectionScript(logger, context, artifacts); buffer.insert(startPos, ss); } replaceAll(buffer, "__MANIFEST_NAME__", getManifestName()); artifacts.add(emitString(logger, buffer.toString(), offlineScreenId, System.currentTimeMillis())); } private String generateSelectionScript(TreeLogger logger, LinkerContext context, ArtifactSet artifacts) throws UnableToCompleteException { String selectionScriptText; StringBuffer buffer = readFileToStringBuffer(getSelectionScriptTemplate(logger, context), logger); appendProcessMetas(logger, context, buffer); appendPageLoaderFunction(logger, context, buffer); selectionScriptText = fillSelectionScriptTemplate(buffer, logger, context, artifacts); // fix for some browsers like IE that cannot see the $doc variable outside the iframe tag. selectionScriptText = selectionScriptText.replace("$doc", "document"); selectionScriptText = context.optimizeJavaScript(logger, selectionScriptText); return selectionScriptText; } private void appendProcessMetas(TreeLogger logger, LinkerContext context, StringBuffer buffer) throws UnableToCompleteException { int startPos = buffer.indexOf("// __PROCESS_METAS__"); if (startPos != -1) { StringBuffer processMetas = readFileToStringBuffer(getJsProcessMetas(context), logger); buffer.insert(startPos, processMetas.toString()); } } private void appendPageLoaderFunction(TreeLogger logger, LinkerContext context, StringBuffer buffer) throws UnableToCompleteException { int startPos = buffer.indexOf("// __PAGE_LOADER_FUNCTION__"); if (startPos != -1) { StringBuffer pageLoader = readFileToStringBuffer(getPageLoadFunction(logger, context), logger); buffer.insert(startPos, pageLoader.toString()); } } protected StringBuffer readFileToStringBuffer(String filename, TreeLogger logger) throws UnableToCompleteException { StringBuffer buffer; try { buffer = new StringBuffer(Utility.getFileFromClassPath(filename)); } catch (IOException e) { logger.log(TreeLogger.ERROR, "Unable to read file: " + filename, e); throw new UnableToCompleteException(); } return buffer; } protected static void replaceAll(StringBuffer buf, String search, String replace) { int len = search.length(); for (int pos = buf.indexOf(search); pos >= 0; pos = buf.indexOf(search, pos + 1)) { buf.replace(pos, pos + len, replace); } } private String fillSelectionScriptTemplate(StringBuffer selectionScript, TreeLogger logger, LinkerContext context, ArtifactSet artifacts) throws UnableToCompleteException { permutationsUtil.addPermutationsJs(selectionScript, logger, context); replaceAll(selectionScript, "__MODULE_FUNC__", context.getModuleFunctionName()); replaceAll(selectionScript, "__MODULE_NAME__", context.getModuleName()); return selectionScript.toString(); } /** * Returns the name of the {@code JsProcessMetas} script. By default, returns * {@code "com/google/gwt/core/ext/linker/impl/processMetas.js"}. * * @param context a LinkerContext */ protected String getJsProcessMetas(LinkerContext context) { return "org/cruxframework/crux/core/rebind/offline/processMetas.js"; } protected String getSelectionScriptTemplate(TreeLogger logger, LinkerContext context) { return "org/cruxframework/crux/core/rebind/offline/OfflineSelectionTemplate.js"; } protected String getPageLoadFunction(TreeLogger logger, LinkerContext context) { return "org/cruxframework/crux/core/rebind/offline/LoadPageFunction.js"; } protected String getOfflinePageTemplate(TreeLogger logger, LinkerContext context) { return "org/cruxframework/crux/core/rebind/offline/OfflinePage.html"; } /********************************************************** * utilities **********************************************************/ private static boolean acceptCachedResource(String filename) { if (filename.startsWith("compile-report/")) { return false; } for (String acceptedExtension : acceptedFileExtensions) { if (filename.endsWith(acceptedExtension)) { return true; } } return false; } private String getTargetScreenId(LinkerContext context, TreeLogger logger, String screenID) throws UnableToCompleteException { // if (StringUtils.isEmpty(screenID)) // { // screenID = CruxBridge.getInstance().getLastPageRequested(); // try // { // screenID = ScreenFactory.getInstance().getScreen(screenID, null).getRelativeId(); // } // catch (ScreenConfigException e) // { // logger.log(TreeLogger.ERROR, e.getMessage(), e); // throw new UnableToCompleteException(); // } // } // // TODO checar se a pagina esta no lugar certo... aceitar apenas na raiz do modulo (/cruxsite/<nomePagina>.html) // return screenID; return null; } static long getCurrentTimeTruncatingMiliseconds() { long currentTime = (System.currentTimeMillis() / 1000) * 1000; return currentTime; } static String getManifestName() { return "offline.appcache"; } private static String getManifestName(String artifactGroupId) { return artifactGroupId + ".appcache"; } public static String getManifestName(ArtifactSet artifacts) { String userAgent = getUserAgent(artifacts); String deviceFeatures = getDeviceFeatures(artifacts); return getManifestName(new ArtifactsGroup(userAgent, deviceFeatures).getGroupId()); } private static String getManifestLoaderName(String artifactGroupId) { return "offlineLoader_" + artifactGroupId + ".cache.html"; } public ArtifactsGroup getGroup(ArtifactSet artifacts) { String userAgent = getUserAgent(artifacts); String deviceFeatures = getDeviceFeatures(artifacts); return new ArtifactsGroup(userAgent, deviceFeatures); } private static String getUserAgent(ArtifactSet artifacts) { return getProperty(artifacts, "user.agent"); } private static String getDeviceFeatures(ArtifactSet artifacts) { return getProperty(artifacts, "device.features"); } private static String getProperty(ArtifactSet artifacts, String property) { for (CompilationResult result : artifacts.find(CompilationResult.class)) { if (result.getPropertyMap() != null && !result.getPropertyMap().isEmpty()) { for (SortedMap<SelectionProperty, String> propertyMap : result.getPropertyMap()) { for (SelectionProperty selectionProperty : propertyMap.keySet()) { if (property.equals(selectionProperty.getName())) { return propertyMap.get(selectionProperty); } } } } } return null; } } /** * Class description: Implements a artifacts group. A group is defined by user.agent and locale of an artifact. * * @author alexandre.costa */ class ArtifactsGroup { private static final String DEFAULT = "default"; private final String userAgent; private final String deviceFeatures; /** * Constructor. * * @param userAgent user agent * @param deviceFeatures device features */ public ArtifactsGroup(String userAgent, String deviceFeatures) { this.userAgent = userAgent == null || userAgent.trim().equals("") ? DEFAULT : userAgent; this.deviceFeatures = deviceFeatures == null || deviceFeatures.trim().equals("") ? DEFAULT : deviceFeatures; } public String getGroupId() { return userAgent + "_" + deviceFeatures; } /**************************************** * hashCode and equals ****************************************/ /* * (non-Javadoc) * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((deviceFeatures == null) ? 0 : deviceFeatures.hashCode()); result = prime * result + ((userAgent == null) ? 0 : userAgent.hashCode()); return result; } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } ArtifactsGroup other = (ArtifactsGroup) obj; if (deviceFeatures == null) { if (other.deviceFeatures != null) { return false; } } else if (!deviceFeatures.equals(other.deviceFeatures)) { return false; } if (userAgent == null) { if (other.userAgent != null) { return false; } } else if (!userAgent.equals(other.userAgent)) { return false; } return true; } /**************************************** * getters and setters ***************************************/ /** * @return the userAgent */ public String getUserAgent() { return userAgent; } /** * @return the locale */ public String getDeviceFeatures() { return deviceFeatures; } }