com.google.gwt.dev.javac.StandardGeneratorContext.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.dev.javac.StandardGeneratorContext.java

Source

/*
 * 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.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.GeneratorContextExt;
import com.google.gwt.core.ext.GeneratorExt;
import com.google.gwt.core.ext.GeneratorExtWrapper;
import com.google.gwt.core.ext.PropertyOracle;
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.javac.rebind.CachedRebindResult;
import com.google.gwt.dev.javac.rebind.RebindResult;
import com.google.gwt.dev.javac.rebind.RebindRuleResolver;
import com.google.gwt.dev.javac.rebind.RebindStatus;
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 GeneratorContextExt {

    /**
     * Extras added to {@link GeneratedUnit}.
     */
    private static interface Generated extends GeneratedUnit {
        void abort();

        void commit(TreeLogger logger);

        /**
         * Returns the strong hash of the source.
         */
        String getStrongHash();

        String getTypeName();
    }

    /**
     * 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;
        }

        public void abort() {
            sw = null;
        }

        /**
         * Finalizes the source and adds this generated unit to the host.
         */
        public void commit(TreeLogger logger) {
            String source = sw.toString();
            strongHash = Util.computeStrongName(Util.getBytes(source));
            sourceToken = diskCache.writeString(source);
            sw = null;
            creationTime = System.currentTimeMillis();
        }

        public long creationTime() {
            return creationTime;
        }

        public String getSource() {
            if (sw != null) {
                throw new IllegalStateException("source not committed");
            }
            return diskCache.readString(sourceToken);
        }

        public long getSourceToken() {
            if (sw != null) {
                throw new IllegalStateException("source not committed");
            }
            return sourceToken;
        }

        public String getStrongHash() {
            return strongHash;
        }

        public String getTypeName() {
            return typeName;
        }

        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() ? 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 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 CachedRebindResult 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.
     */
    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.
     */
    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).
     */
    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);
            }
        }
    }

    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();
            cachedRebindResult = null;
            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.
     */
    public CachedRebindResult 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;
    }

    public final PropertyOracle getPropertyOracle() {
        return propOracle;
    }

    public ResourceOracle getResourcesOracle() {
        return module.getResourcesOracle();
    }

    public final TypeOracle getTypeOracle() {
        return compilationState.getTypeOracle();
    }

    public boolean isGeneratorResultCachingEnabled() {
        return generatorResultCachingEnabled;
    }

    public boolean isProdMode() {
        return isProdMode;
    }

    /**
     * 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.
     */
    public boolean reuseTypeFromCacheIfAvailable(String typeName) {
        if (!isGeneratorResultCachingEnabled() || cachedRebindResult == null
                || !cachedRebindResult.isTypeCached(typeName)) {
            return false;
        }

        if (cachedTypeNamesToReuse == null) {
            cachedTypeNamesToReuse = new ArrayList<String>();
        }
        cachedTypeNamesToReuse.add(typeName);
        return true;
    }

    /**
     * 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.getReturnedTypeName();
    }

    /**
     * Runs a generator incrementally, with support for managing the returned
     * {@link RebindResult} object, which can contain status and cached results.
     * This is a replacement for the {@link #runGenerator} method.
     * <p>
     * If the passed in generatorClass is an instance of {@link GeneratorExt},
     * it's {@link GeneratorExt#generateIncrementally} method will be called.
     * <p>
     * Otherwise, for backwards compatibility, the generatorClass will be wrapped
     * in a {@link GeneratorExt} 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 {
            GeneratorExt generatorExt;
            if (generator instanceof GeneratorExt) {
                generatorExt = (GeneratorExt) generator;
            } else {
                generatorExt = GeneratorExtWrapper.newInstance(generator);
            }

            RebindResult result;
            result = generatorExt.generateIncrementally(logger, this, typeName);

            if (loggable) {
                if (result.getResultStatus() == RebindStatus.USE_EXISTING) {
                    msg = "Generator did not return a new class, type will be used as is";
                } else {
                    msg = "Generator returned class '" + result.getReturnedTypeName() + "'";
                }
                long after = System.currentTimeMillis();
                msg += "; 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(CachedRebindResult cachedRebindResult) {
        this.cachedRebindResult = 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;
    }

    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;
    }

    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;
    }

    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();
    }
}