/* * Copyright 2009 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.chrome.crx.linker; import java.io.IOException; import java.io.StringWriter; import java.util.Set; import java.util.SortedSet; import com.google.gwt.chrome.crx.client.Extension; import com.google.gwt.chrome.crx.linker.artifact.BrowserActionArtifact; import com.google.gwt.chrome.crx.linker.artifact.ContentScriptArtifact; import com.google.gwt.chrome.crx.linker.artifact.ExtensionArtifact; import com.google.gwt.chrome.crx.linker.artifact.ExtensionArtifact.IconInfo; import com.google.gwt.chrome.crx.linker.artifact.ExtensionScriptArtifact; import com.google.gwt.chrome.crx.linker.artifact.GwtContentScriptArtifact; import com.google.gwt.chrome.crx.linker.artifact.PageActionArtifact; import com.google.gwt.chrome.crx.linker.artifact.PluginArtifact; import com.google.gwt.chrome.crx.linker.artifact.ToolStripArtifact; 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.LinkerOrder; import com.google.gwt.core.ext.linker.LinkerOrder.Order; import com.google.json.serialization.JsonArray; import com.google.json.serialization.JsonObject; import com.google.json.serialization.JsonValue; /** * Linker for chrome Extensions. */ @LinkerOrder(Order.PRIMARY) public class ExtensionLinker extends AbstractLinker { private static ArtifactSet addArtifacts(ArtifactSet artifacts, Artifact<?>... newArtifacts) { final ArtifactSet newSet = new ArtifactSet(artifacts); for (Artifact<?> artifact : newArtifacts) { newSet.add(artifact); } return newSet; } private static JsonValue createContentScriptsArray(SortedSet<ContentScriptArtifact> contentScripts, SortedSet<GwtContentScriptArtifact> gwtContentScripts) { final JsonArray array = JsonArray.create(); for (ContentScriptArtifact contentScript : contentScripts) { JsonObject entry = JsonObject.create(); JsonArray whiteList = JsonArray.create(); String[] patterns = contentScript.getWhiteList(); for (String pattern : patterns) { whiteList.add(pattern); } entry.put("matches", whiteList); JsonArray path = JsonArray.create(); path.add(contentScript.getPath()); entry.put("js", path); entry.put("run_at", contentScript.getRunAt()); entry.put("all_frames", true); array.add(entry); } for (GwtContentScriptArtifact contentScript : gwtContentScripts) { JsonObject entry = JsonObject.create(); JsonArray whiteList = JsonArray.create(); String[] patterns = contentScript.getMatches(); for (String pattern : patterns) { whiteList.add(pattern); } entry.put("matches", whiteList); JsonArray path = JsonArray.create(); path.add(contentScript.getPath()); entry.put("js", path); entry.put("run_at", contentScript.getRunAt()); entry.put("all_frames", contentScript.isAllFrames()); array.add(entry); } return array; } private static JsonValue createPageActionsArray(SortedSet<PageActionArtifact> pageActions) { final JsonArray pageActionArray = JsonArray.create(); for (PageActionArtifact pageAction : pageActions) { final JsonObject pageActionObject = JsonObject.create(); pageActionObject.put("id", pageAction.getId()); pageActionObject.put("name", pageAction.getName()); if (null != pageAction.getPopup()) { pageActionObject.put("popup", pageAction.getPopup()); } final JsonArray iconsArray = JsonArray.create(); String[] icons = pageAction.getIcons(); for (String icon : icons) { iconsArray.add(icon); } pageActionObject.put("icons", iconsArray); pageActionArray.add(pageActionObject); } return pageActionArray; } private static JsonValue createPluginsArray(SortedSet<PluginArtifact> plugins) { JsonArray pluginsArray = JsonArray.create(); for (PluginArtifact plugin : plugins) { JsonObject pluginObject = JsonObject.create(); pluginObject.put("path", plugin.getPath()); pluginObject.put("public", plugin.isPublic()); pluginsArray.add(pluginObject); } return pluginsArray; } private static JsonArray createToolStripsArray(Set<ToolStripArtifact> toolstrips) { final JsonArray array = JsonArray.create(); for (ToolStripArtifact toolstrip : toolstrips) { array.add(toolstrip.getPath()); } return array; } private static CompilationResult findCompilation(TreeLogger logger, ArtifactSet artifacts) throws UnableToCompleteException { final SortedSet<CompilationResult> compilations = artifacts.find(CompilationResult.class); if (compilations.size() > 1) { logger.log(TreeLogger.ERROR, "One permutation per module, please. Seriously, you changed something you weren't supposed to."); throw new UnableToCompleteException(); } return compilations.first(); } private static ExtensionArtifact findExtensionArtifact(TreeLogger logger, ArtifactSet artifacts) throws UnableToCompleteException { final SortedSet<ExtensionArtifact> extensions = artifacts.find(ExtensionArtifact.class); if (extensions.size() > 1) { // TODO(knorton): Improve error message. logger.log(TreeLogger.ERROR, "One extension per module, please."); throw new UnableToCompleteException(); } return extensions.first(); } private static String generateHtmlContents(TreeLogger logger, CompilationResult js, SortedSet<ExtensionScriptArtifact> extensionScripts) throws UnableToCompleteException { // Thou shalt not reference $doc or $wnd !!! String compiledJs = js.getJavaScript()[0]; final StringBuffer buffer = new StringBuffer(); buffer.append("<html>"); buffer.append("<head>"); if (!extensionScripts.isEmpty()) { for (ExtensionScriptArtifact extensionScriptArtifact : extensionScripts) { if (!"".equals(extensionScriptArtifact.getPath())) { buffer.append("<script type=\"text/javascript\" src=\"" + extensionScriptArtifact.getPath() + "\"></script>"); } if (!"".equals(extensionScriptArtifact.getScript())) { buffer.append("<script type=\"text/javascript\">" + extensionScriptArtifact.getScript() + "</script>"); } } } buffer.append("</head>"); buffer.append("<body>"); buffer.append("<script>var $stats;\n" + "var $wnd = window;\nvar $doc = $wnd.document;" + compiledJs + "gwtOnLoad();\n</script>"); buffer.append("</body>"); buffer.append("</html>"); return buffer.toString(); } private static String getHtmlFilename(LinkerContext context) { return context.getModuleName() + ".html"; } @Override public String getDescription() { return "Chrome Extension Linker"; } @Override public ArtifactSet link(TreeLogger logger, LinkerContext context, ArtifactSet artifacts) throws UnableToCompleteException { // Retrieve the spec for the extension. final ExtensionArtifact extension = findExtensionArtifact(logger, artifacts); // Retrieve the PageActions. final SortedSet<PageActionArtifact> pageActions = artifacts.find(PageActionArtifact.class); // Retrieve the BrowserAction (should be a collection of size one). final SortedSet<BrowserActionArtifact> browserActions = artifacts.find(BrowserActionArtifact.class); // Retrieve the ToolStrips. final SortedSet<ToolStripArtifact> toolStrips = artifacts.find(ToolStripArtifact.class); // Retrieve the ContentScripts final SortedSet<ContentScriptArtifact> contentScripts = artifacts.find(ContentScriptArtifact.class); // Retrieve the ExtensionScripts final SortedSet<ExtensionScriptArtifact> extensionScripts = artifacts.find(ExtensionScriptArtifact.class); // Retrieve the plugins. final SortedSet<PluginArtifact> plugins = artifacts.find(PluginArtifact.class); final SortedSet<GwtContentScriptArtifact> gwtContentScripts = artifacts.find(GwtContentScriptArtifact.class); // Retrieve the compilation. final CompilationResult compilation = findCompilation(logger, artifacts); String backgroundPageFileName = getHtmlFilename(context); return addArtifacts( artifacts, emitString(logger, generateHtmlContents(logger, compilation, extensionScripts), backgroundPageFileName), emitString( logger, generateManifestContents(logger, backgroundPageFileName, extension, pageActions, browserActions, toolStrips, contentScripts, plugins, gwtContentScripts), "manifest.json")); } private JsonObject createBrowserAction(SortedSet<BrowserActionArtifact> browserActions) { int size = browserActions.size(); if (size == 0) { return null; } // TODO(jaimeyap): This should really be a user error and not an // assertion // failure. assert (size == 1) : "Only 1 BrowserAction per extension allowed."; BrowserActionArtifact browserAction = browserActions.first(); final JsonObject browserActionObject = JsonObject.create(); browserActionObject.put("name", browserAction.getName()); final JsonArray iconsArray = JsonArray.create(); String[] icons = browserAction.getIcons(); for (String icon : icons) { iconsArray.add(icon); } browserActionObject.put("icons", iconsArray); browserActionObject.put("default_icon", browserAction.getDefaultIcon()); return browserActionObject; } private String generateManifestContents(TreeLogger logger, String backgroundPageFileName, ExtensionArtifact extension, SortedSet<PageActionArtifact> pageActions, SortedSet<BrowserActionArtifact> browserActions, SortedSet<ToolStripArtifact> toolStrips, SortedSet<ContentScriptArtifact> contentScripts, SortedSet<PluginArtifact> plugins, SortedSet<GwtContentScriptArtifact> gwtContentScripts) throws UnableToCompleteException { final JsonObject config = JsonObject.create(); config.put("name", extension.getName()); config.put("version", extension.getVersion()); if (!extension.getUpdateUrl().equals(Extension.NO_UPDATE_URL)) { config.put("update_url", extension.getUpdateUrl()); } // All other fields in the manifest are optional if (extension.getDescription().length() > 0) { config.put("description", extension.getDescription()); } if (backgroundPageFileName.length() > 0) { config.put("background_page", backgroundPageFileName); } if (toolStrips.size() > 0) { config.put("toolstrips", createToolStripsArray(toolStrips)); } if (!contentScripts.isEmpty() || !gwtContentScripts.isEmpty()) { config.put("content_scripts", createContentScriptsArray(contentScripts, gwtContentScripts)); } if (pageActions.size() > 0) { config.put("page_actions", createPageActionsArray(pageActions)); } if (browserActions.size() > 0) { JsonValue browserAction = createBrowserAction(browserActions); if (browserAction != null) { config.put("browser_action", browserAction); } } if (plugins.size() > 0) { config.put("plugins", createPluginsArray(plugins)); } if (extension.getPermissions().length > 0) { final JsonArray perms = JsonArray.create(); for (String perm : extension.getPermissions()) { perms.add(perm); } config.put("permissions", perms); } if (extension.getIcons().length > 0) { JsonObject icons = JsonObject.create(); for (IconInfo info : extension.getIcons()) { icons.put(Integer.toString(info.getSize()), info.getFilename()); } config.put("icons", icons); } final StringWriter writer = new StringWriter(); try { config.write(writer); } catch (IOException e) { logger.log(TreeLogger.ERROR, "Unexpected error.", e); throw new UnableToCompleteException(); } return writer.toString(); } }