| /* |
| * Copyright 2008 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.ArtifactSet; |
| import com.google.gwt.core.ext.linker.CompilationResult; |
| import com.google.gwt.core.ext.linker.ConfigurationProperty; |
| import com.google.gwt.core.ext.linker.LinkerOrder; |
| import com.google.gwt.core.ext.linker.LinkerOrder.Order; |
| import com.google.gwt.core.ext.linker.Shardable; |
| import com.google.gwt.core.ext.linker.StatementRanges; |
| import com.google.gwt.core.ext.linker.impl.SelectionScriptLinker; |
| import com.google.gwt.dev.About; |
| import com.google.gwt.dev.util.DefaultTextOutput; |
| import com.google.gwt.dev.util.Util; |
| |
| import java.util.SortedSet; |
| |
| /** |
| * Implements the canonical GWT bootstrap sequence that loads the GWT module in |
| * a separate iframe. |
| */ |
| @LinkerOrder(Order.PRIMARY) |
| @Shardable |
| public class IFrameLinker extends SelectionScriptLinker { |
| /** |
| * This string is inserted between script chunks. It is made default access |
| * for testing. |
| */ |
| static final String SCRIPT_CHUNK_SEPARATOR = "--></script>\n<script><!--\n"; |
| |
| /** |
| * A configuration property indicating how large each script tag should be. |
| */ |
| private static final String CHUNK_SIZE_PROPERTY = "iframe.linker.script.chunk.size"; |
| |
| /** |
| * A configuration property that can be used to have the linker load from |
| * somewhere other than {@link #FRAGMENT_SUBDIR}. |
| */ |
| private static final String PROP_FRAGMENT_SUBDIR_OVERRIDE = "iframe.linker.deferredjs.subdir"; |
| |
| /** |
| * Split a JavaScript string into multiple chunks, at statement boundaries. |
| * Insert and end-script tag and a start-script tag in between each chunk. |
| * This method is made default access for testing. |
| * |
| * @param ranges Describes where the statements are located within the |
| * JavaScript code. If <code>null</code>, then return <code>js</code> |
| * unchanged. |
| * @param js The JavaScript code to be split up. |
| * @param charsPerChunk The number of characters to be put in each script tag |
| */ |
| static String splitPrimaryJavaScript(StatementRanges ranges, String js, |
| int charsPerChunk) { |
| if (charsPerChunk < 0 || ranges == null) { |
| return js; |
| } |
| |
| StringBuilder sb = new StringBuilder(); |
| int bytesInCurrentTag = 0; |
| |
| for (int i = 0; i < ranges.numStatements(); i++) { |
| int start = ranges.start(i); |
| int end = ranges.end(i); |
| int length = end - start; |
| if (bytesInCurrentTag > 0 && bytesInCurrentTag + length > charsPerChunk) { |
| if (lastChar(sb) != '\n') { |
| sb.append('\n'); |
| } |
| sb.append(SCRIPT_CHUNK_SEPARATOR); |
| bytesInCurrentTag = 0; |
| } |
| if (bytesInCurrentTag > 0) { |
| char lastChar = lastChar(sb); |
| if (lastChar != '\n' && lastChar != ';' && lastChar != '}') { |
| /* |
| * Make sure this statement has a separator from the last one. |
| */ |
| sb.append(";"); |
| } |
| } |
| sb.append(js, start, end); |
| bytesInCurrentTag += length; |
| } |
| return sb.toString(); |
| } |
| |
| private static char lastChar(StringBuilder sb) { |
| return sb.charAt(sb.length() - 1); |
| } |
| |
| @Override |
| public String getDescription() { |
| return "Standard"; |
| } |
| |
| /* |
| * This implementation divides the code of the initial fragment into multiple |
| * script tags. These chunked script tags loads faster on Firefox even when |
| * the data is cached. Additionally, having the script tags separated means |
| * that the early ones can be evaluated before the later ones have finished |
| * downloading. As a result of this parallelism, the overall time to get the |
| * JavaScript downloaded and evaluated can lower. |
| */ |
| @Override |
| protected byte[] generatePrimaryFragment(TreeLogger logger, |
| LinkerContext context, CompilationResult result, String[] js, |
| ArtifactSet artifacts) throws UnableToCompleteException { |
| StringBuffer b = new StringBuffer(); |
| b.append(getModulePrefix(logger, context, result.getStrongName(), js.length)); |
| b.append(splitPrimaryJavaScript(result.getStatementRanges()[0], js[0], |
| charsPerChunk(context, logger))); |
| b.append(getModuleSuffix(logger, context)); |
| return Util.getBytes(b.toString()); |
| } |
| |
| @Override |
| protected String getCompilationExtension(TreeLogger logger, |
| LinkerContext context) { |
| return ".cache.html"; |
| } |
| |
| /** |
| * Returns the subdirectory name to be used by getModulPrefix when requesting |
| * a runAsync module. It is specified by |
| * {@link #PROP_FRAGMENT_SUBDIR_OVERRIDE} and, aside from test cases, is |
| * always {@link #FRAGMENT_SUBDIR}. |
| */ |
| protected final String getFragmentSubdir(TreeLogger logger, |
| LinkerContext context) throws UnableToCompleteException { |
| String subdir = null; |
| for (ConfigurationProperty prop : context.getConfigurationProperties()) { |
| if (prop.getName().equals(PROP_FRAGMENT_SUBDIR_OVERRIDE)) { |
| subdir = prop.getValues().get(0); |
| } |
| } |
| |
| if (subdir == null) { |
| logger.log(TreeLogger.ERROR, "Could not find property " |
| + PROP_FRAGMENT_SUBDIR_OVERRIDE); |
| throw new UnableToCompleteException(); |
| } |
| |
| return subdir; |
| } |
| |
| @Override |
| protected String getHostedFilename() { |
| return "hosted.html"; |
| } |
| |
| @Override |
| protected String getModulePrefix(TreeLogger logger, LinkerContext context, |
| String strongName) throws UnableToCompleteException { |
| return getModulePrefix(logger, context, strongName, true); |
| } |
| |
| @Override |
| protected String getModulePrefix(TreeLogger logger, LinkerContext context, |
| String strongName, int numFragments) throws UnableToCompleteException { |
| return getModulePrefix(logger, context, strongName, numFragments > 1); |
| } |
| |
| @Override |
| protected String getModuleSuffix(TreeLogger logger, LinkerContext context) { |
| DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact()); |
| |
| out.print("$stats && $stats({moduleName:'" + context.getModuleName() |
| + "',sessionId:$sessionId" |
| + ",subSystem:'startup',evtGroup:'moduleStartup'" |
| + ",millis:(new Date()).getTime(),type:'moduleEvalEnd'});"); |
| |
| // Generate the call to tell the bootstrap code that we're ready to go. |
| out.newlineOpt(); |
| out.print("if ($wnd." + context.getModuleFunctionName() + ") $wnd." |
| + context.getModuleFunctionName() + ".onScriptLoad();"); |
| out.newline(); |
| out.print("--></script></body></html>"); |
| out.newlineOpt(); |
| |
| return out.toString(); |
| } |
| |
| @Override |
| protected String getSelectionScriptTemplate(TreeLogger logger, LinkerContext context) { |
| return "com/google/gwt/core/linker/IFrameTemplate.js"; |
| } |
| |
| protected String modifyPrimaryJavaScript(String js) { |
| return js; |
| } |
| |
| /** |
| * Extract via {@link #CHUNK_SIZE_PROPERTY} the number of characters to be |
| * included in each script tag. |
| */ |
| private int charsPerChunk(LinkerContext context, TreeLogger logger) |
| throws UnableToCompleteException { |
| SortedSet<ConfigurationProperty> configProps = context.getConfigurationProperties(); |
| for (ConfigurationProperty prop : configProps) { |
| if (prop.getName().equals(CHUNK_SIZE_PROPERTY)) { |
| return Integer.parseInt(prop.getValues().get(0)); |
| } |
| } |
| |
| logger.log(TreeLogger.ERROR, "Unable to find configuration property " |
| + CHUNK_SIZE_PROPERTY); |
| throw new UnableToCompleteException(); |
| } |
| |
| /** |
| * This is the real implementation of <code>getModulePrefix</code> for this |
| * linker. The other versions forward to this one. |
| */ |
| private String getModulePrefix(TreeLogger logger, LinkerContext context, |
| String strongName, boolean supportRunAsync) |
| throws UnableToCompleteException { |
| DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact()); |
| out.print("<html>"); |
| out.newlineOpt(); |
| |
| // Setup the well-known variables. |
| out.print("<head><meta charset=\"UTF-8\" /><script>"); |
| out.newlineOpt(); |
| out.print("var $gwt_version = \"" + About.getGwtVersionNum() + "\";"); |
| out.newlineOpt(); |
| out.print("var $wnd = parent;"); |
| out.newlineOpt(); |
| out.print("var $doc = $wnd.document;"); |
| out.newlineOpt(); |
| out.print("var $moduleName, $moduleBase;"); |
| out.newlineOpt(); |
| out.print("var $strongName = '" + strongName + "';"); |
| out.newlineOpt(); |
| if (supportRunAsync) { |
| out.print("function __gwtStartLoadingFragment(frag) {"); |
| out.indentIn(); |
| out.newlineOpt(); |
| out.print(" return $moduleBase + '" + getFragmentSubdir(logger, context) |
| + "/' + $strongName + '/' + frag + '" + FRAGMENT_EXTENSION + "';"); |
| out.indentOut(); |
| out.newlineOpt(); |
| out.print("};"); |
| out.newlineOpt(); |
| out.print("function __gwtInstallCode(code) {"); |
| /* |
| * Use a script tag on all platforms, for simplicity. It would be cleaner |
| * to use window.eval, but at the time of writing that only reliably works |
| * on Firefox. It would also be cleaner to use window.execScript on |
| * platforms that support it (IE and Chrome). However, trying this causes |
| * IE 6 (and possibly others) to emit "error 80020101", apparently due to |
| * something objectionable in the compiler's output JavaScript. |
| */ |
| out.indentIn(); |
| out.newlineOpt(); |
| out.print("var head = document.getElementsByTagName('head').item(0);"); |
| out.newlineOpt(); |
| out.print("var script = document.createElement('script');"); |
| out.newlineOpt(); |
| out.print("script.type = 'text/javascript';"); |
| out.newlineOpt(); |
| out.print("script.text = code;"); |
| out.newlineOpt(); |
| out.print("head.appendChild(script);"); |
| out.indentOut(); |
| out.newlineOpt(); |
| out.print("};"); |
| out.newlineOpt(); |
| } |
| out.print("var $stats = $wnd.__gwtStatsEvent ? function(a) {return $wnd.__gwtStatsEvent(a);} : null,"); |
| out.newlineOpt(); |
| out.print("$sessionId = $wnd.__gwtStatsSessionId ? $wnd.__gwtStatsSessionId : null;"); |
| out.newlineOpt(); |
| out.print("$stats && $stats({moduleName:'" + context.getModuleName() |
| + "',sessionId:$sessionId" |
| + ",subSystem:'startup',evtGroup:'moduleStartup'" |
| + ",millis:(new Date()).getTime(),type:'moduleEvalStart'});"); |
| out.newlineOpt(); |
| out.print("</script></head>"); |
| out.newlineOpt(); |
| out.print("<body>"); |
| out.newlineOpt(); |
| |
| // Begin a script block inside the body. It's commented out so that the |
| // browser won't mistake strings containing "<script>" for actual script. |
| out.print("<script><!--"); |
| out.newline(); |
| return out.toString(); |
| } |
| |
| } |