| /* |
| * 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.ext.linker.impl; |
| |
| import com.google.gwt.core.ext.LinkerContext; |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.TreeLogger.Type; |
| 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.SelectionProperty; |
| import com.google.gwt.core.ext.linker.SoftPermutation; |
| import com.google.gwt.core.ext.linker.StatementRanges; |
| import com.google.gwt.core.linker.SymbolMapsLinker; |
| import com.google.gwt.dev.util.Util; |
| import com.google.gwt.util.tools.Utility; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URL; |
| import java.net.URLConnection; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.SortedMap; |
| import java.util.SortedSet; |
| import java.util.TreeMap; |
| |
| /** |
| * A base class for Linkers that use an external script to boostrap the GWT module. This |
| * implementation injects JavaScript Snippets into a JS program defined in an external file.<br /> |
| * |
| * Created nocache.js files must provide an implementation of both __gwt_isKnownPropertyValue() and |
| * __gwt_getMetaProperty() to support the permutation selection process. These functions must be |
| * available within the nocache.js scope and must be made available to the permutation js scope. |
| */ |
| public abstract class SelectionScriptLinker extends AbstractLinker { |
| |
| public static final String USE_SOURCE_MAPS_PROPERTY = "compiler.useSourceMaps"; |
| |
| /** |
| * File name for computeScriptBase.js. |
| */ |
| protected static final String COMPUTE_SCRIPT_BASE_JS = "com/google/gwt/core/ext/linker/impl/computeScriptBaseOld.js"; |
| |
| /** |
| * The extension added to demand-loaded fragment files. |
| */ |
| protected static final String FRAGMENT_EXTENSION = ".cache.js"; |
| |
| /** |
| * A subdirectory to hold all the generated fragments. |
| */ |
| protected static final String FRAGMENT_SUBDIR = "deferredjs"; |
| |
| /** |
| * Utility class to handle insertion of permutations code. |
| */ |
| protected static PermutationsUtil permutationsUtil = new PermutationsUtil(); |
| |
| /** |
| * File name for processMetas.js. |
| */ |
| protected static final String PROCESS_METAS_JS = "com/google/gwt/core/ext/linker/impl/processMetasOld.js"; |
| |
| /** |
| * TODO(bobv): Move this class into c.g.g.core.linker when HostedModeLinker |
| * goes away? |
| */ |
| |
| /** |
| * 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. 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. |
| * @param scriptChunkSeparator The string to insert between chunks. |
| * @param context |
| */ |
| public static String splitPrimaryJavaScript(StatementRanges ranges, String js, |
| int charsPerChunk, String scriptChunkSeparator, LinkerContext context) { |
| boolean useSourceMaps = false; |
| for (SelectionProperty prop : context.getProperties()) { |
| if (USE_SOURCE_MAPS_PROPERTY.equals(prop.getName())) { |
| String str = prop.tryGetValue(); |
| useSourceMaps = str == null ? false : Boolean.parseBoolean(str); |
| break; |
| } |
| } |
| |
| // TODO(cromwellian) enable chunking with sourcemaps |
| if (charsPerChunk < 0 || ranges == null || useSourceMaps) { |
| return js; |
| } |
| |
| StringBuilder sb = new StringBuilder((int) (js.length() * 1.05)); |
| int bytesInCurrentChunk = 0; |
| |
| for (int i = 0; i < ranges.numStatements(); i++) { |
| int start = ranges.start(i); |
| int end = ranges.end(i); |
| int length = end - start; |
| if (bytesInCurrentChunk > 0 && bytesInCurrentChunk + length > charsPerChunk) { |
| if (lastChar(sb) != '\n') { |
| sb.append('\n'); |
| } |
| sb.append(scriptChunkSeparator); |
| bytesInCurrentChunk = 0; |
| } |
| if (bytesInCurrentChunk > 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); |
| bytesInCurrentChunk += length; |
| } |
| return sb.toString(); |
| } |
| |
| 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 static char lastChar(StringBuilder sb) { |
| return sb.charAt(sb.length() - 1); |
| } |
| |
| /** |
| * This method is left in place for existing subclasses of SelectionScriptLinker that have not |
| * been upgraded for the sharding API. |
| */ |
| @Override |
| public ArtifactSet link(TreeLogger logger, LinkerContext context, |
| ArtifactSet artifacts) throws UnableToCompleteException { |
| ArtifactSet toReturn = link(logger, context, artifacts, true); |
| toReturn = link(logger, context, toReturn, false); |
| return toReturn; |
| } |
| |
| @Override |
| public ArtifactSet link(TreeLogger logger, LinkerContext context, |
| ArtifactSet artifacts, boolean onePermutation) |
| throws UnableToCompleteException { |
| if (onePermutation) { |
| ArtifactSet toReturn = new ArtifactSet(artifacts); |
| ArtifactSet writableArtifacts = new ArtifactSet(artifacts); |
| |
| /* |
| * Support having multiple compilation results because this method is also |
| * called from the legacy link method. |
| */ |
| for (CompilationResult compilation : toReturn.find(CompilationResult.class)) { |
| // pass a writable set so that other stages can use this set for temporary storage |
| toReturn.addAll(doEmitCompilation(logger, context, compilation, writableArtifacts)); |
| maybeAddHostedModeFile(logger, context, toReturn, compilation); |
| } |
| /* |
| * Find edit artifacts added during linking and add them. This is done as a way of returning |
| * arbitrary extra outputs from within the linker methods without having to modify |
| * their return signatures to pass extra return data around. |
| */ |
| for (SymbolMapsLinker.ScriptFragmentEditsArtifact ea : writableArtifacts.find( |
| SymbolMapsLinker.ScriptFragmentEditsArtifact.class)) { |
| toReturn.add(ea); |
| } |
| return toReturn; |
| } else { |
| permutationsUtil.setupPermutationsMap(artifacts); |
| ArtifactSet toReturn = new ArtifactSet(artifacts); |
| EmittedArtifact art = emitSelectionScript(logger, context, artifacts); |
| if (art != null) { |
| toReturn.add(art); |
| } |
| maybeOutputPropertyMap(logger, context, toReturn); |
| maybeAddHostedModeFile(logger, context, toReturn, null); |
| return toReturn; |
| } |
| } |
| |
| @Override |
| public boolean supportsDevModeInJunit(LinkerContext context) { |
| return !"".equals(getHostedFilename()); |
| } |
| |
| /** |
| * Extract via {@link #CHUNK_SIZE_PROPERTY} the number of characters to be included in each |
| * chunk. |
| */ |
| protected int charsPerChunk(LinkerContext context, TreeLogger logger) { |
| SortedSet<ConfigurationProperty> configurationProperties = context.getConfigurationProperties(); |
| for (ConfigurationProperty property : configurationProperties) { |
| if (property.getName().equals(CHUNK_SIZE_PROPERTY)) { |
| return Integer.parseInt(property.getValues().get(0)); |
| } |
| } |
| // CompilerParameters.gwt.xml indicates that if this property is -1, then |
| // no chunking is performed, so we return that as the default. Since |
| // Core.gwt.xml contains a definition for this property, this should never |
| // happen in production, but some tests mock out the ConfigurationProperties |
| // so we want to have a reasonable default rather than making them all add |
| // a value for this property. |
| return -1; |
| } |
| |
| protected Collection<Artifact<?>> doEmitCompilation(TreeLogger logger, |
| LinkerContext context, CompilationResult result, ArtifactSet artifacts) |
| throws UnableToCompleteException { |
| String[] js = result.getJavaScript(); |
| |
| Collection<Artifact<?>> toReturn = new ArrayList<Artifact<?>>(); |
| |
| byte[] primary = generatePrimaryFragment(logger, context, result, js, artifacts); |
| toReturn.add(emitBytes(logger, primary, result.getStrongName() |
| + getCompilationExtension(logger, context))); |
| primary = null; |
| |
| for (int i = 1; i < js.length; i++) { |
| byte[] bytes = Util.getBytes(generateDeferredFragment(logger, context, i, js[i], artifacts, |
| result)); |
| toReturn.add(emitBytes(logger, bytes, FRAGMENT_SUBDIR + File.separator |
| + result.getStrongName() + File.separator + i + FRAGMENT_EXTENSION)); |
| } |
| |
| toReturn.addAll(emitSelectionInformation(result.getStrongName(), result)); |
| return toReturn; |
| } |
| |
| protected List<Artifact<?>> emitSelectionInformation(String strongName, |
| CompilationResult result) { |
| List<Artifact<?>> emitted = new ArrayList<Artifact<?>>(); |
| |
| for (SortedMap<SelectionProperty, String> propertyMap : result.getPropertyMap()) { |
| TreeMap<String, String> propMap = new TreeMap<String, String>(); |
| for (Map.Entry<SelectionProperty, String> entry : propertyMap.entrySet()) { |
| propMap.put(entry.getKey().getName(), entry.getValue()); |
| } |
| |
| // The soft properties may not be a subset of the existing set |
| for (SoftPermutation soft : result.getSoftPermutations()) { |
| // Make a copy we can add add more properties to |
| TreeMap<String, String> softMap = new TreeMap<String, String>(propMap); |
| // Make sure this SelectionInformation contains the soft properties |
| for (Map.Entry<SelectionProperty, String> entry : soft.getPropertyMap().entrySet()) { |
| softMap.put(entry.getKey().getName(), entry.getValue()); |
| } |
| emitted.add(new SelectionInformation(strongName, soft.getId(), softMap)); |
| } |
| } |
| |
| return emitted; |
| } |
| |
| protected EmittedArtifact emitSelectionScript(TreeLogger logger, |
| LinkerContext context, ArtifactSet artifacts) |
| throws UnableToCompleteException { |
| /* |
| * Last modified is important to keep Development Mode refreses from |
| * clobbering Production Mode compiles. We set the timestamp on the |
| * Development Mode selection script to the same mod time as the module (to |
| * allow updates). For Production Mode, we just set it to now. |
| */ |
| long lastModified; |
| if (permutationsUtil.getPermutationsMap().isEmpty()) { |
| lastModified = context.getModuleLastModified(); |
| } else { |
| lastModified = System.currentTimeMillis(); |
| } |
| String ss = generateSelectionScript(logger, context, artifacts); |
| return emitString(logger, ss, context.getModuleName() |
| + ".nocache.js", lastModified); |
| } |
| |
| /** |
| * Generate a selection script. The selection information should previously have been scanned |
| * using {@link PermutationsUtil#setupPermutationsMap(ArtifactSet)}. |
| */ |
| protected String fillSelectionScriptTemplate(StringBuffer selectionScript, |
| TreeLogger logger, LinkerContext context, ArtifactSet artifacts, |
| CompilationResult result) throws |
| UnableToCompleteException { |
| String computeScriptBase; |
| String processMetas; |
| try { |
| computeScriptBase = Utility.getFileFromClassPath(COMPUTE_SCRIPT_BASE_JS); |
| processMetas = Utility.getFileFromClassPath(PROCESS_METAS_JS); |
| } catch (IOException e) { |
| logger.log(TreeLogger.ERROR, "Unable to read selection script template", |
| e); |
| throw new UnableToCompleteException(); |
| } |
| replaceAll(selectionScript, "__COMPUTE_SCRIPT_BASE__", computeScriptBase); |
| replaceAll(selectionScript, "__PROCESS_METAS__", processMetas); |
| |
| selectionScript = ResourceInjectionUtil.injectResources(selectionScript, artifacts); |
| permutationsUtil.addPermutationsJs(selectionScript, logger, context); |
| |
| replaceAll(selectionScript, "__MODULE_FUNC__", |
| context.getModuleFunctionName()); |
| replaceAll(selectionScript, "__MODULE_NAME__", context.getModuleName()); |
| replaceAll(selectionScript, "__HOSTED_FILENAME__", getHostedFilename()); |
| |
| return selectionScript.toString(); |
| } |
| |
| /** |
| * @param logger a TreeLogger |
| * @param context a LinkerContext |
| * @param fragment the fragment number |
| */ |
| protected String generateDeferredFragment(TreeLogger logger, |
| LinkerContext context, int fragment, String js, ArtifactSet artifacts, |
| CompilationResult result) |
| throws UnableToCompleteException { |
| StringBuilder b = new StringBuilder(); |
| String strongName = result == null ? "" : result.getStrongName(); |
| String prefix = getDeferredFragmentPrefix(logger, context, fragment); |
| b.append(prefix); |
| b.append(js); |
| String suffix = getDeferredFragmentSuffix2(logger, context, fragment, strongName); |
| if (suffix == null) { |
| logger.log(Type.ERROR, "getDeferredFragmentSuffix2 " |
| + "was not overridden in linker: " + getClass().getName()); |
| throw new UnableToCompleteException(); |
| } |
| b.append(suffix); |
| SymbolMapsLinker.ScriptFragmentEditsArtifact editsArtifact |
| = new SymbolMapsLinker.ScriptFragmentEditsArtifact(strongName, fragment); |
| editsArtifact.prefixLines(prefix); |
| artifacts.add(editsArtifact); |
| return wrapDeferredFragment(logger, context, fragment, b.toString(), artifacts); |
| } |
| |
| /** |
| * Generate the primary fragment. The default implementation is based on {@link |
| * #getModulePrefix(TreeLogger, LinkerContext, String, int)} and {@link |
| * #getModuleSuffix2(TreeLogger, LinkerContext, String)}. |
| */ |
| protected byte[] generatePrimaryFragment(TreeLogger logger, |
| LinkerContext context, CompilationResult result, String[] js, |
| ArtifactSet artifacts) throws UnableToCompleteException { |
| String temp = splitPrimaryJavaScript(result.getStatementRanges()[0], js[0], |
| charsPerChunk(context, logger), getScriptChunkSeparator(logger, context), context); |
| String primaryFragmentString = |
| generatePrimaryFragmentString(logger, context, result, temp, js.length, artifacts); |
| return Util.getBytes(primaryFragmentString); |
| } |
| |
| protected String generatePrimaryFragmentString(TreeLogger logger, |
| LinkerContext context, CompilationResult result, String js, int length, |
| ArtifactSet artifacts) |
| throws UnableToCompleteException { |
| StringBuilder b = new StringBuilder(); |
| String strongName = result == null ? "" : result.getStrongName(); |
| |
| String modulePrefix = getModulePrefix(logger, context, strongName, length); |
| SymbolMapsLinker.ScriptFragmentEditsArtifact editsArtifact |
| = new SymbolMapsLinker.ScriptFragmentEditsArtifact(strongName, 0); |
| editsArtifact.prefixLines(modulePrefix); |
| artifacts.add(editsArtifact); |
| b.append(modulePrefix); |
| b.append(js); |
| String suffix = getModuleSuffix2(logger, context, strongName); |
| if (suffix == null) { |
| logger.log(Type.ERROR, "getModuleSuffix2 was not overridden in " |
| + "linker: " + getClass().getName()); |
| throw new UnableToCompleteException(); |
| } |
| b.append(suffix); |
| return wrapPrimaryFragment(logger, context, b.toString(), artifacts, result); |
| } |
| |
| protected String generateSelectionScript(TreeLogger logger, |
| LinkerContext context, ArtifactSet artifacts) throws UnableToCompleteException { |
| return generateSelectionScript(logger, context, artifacts, null); |
| } |
| |
| protected String generateSelectionScript(TreeLogger logger, |
| LinkerContext context, ArtifactSet artifacts, CompilationResult result) |
| throws UnableToCompleteException { |
| String selectionScriptText; |
| StringBuffer buffer = readFileToStringBuffer( |
| getSelectionScriptTemplate(logger, context), logger); |
| selectionScriptText = fillSelectionScriptTemplate( |
| buffer, logger, context, artifacts, result); |
| selectionScriptText = |
| context.optimizeJavaScript(logger, selectionScriptText); |
| return selectionScriptText; |
| } |
| |
| protected abstract String getCompilationExtension(TreeLogger logger, |
| LinkerContext context) throws UnableToCompleteException; |
| |
| protected String getDeferredFragmentPrefix(TreeLogger logger, LinkerContext context, |
| int fragment) { |
| return ""; |
| } |
| |
| /** |
| * Returns the suffix at the end of a JavaScript fragment other than the initial fragment. |
| */ |
| protected String getDeferredFragmentSuffix2(TreeLogger logger, LinkerContext context, |
| int fragment, String strongName) { |
| return ""; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| protected String getHostedFilename() { |
| return ""; |
| } |
| |
| /** |
| * Compute the beginning of a JavaScript file that will hold the main module implementation. |
| */ |
| protected abstract String getModulePrefix(TreeLogger logger, |
| LinkerContext context, String strongName) |
| throws UnableToCompleteException; |
| |
| /** |
| * Compute the beginning of a JavaScript file that will hold the main module implementation. By |
| * default, calls {@link #getModulePrefix(TreeLogger, LinkerContext, String)}. |
| * |
| * @param strongName strong name of the module being emitted |
| * @param numFragments the number of fragments for this module, including the main fragment |
| * (fragment 0) |
| */ |
| protected String getModulePrefix(TreeLogger logger, LinkerContext context, |
| String strongName, int numFragments) throws UnableToCompleteException { |
| return getModulePrefix(logger, context, strongName); |
| } |
| |
| /** |
| * Returns the suffix for the initial JavaScript fragment. |
| */ |
| protected String getModuleSuffix2(TreeLogger logger, |
| LinkerContext context, String strongName) throws UnableToCompleteException { |
| return null; |
| } |
| |
| /** |
| * Some subclasses support "chunking" of the primary fragment. If chunking will be supported, this |
| * function should be overridden to return the string which should be inserted between each |
| * chunk. |
| */ |
| protected String getScriptChunkSeparator(TreeLogger logger, LinkerContext context) { |
| return ""; |
| } |
| |
| protected abstract String getSelectionScriptTemplate(TreeLogger logger, |
| LinkerContext context) throws UnableToCompleteException; |
| |
| /** |
| * Add the Development Mode file to the artifact set. |
| */ |
| protected void maybeAddHostedModeFile(TreeLogger logger, |
| LinkerContext context, ArtifactSet artifacts, CompilationResult result) |
| throws UnableToCompleteException { |
| String hostedFilename = getHostedFilename(); |
| if ("".equals(hostedFilename) || result != null) { |
| return; |
| } |
| try { |
| URL resource = SelectionScriptLinker.class.getResource(hostedFilename); |
| if (resource == null) { |
| logger.log(TreeLogger.ERROR, |
| "Unable to find support resource: " + hostedFilename); |
| throw new UnableToCompleteException(); |
| } |
| |
| final URLConnection connection = resource.openConnection(); |
| // TODO: extract URLArtifact class? |
| EmittedArtifact hostedFile = new EmittedArtifact( |
| SelectionScriptLinker.class, hostedFilename) { |
| @Override |
| public InputStream getContents(TreeLogger logger) |
| throws UnableToCompleteException { |
| try { |
| return connection.getInputStream(); |
| } catch (IOException e) { |
| logger.log(TreeLogger.ERROR, "Unable to copy support resource", e); |
| throw new UnableToCompleteException(); |
| } |
| } |
| |
| @Override |
| public long getLastModified() { |
| return connection.getLastModified(); |
| } |
| }; |
| artifacts.add(hostedFile); |
| } catch (IOException e) { |
| logger.log(TreeLogger.ERROR, "Unable to copy support resource", e); |
| throw new UnableToCompleteException(); |
| } |
| } |
| |
| protected void maybeOutputPropertyMap(TreeLogger logger, |
| LinkerContext context, ArtifactSet toReturn) { |
| return; |
| } |
| |
| 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 String wrapDeferredFragment(TreeLogger logger, |
| LinkerContext context, int fragment, String script, ArtifactSet artifacts) |
| throws UnableToCompleteException { |
| return script; |
| } |
| |
| protected String wrapPrimaryFragment(TreeLogger logger, |
| LinkerContext context, String script, ArtifactSet artifacts, |
| CompilationResult result) throws UnableToCompleteException { |
| return script; |
| } |
| } |