/* * Copyright 2007 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.dev.javac; import com.google.gwt.core.ext.CachedGeneratorResult; import com.google.gwt.core.ext.Generator; import com.google.gwt.core.ext.GeneratorContext; import com.google.gwt.core.ext.IncrementalGenerator; import com.google.gwt.core.ext.PropertyOracle; import com.google.gwt.core.ext.RebindResult; import com.google.gwt.core.ext.RebindRuleResolver; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.core.ext.linker.Artifact; import com.google.gwt.core.ext.linker.ArtifactSet; import com.google.gwt.core.ext.linker.GeneratedResource; import com.google.gwt.core.ext.linker.impl.StandardGeneratedResource; import com.google.gwt.core.ext.typeinfo.JClassType; import com.google.gwt.core.ext.typeinfo.TypeOracle; import com.google.gwt.dev.cfg.ModuleDef; import com.google.gwt.dev.resource.ResourceOracle; import com.google.gwt.dev.util.DiskCache; import com.google.gwt.dev.util.Util; import com.google.gwt.dev.util.collect.HashSet; import com.google.gwt.dev.util.collect.IdentityHashMap; import com.google.gwt.dev.util.log.speedtracer.CompilerEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; import com.google.gwt.util.tools.Utility; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedSet; /** * Manages generators and generated units during a single compilation. */ public class StandardGeneratorContext implements GeneratorContext { /** * Extras added to {@link GeneratedUnit}. */ private static interface Generated extends GeneratedUnit { void abort(); void commit(TreeLogger logger); } /** * This generated unit acts as a normal generated unit as well as a buffer * into which generators can write their source. A controller should ensure * that source isn't requested until the generator has finished writing it. * This version is backed by {@link StandardGeneratorContext#diskCache}. */ private static class GeneratedUnitImpl implements Generated { /** * A token to retrieve this object's bytes from the disk cache. */ protected long sourceToken = -1; private long creationTime; private String strongHash; // cache so that refreshes work correctly private StringWriter sw; private final String typeName; public GeneratedUnitImpl(StringWriter sw, String typeName) { this.typeName = typeName; this.sw = sw; } @Override public void abort() { sw = null; } /** * Finalizes the source and adds this generated unit to the host. */ @Override public void commit(TreeLogger logger) { String source = sw.toString(); strongHash = Util.computeStrongName(Util.getBytes(source)); sourceToken = diskCache.writeString(source); sw = null; creationTime = System.currentTimeMillis(); } @Override public long creationTime() { return creationTime; } @Override public String getSource() { if (sw != null) { throw new IllegalStateException("source not committed"); } return diskCache.readString(sourceToken); } @Override public long getSourceToken() { if (sw != null) { throw new IllegalStateException("source not committed"); } return sourceToken; } @Override public String getStrongHash() { return strongHash; } @Override public String getTypeName() { return typeName; } @Override public String optionalFileLocation() { return null; } } /** * This generated unit acts as a normal generated unit as well as a buffer * into which generators can write their source. A controller should ensure * that source isn't requested until the generator has finished writing it. * This version is backed by an explicit generated file. */ private static class GeneratedUnitWithFile extends GeneratedUnitImpl { private final File file; public GeneratedUnitWithFile(File file, StringWriter pw, String typeName) { super(pw, typeName); this.file = file; } @Override public void commit(TreeLogger logger) { super.commit(logger); FileOutputStream fos = null; try { fos = new FileOutputStream(file); diskCache.transferToStream(sourceToken, fos); } catch (IOException e) { logger.log(TreeLogger.WARN, "Error writing out generated unit at '" + file.getAbsolutePath() + "': " + e); } finally { Utility.close(fos); } } @Override public String optionalFileLocation() { return file.exists() ? Util.stripJarPathPrefix(file.getAbsolutePath()) : null; } } /** * Manages a resource that is in the process of being created by a generator. */ private static class PendingResource extends OutputStream { private ByteArrayOutputStream baos = new ByteArrayOutputStream(); private final String partialPath; public PendingResource(String partialPath) { this.partialPath = partialPath; } public void abort() { baos = null; } public String getPartialPath() { return partialPath; } public byte[] takeBytes() { byte[] result = baos.toByteArray(); baos = null; return result; } @Override public void write(byte[] b) throws IOException { if (baos == null) { throw new IOException("stream closed"); } baos.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { if (baos == null) { throw new IOException("stream closed"); } baos.write(b, off, len); } @Override public void write(int b) throws IOException { if (baos == null) { throw new IOException("stream closed"); } baos.write(b); } } private static final String GENERATOR_VERSION_ID_KEY = "generator-version-id"; private static DiskCache diskCache = DiskCache.INSTANCE; private static final Map<String, CompilerEventType> eventsByGeneratorType = new HashMap<String, CompilerEventType>(); static { eventsByGeneratorType.put( "com.google.gwt.resources.rebind.context.InlineClientBundleGenerator", CompilerEventType.GENERATOR_CLIENT_BUNDLE); eventsByGeneratorType.put("com.google.gwt.i18n.rebind.LocalizableGenerator", CompilerEventType.GENERATOR_I18N); eventsByGeneratorType.put("com.google.gwt.i18n.rebind.LocaleInfoGenerator", CompilerEventType.GENERATOR_I18N); eventsByGeneratorType.put("com.google.gwt.i18n.rebind.CurrencyListGenerator", CompilerEventType.GENERATOR_I18N); eventsByGeneratorType.put("com.google.gwt.i18n.rebind.CustomDateTimeFormatGenerator", CompilerEventType.GENERATOR_I18N); eventsByGeneratorType.put("com.google.gwt.user.rebind.rpc.ServiceInterfaceProxyGenerator", CompilerEventType.GENERATOR_RPC); eventsByGeneratorType.put("com.google.gwt.rpc.rebind.RpcServiceGenerator", CompilerEventType.GENERATOR_RPC); // deRPC eventsByGeneratorType.put("com.google.gwt.uibinder.rebind.UiBinderGenerator", CompilerEventType.GENERATOR_UIBINDER); eventsByGeneratorType.put("com.google.gwt.inject.rebind.GinjectorGenerator", CompilerEventType.GENERATOR_GIN); } private final ArtifactSet allGeneratedArtifacts; private final Map<String, GeneratedUnit> committedGeneratedCups = new HashMap<String, GeneratedUnit>(); private CompilationState compilationState; private Class<? extends Generator> currentGenerator; private final File genDir; private final Map<Class<? extends Generator>, Generator> generators = new IdentityHashMap<Class<? extends Generator>, Generator>(); private final ModuleDef module; private ArtifactSet newlyGeneratedArtifacts = new ArtifactSet(); private final Set<String> newlyGeneratedTypeNames = new HashSet<String>(); private final Map<String, PendingResource> pendingResources = new HashMap<String, PendingResource>(); private transient PropertyOracle propOracle; private RebindRuleResolver rebindRuleResolver; private final Map<PrintWriter, Generated> uncommittedGeneratedCupsByPrintWriter = new IdentityHashMap<PrintWriter, Generated>(); private CachedGeneratorResultImpl cachedRebindResult = null; private boolean generatorResultCachingEnabled = false; private List<String> cachedTypeNamesToReuse = null; private boolean isProdMode; /** * Normally, the compiler host would be aware of the same types that are * available in the supplied type oracle although it isn't strictly required. */ public StandardGeneratorContext(CompilationState compilationState, ModuleDef module, File genDir, ArtifactSet allGeneratedArtifacts, boolean isProdMode) { this.compilationState = compilationState; this.module = module; this.genDir = genDir; this.allGeneratedArtifacts = allGeneratedArtifacts; this.isProdMode = isProdMode; } /** * Adds a generated unit to the context if not already present, but will not * overwrite an existing unit. */ public void addGeneratedUnit(GeneratedUnit gu) { if (!committedGeneratedCups.containsKey(gu.getTypeName())) { committedGeneratedCups.put(gu.getTypeName(), gu); } } /** * Adds generated units to the context, but will not overwrite any existing * units that might already be present. */ public void addGeneratedUnits(Collection<GeneratedUnit> generatedUnits) { for (GeneratedUnit gu : generatedUnits) { addGeneratedUnit(gu); } } /** * Adds all available cached generated units to the context. Existing units * for a given type will not be overwritten. */ public void addGeneratedUnitsFromCache() { if (cachedRebindResult != null && cachedRebindResult.getGeneratedUnits() != null) { addGeneratedUnits(cachedRebindResult.getGeneratedUnits()); } } /** * Adds cached generated units to the context that have been marked for reuse. * Existing units for a given type will not be overwritten. */ public void addGeneratedUnitsMarkedForReuseFromCache() { if (cachedTypeNamesToReuse != null && cachedRebindResult != null) { for (String typeName : cachedTypeNamesToReuse) { GeneratedUnit gu = cachedRebindResult.getGeneratedUnit(typeName); if (gu != null) { addGeneratedUnit(gu); } } } } /** * Checks whether a rebind rule is available for a given sourceTypeName. */ @Override public boolean checkRebindRuleAvailable(String sourceTypeName) { if (rebindRuleResolver != null) { return rebindRuleResolver.checkRebindRuleResolvable(sourceTypeName); } else { return false; } } /** * Frees memory used up by compilation state. */ public void clear() { compilationState = null; generators.clear(); } /** * Commits a pending generated type. */ @Override public final void commit(TreeLogger logger, PrintWriter pw) { Generated gcup = uncommittedGeneratedCupsByPrintWriter.get(pw); if (gcup != null) { gcup.commit(logger); uncommittedGeneratedCupsByPrintWriter.remove(pw); committedGeneratedCups.put(gcup.getTypeName(), gcup); } else { logger.log(TreeLogger.WARN, "Generator attempted to commit an unknown PrintWriter", null); } } /** * Adds an Artifact to the context's ArtifactSets. This will replace a * pre-existing entry in allGeneratedArtifacts, but will not overwrite an * entry in the newlyGeneratedArtifacts (since it is assumed by convention * that only new entries will ever be inserted here for a given generator * run). */ @Override public void commitArtifact(TreeLogger logger, Artifact<?> artifact) { allGeneratedArtifacts.replace(artifact); newlyGeneratedArtifacts.add(artifact); } /** * Commits all available cached Artifacts to the context. */ public void commitArtifactsFromCache(TreeLogger logger) { if (cachedRebindResult != null && cachedRebindResult.getArtifacts() != null) { for (Artifact<?> art : cachedRebindResult.getArtifacts()) { commitArtifact(logger, art); } } } @Override public GeneratedResource commitResource(TreeLogger logger, OutputStream os) throws UnableToCompleteException { PendingResource pendingResource = null; String partialPath = null; if (os instanceof PendingResource) { pendingResource = (PendingResource) os; partialPath = pendingResource.getPartialPath(); // Make sure it's ours by looking it up in the map. if (pendingResource != pendingResources.get(partialPath)) { pendingResource = null; } } if (pendingResource == null) { logger.log(TreeLogger.WARN, "Generator attempted to commit an unknown OutputStream", null); throw new UnableToCompleteException(); } // Add the GeneratedResource to the ArtifactSet GeneratedResource toReturn = new StandardGeneratedResource(currentGenerator, partialPath, pendingResource.takeBytes()); commitArtifact(logger, toReturn); pendingResources.remove(pendingResource.getPartialPath()); return toReturn; } /** * Call this whenever generators are known to not be running to clear out * uncommitted compilation units and to force committed compilation units to * be parsed and added to the type oracle. * * @return any newly generated artifacts since the last call */ public final ArtifactSet finish(TreeLogger logger) { abortUncommittedResources(logger); try { TreeLogger branch; if (!committedGeneratedCups.isEmpty()) { // Assimilate the new types into the type oracle. // String msg = "Assimilating generated source"; branch = logger.branch(TreeLogger.DEBUG, msg, null); TreeLogger subBranch = null; if (branch.isLoggable(TreeLogger.DEBUG)) { subBranch = branch.branch(TreeLogger.DEBUG, "Generated source files...", null); } for (GeneratedUnit gcup : committedGeneratedCups.values()) { String qualifiedTypeName = gcup.getTypeName(); if (subBranch != null) { subBranch.log(TreeLogger.DEBUG, qualifiedTypeName, null); } } compilationState.addGeneratedCompilationUnits(logger, committedGeneratedCups.values()); } return newlyGeneratedArtifacts; } finally { // Remind the user if there uncommitted cups. if (!uncommittedGeneratedCupsByPrintWriter.isEmpty()) { String msg = "For the following type(s), generated source was never committed (did you forget to call commit()?)"; logger = logger.branch(TreeLogger.WARN, msg, null); for (Generated unit : uncommittedGeneratedCupsByPrintWriter.values()) { logger.log(TreeLogger.WARN, unit.getTypeName(), null); } } uncommittedGeneratedCupsByPrintWriter.clear(); committedGeneratedCups.clear(); newlyGeneratedTypeNames.clear(); newlyGeneratedArtifacts = new ArtifactSet(); cachedTypeNamesToReuse = null; } } public Set<String> getActiveLinkerNames() { return module.getActiveLinkerNames(); } /** * Gets newly committed artifacts. */ public ArtifactSet getArtifacts() { return new ArtifactSet(newlyGeneratedArtifacts); } /** * Gets the previously cached rebind result for the current generator. */ @Override public CachedGeneratorResult getCachedGeneratorResult() { return cachedRebindResult; } public GeneratorContext getCanonicalContext() { return this; } public CompilationState getCompilationState() { return compilationState; } /** * Gets all committed Java units. */ public Map<String, GeneratedUnit> getGeneratedUnitMap() { return committedGeneratedCups; } @Override public final PropertyOracle getPropertyOracle() { return propOracle; } @Override public ResourceOracle getResourcesOracle() { return module.getResourcesOracle(); } @Override public final TypeOracle getTypeOracle() { return compilationState.getTypeOracle(); } @Override public boolean isGeneratorResultCachingEnabled() { return generatorResultCachingEnabled; } @Override public boolean isProdMode() { return isProdMode; } /** * This method is maintained for backwards compatibility. * {@link #runGeneratorIncrementally} should be used instead. */ public String runGenerator(TreeLogger logger, Class<? extends Generator> generatorClass, String typeName) throws UnableToCompleteException { RebindResult result = runGeneratorIncrementally(logger, generatorClass, typeName); return result.getResultTypeName(); } /** * Runs a generator incrementally, with support for managing the returned * {@link RebindResult} object, which can contain cached results. This is a * replacement for the {@link #runGenerator} method. * <p> * If the passed in generatorClass is an instance of * {@link IncrementalGenerator}, it's * {@link IncrementalGenerator#generateIncrementally} method will be called. * <p> * Otherwise, for backwards compatibility, the generatorClass will be wrapped * in a {@link IncrementalGenerator} instance, and it's * {@link Generator#generate} method will be called. * * @param logger * @param generatorClass * @param typeName * @return a RebindResult * @throws UnableToCompleteException */ public RebindResult runGeneratorIncrementally(TreeLogger logger, Class<? extends Generator> generatorClass, String typeName) throws UnableToCompleteException { String msg = "Invoking generator " + generatorClass.getName(); logger = logger.branch(TreeLogger.DEBUG, msg, null); Generator generator = generators.get(generatorClass); if (generator == null) { try { generator = generatorClass.newInstance(); generators.put(generatorClass, generator); } catch (Throwable e) { logger.log(TreeLogger.ERROR, "Unexpected error trying to instantiate Generator '" + generatorClass.getName() + "'", e); throw new UnableToCompleteException(); } } setCurrentGenerator(generatorClass); // Avoid call to System.currentTimeMillis() if not logging DEBUG level boolean loggable = logger.isLoggable(TreeLogger.DEBUG); long before = loggable ? System.currentTimeMillis() : 0L; String generatorClassName = generator.getClass().getName(); CompilerEventType type = eventsByGeneratorType.get(generatorClassName); if (type == null) { type = CompilerEventType.GENERATOR_OTHER; } Event generatorEvent = SpeedTracerLogger.start(type, "class", generatorClassName, "type", typeName); try { RebindResult result; if (generator instanceof IncrementalGenerator) { IncrementalGenerator incGenerator = (IncrementalGenerator) generator; // check version id for any previously cached rebind result if (cachedRebindResult != null) { Long cachedVersionId = (Long) cachedRebindResult.getClientData(GENERATOR_VERSION_ID_KEY); if (cachedVersionId != null && cachedVersionId != incGenerator.getVersionId()) { // remove from context if (logger.isLoggable(TreeLogger.TRACE)) { logger.log(TreeLogger.TRACE, "Got version mismatch with cached generator result for " + typeName + ", invalidating cached result"); } cachedRebindResult = null; } } // run the generator result = incGenerator.generateIncrementally(logger, this, typeName); // add version id to the returned result result.putClientData(GENERATOR_VERSION_ID_KEY, incGenerator.getVersionId()); } else { // run a non-incremental generator result = IncrementalGenerator.generateNonIncrementally(logger, generator, this, typeName); } if (loggable) { long after = System.currentTimeMillis(); msg = "Generator returned type '" + result.getResultTypeName() + "; mode " + result.getRebindMode() + "; in " + (after - before) + " ms"; logger.log(TreeLogger.DEBUG, msg, null); } return result; } catch (AssertionError e) { // Catch and log the assertion as a convenience to the developer logger.log(TreeLogger.ERROR, "Generator '" + generatorClass.getName() + "' failed an assertion while rebinding '" + typeName + "'", e); throw new UnableToCompleteException(); } catch (RuntimeException e) { logger.log(TreeLogger.ERROR, "Generator '" + generatorClass.getName() + "' threw an exception while rebinding '" + typeName + "'", e); throw new UnableToCompleteException(); } finally { generatorEvent.end(); } } /** * Set previously cached rebind result for currently active generator. */ public void setCachedGeneratorResult(CachedGeneratorResult cachedRebindResult) { this.cachedRebindResult = (CachedGeneratorResultImpl) cachedRebindResult; } public void setCurrentGenerator(Class<? extends Generator> currentGenerator) { this.currentGenerator = currentGenerator; } public void setGeneratorResultCachingEnabled(boolean enabled) { this.generatorResultCachingEnabled = enabled; } /** * Sets the current transient property oracle to answer current property * questions. */ public void setPropertyOracle(PropertyOracle propOracle) { this.propOracle = propOracle; } public void setRebindRuleResolver(RebindRuleResolver resolver) { this.rebindRuleResolver = resolver; } @Override public final PrintWriter tryCreate(TreeLogger logger, String packageName, String simpleTypeName) { String typeName; if (packageName.length() == 0) { typeName = simpleTypeName; } else { typeName = packageName + '.' + simpleTypeName; } // Is type already known to the host? JClassType existingType = getTypeOracle().findType(packageName, simpleTypeName); if (existingType != null) { if (logger.isLoggable(TreeLogger.DEBUG)) { logger.log(TreeLogger.DEBUG, "Type '" + typeName + "' already exists and will not be re-created ", null); } return null; } // Type recently generated? if (newlyGeneratedTypeNames.contains(typeName)) { return null; } // The type isn't there, so we can let the caller create it. Remember that // it is pending so another attempt to create the same type will fail. Generated gcup; StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw, true) { /** * Overridden to force unix-style line endings for consistent behavior * across platforms. */ @Override public void println() { super.print('\n'); super.flush(); } }; if (this.genDir == null) { gcup = new GeneratedUnitImpl(sw, typeName); } else { File dir = new File(genDir, packageName.replace('.', File.separatorChar)); // No need to check mkdirs result because an IOException will occur anyway dir.mkdirs(); File srcFile = new File(dir, simpleTypeName + ".java"); if (srcFile.exists()) { srcFile.delete(); } gcup = new GeneratedUnitWithFile(srcFile, sw, typeName); } uncommittedGeneratedCupsByPrintWriter.put(pw, gcup); newlyGeneratedTypeNames.add(typeName); return pw; } @Override public OutputStream tryCreateResource(TreeLogger logger, String partialPath) throws UnableToCompleteException { logger = logger.branch(TreeLogger.DEBUG, "Preparing pending output resource '" + partialPath + "'", null); // Disallow null or empty names. if (partialPath == null || partialPath.trim().equals("")) { logger.log(TreeLogger.ERROR, "The resource name must be a non-empty string", null); throw new UnableToCompleteException(); } // Disallow absolute paths. if (new File(partialPath).isAbsolute()) { logger .log( TreeLogger.ERROR, "Resource paths are intended to be relative to the compiled output directory and cannot be absolute", null); throw new UnableToCompleteException(); } // Disallow backslashes (to promote consistency in calling code). if (partialPath.indexOf('\\') >= 0) { logger.log(TreeLogger.ERROR, "Resource paths must contain forward slashes (not backslashes) to denote subdirectories", null); throw new UnableToCompleteException(); } // Check for public path collision. if (module.findPublicFile(partialPath) != null) { logger.log(TreeLogger.WARN, "Cannot create resource '" + partialPath + "' because it already exists on the public path", null); return null; } // See if the file is already committed. SortedSet<GeneratedResource> resources = allGeneratedArtifacts.find(GeneratedResource.class); for (GeneratedResource resource : resources) { if (partialPath.equals(resource.getPartialPath())) { return null; } } // See if the file is pending. if (pendingResources.containsKey(partialPath)) { // It is already pending. logger.log(TreeLogger.WARN, "The file '" + partialPath + "' is already a pending resource", null); return null; } PendingResource pendingResource = new PendingResource(partialPath); pendingResources.put(partialPath, pendingResource); return pendingResource; } /** * Adds a type name to the list of types to be reused from cache, if * available. * * @param typeName The fully qualified name of a type. * * @return true, if the type is available in the cache and was successfully * added to the list for reuse, false otherwise. */ @Override public boolean tryReuseTypeFromCache(String typeName) { if (!isGeneratorResultCachingEnabled() || cachedRebindResult == null || !cachedRebindResult.isTypeCached(typeName)) { return false; } if (cachedTypeNamesToReuse == null) { cachedTypeNamesToReuse = new ArrayList<String>(); } cachedTypeNamesToReuse.add(typeName); return true; } private void abortUncommittedResources(TreeLogger logger) { if (pendingResources.isEmpty()) { // Nothing to do. return; } // Warn the user about uncommitted resources. logger = logger .branch( TreeLogger.WARN, "The following resources will not be created because they were never committed (did you forget to call commit()?)", null); for (Entry<String, PendingResource> entry : pendingResources.entrySet()) { logger.log(TreeLogger.WARN, entry.getKey()); entry.getValue().abort(); } pendingResources.clear(); } }