io.takari.maven.plugins.compile.jdt.CompilerJdt.java Source code

Java tutorial

Introduction

Here is the source code for io.takari.maven.plugins.compile.jdt.CompilerJdt.java

Source

/**
 * Copyright (c) 2014 Takari, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package io.takari.maven.plugins.compile.jdt;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;
import javax.inject.Named;
import javax.tools.StandardLocation;

import org.apache.maven.plugin.MojoExecutionException;
import org.eclipse.jdt.core.compiler.CategorizedProblem;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ClassFile;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.Compiler;
import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies;
import org.eclipse.jdt.internal.compiler.ICompilerRequestor;
import org.eclipse.jdt.internal.compiler.IErrorHandlingPolicy;
import org.eclipse.jdt.internal.compiler.IProblemFactory;
import org.eclipse.jdt.internal.compiler.apt.util.EclipseFileManager;
import org.eclipse.jdt.internal.compiler.batch.CompilationUnit;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.eclipse.jdt.internal.compiler.env.ICompilationUnit;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.impl.IrritantSet;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblem;
import org.eclipse.jdt.internal.compiler.util.SuffixConstants;
import org.eclipse.jdt.internal.core.builder.ProblemFactory;

import com.google.common.base.Stopwatch;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;

import io.takari.incrementalbuild.MessageSeverity;
import io.takari.incrementalbuild.Output;
import io.takari.incrementalbuild.Resource;
import io.takari.incrementalbuild.ResourceMetadata;
import io.takari.incrementalbuild.ResourceStatus;
import io.takari.maven.plugins.compile.AbstractCompileMojo.AccessRulesViolation;
import io.takari.maven.plugins.compile.AbstractCompileMojo.Debug;
import io.takari.maven.plugins.compile.AbstractCompileMojo.Proc;
import io.takari.maven.plugins.compile.AbstractCompiler;
import io.takari.maven.plugins.compile.CompilerBuildContext;
import io.takari.maven.plugins.compile.ProjectClasspathDigester;
import io.takari.maven.plugins.compile.jdt.classpath.Classpath;
import io.takari.maven.plugins.compile.jdt.classpath.ClasspathDirectory;
import io.takari.maven.plugins.compile.jdt.classpath.ClasspathEntry;
import io.takari.maven.plugins.compile.jdt.classpath.DependencyClasspathEntry;
import io.takari.maven.plugins.compile.jdt.classpath.JavaInstallation;
import io.takari.maven.plugins.compile.jdt.classpath.MutableClasspathEntry;

/**
 * @TODO test classpath order changes triggers rebuild of affected sources (same type name, different classes)
 * @TODO figure out why JDT needs to worry about duplicate types (maybe related to classpath order above)
 * @TODO test affected sources are recompiled after source gets compile error
 * @TODO test nested types because addDependentsOf has some special handling
 */
@Named(CompilerJdt.ID)
public class CompilerJdt extends AbstractCompiler implements ICompilerRequestor {
    public static final String ID = "jdt";

    /**
     * Output .class file structure hash
     */
    private static final String ATTR_CLASS_DIGEST = "jdt.class.digest";

    /**
     * Classpath digest, map of accessible types to their .class structure hashes.
     */
    private static final String ATTR_CLASSPATH_DIGEST = "jdt.classpath.digest";

    /**
     * Java source {@link ReferenceCollection}
     */
    private static final String ATTR_REFERENCES = "jdt.references";

    private List<File> dependencies;

    private List<File> processorpath;

    private boolean lenientProcOnly;

    private Classpath dependencypath;

    private final Map<File, ResourceMetadata<File>> sources = new LinkedHashMap<>();

    /**
     * Set of ICompilationUnit to be compiled.
     */
    private final Map<File, ICompilationUnit> compileQueue = new LinkedHashMap<>();

    private final ClassfileDigester digester = new ClassfileDigester();

    private final ClasspathEntryCache classpathCache;

    private final ClasspathDigester classpathDigester;

    private final ProjectClasspathDigester processorpathDigester;

    private abstract class CompilationStrategy {
        protected final Multimap<File, File> sourceOutputs = HashMultimap.create();

        public abstract boolean setSources(List<ResourceMetadata<File>> sources) throws IOException;

        public abstract void enqueueAffectedSources(HashMap<String, byte[]> digest, Map<String, byte[]> oldDigest)
                throws IOException;

        public abstract void enqueueAllSources() throws IOException;

        public abstract void addDependentsOf(String typeOrPackage);

        protected abstract void addDependentsOf(File resource);

        public abstract int compile(Classpath namingEnvironment, Compiler compiler) throws IOException;

        public Classpath createClasspath() throws IOException {
            return CompilerJdt.this.createClasspath(sourceOutputs.values());
        }

        public abstract void addGeneratedSource(Output<File> generatedSource);

        protected boolean deleteOrphanedOutputs() throws IOException {
            boolean changed = false;
            for (ResourceMetadata<File> source : context.getRemovedSources()) {
                for (ResourceMetadata<File> output : context.getAssociatedOutputs(source)) {
                    File outputFile = output.getResource();
                    context.deleteOutput(outputFile);
                    addDependentsOf(outputFile);
                    changed = true;
                }
            }

            return changed;
        }

        protected boolean deleteStaleOutputs() throws IOException {
            boolean changed = false;
            for (File sourceFile : sourceOutputs.keySet()) {
                for (File associatedOutput : sourceOutputs.get(sourceFile)) {
                    if (!context.isProcessedOutput(associatedOutput)) {
                        context.deleteOutput(associatedOutput);
                        addDependentsOf(associatedOutput);
                        changed = true;
                    }
                }
            }
            return changed;
        }

        /**
         * Marks sources as "processed" in the build context. Masks old associated outputs from naming environments by adding them to {@link #sourceOutputs} map.
         */
        protected void processSources() {
            sourceOutputs.clear();
            for (File sourceFile : compileQueue.keySet()) {
                ResourceMetadata<File> source = sources.get(sourceFile);
                for (ResourceMetadata<File> output : context.getAssociatedOutputs(source)) {
                    sourceOutputs.put(sourceFile, output.getResource());
                }
                sources.put(source.getResource(), source.process());
            }
        }
    }

    private class IncrementalCompilationStrategy extends CompilationStrategy {

        /**
         * Set of File that have already been added to the compile queue during this incremental compile loop iteration.
         */
        private final Set<File> processedQueue = new HashSet<>();

        /**
         * Set of File that have already been added to the compile queue.
         */
        private final Multiset<File> processedSources = HashMultiset.create();

        private final Set<String> rootNames = new LinkedHashSet<>();

        private final Set<String> qualifiedNames = new LinkedHashSet<>();

        private final Set<String> simpleNames = new LinkedHashSet<>();

        @Override
        public int compile(Classpath namingEnvironment, Compiler compiler) throws IOException {
            // if (getProc() == Proc.only) {
            // // proc==only cannot be implemented incrementally
            // // changed sources may require types defined in sources that did not change,
            // // which are not available during incremental build without generated .class files
            // for (ResourceMetadata<File> source : sources.values()) {
            // if (!compileQueue.containsKey(source.getResource())) {
            // enqueue(source);
            // }
            // }
            // }

            // incremental compilation loop
            // keep calling the compiler while there are sources in the queue
            while (!compileQueue.isEmpty()) {
                processedQueue.clear();
                processedQueue.addAll(compileQueue.keySet());

                processSources();

                // invoke the compiler
                ICompilationUnit[] compilationUnits = compileQueue.values()
                        .toArray(new ICompilationUnit[compileQueue.size()]);
                compileQueue.clear();
                compiler.compile(compilationUnits);
                namingEnvironment.reset();

                if (compiler.annotationProcessorManager != null) {
                    ((AnnotationProcessorManager) compiler.annotationProcessorManager).hardReset();
                }

                deleteStaleOutputs(); // delete stale outputs and enqueue affected sources

                enqueueAffectedSources();
            }

            return processedSources.size();
        }

        @Override
        protected void addDependentsOf(File resource) {
            addDependentsOf(getJavaType(resource));
        }

        @Override
        public boolean setSources(List<ResourceMetadata<File>> sources) throws IOException {
            for (ResourceMetadata<File> source : sources) {
                CompilerJdt.this.sources.put(source.getResource(), source);
                if (source.getStatus() != ResourceStatus.UNMODIFIED) {
                    enqueue(source);
                }
            }

            boolean compilationRequired = false;

            // delete orphaned outputs and rebuild all sources that reference them
            compilationRequired = deleteOrphanedOutputs() || compilationRequired;

            enqueueAffectedSources();

            return compilationRequired || !compileQueue.isEmpty();
        }

        private void enqueueAffectedSources() throws IOException {
            for (ResourceMetadata<File> input : sources.values()) {
                final File resource = input.getResource();
                if (!processedQueue.contains(resource) && resource.canRead()) {
                    ReferenceCollection references = context.getAttribute(resource, ATTR_REFERENCES,
                            ReferenceCollection.class);
                    if (references != null && references.includes(qualifiedNames, simpleNames, rootNames)) {
                        enqueue(input);
                    }
                }
            }

            qualifiedNames.clear();
            simpleNames.clear();
            rootNames.clear();
        }

        private String getJavaType(File outputFile) {
            String outputDirectory = getOutputDirectory().getAbsolutePath();
            String path = outputFile.getAbsolutePath();
            if (!path.startsWith(outputDirectory) || !path.endsWith(".class")) {
                return null;
            }
            path = path.substring(outputDirectory.length(), path.length() - ".class".length());
            if (path.startsWith(File.separator)) {
                path = path.substring(1);
            }
            return path.replace(File.separatorChar, '.');
        }

        @Override
        public void enqueueAllSources() throws IOException {
            for (ResourceMetadata<File> input : sources.values()) {
                final File resource = input.getResource();
                if (!processedQueue.contains(resource) && resource.canRead()) {
                    enqueue(input);
                }
            }

            qualifiedNames.clear();
            simpleNames.clear();
            rootNames.clear();
        }

        @Override
        public void addDependentsOf(String typeOrPackage) {
            if (typeOrPackage != null) {
                // adopted from org.eclipse.jdt.internal.core.builder.IncrementalImageBuilder.addDependentsOf
                // TODO deal with package-info
                int idx = typeOrPackage.indexOf('.');
                if (idx > 0) {
                    rootNames.add(typeOrPackage.substring(0, idx));
                    idx = typeOrPackage.lastIndexOf('.');
                    qualifiedNames.add(typeOrPackage.substring(0, idx));
                    simpleNames.add(typeOrPackage.substring(idx + 1));
                } else {
                    rootNames.add(typeOrPackage);
                    simpleNames.add(typeOrPackage);
                }
            }
        }

        private void enqueue(ResourceMetadata<File> input) {
            File sourceFile = input.getResource();
            if (processedSources.count(sourceFile) > 10) {
                throw new IllegalStateException("Too many recompiles " + sourceFile);
            }
            processedSources.add(sourceFile);
            compileQueue.put(sourceFile, newSourceFile(sourceFile));
        }

        @Override
        public void enqueueAffectedSources(HashMap<String, byte[]> digest, Map<String, byte[]> oldDigest)
                throws IOException {
            if (oldDigest != null) {
                Set<String> changedPackages = new HashSet<String>();

                for (Map.Entry<String, byte[]> entry : digest.entrySet()) {
                    String type = entry.getKey();
                    byte[] hash = entry.getValue();
                    if (!Arrays.equals(hash, oldDigest.get(type))) {
                        addDependentsOf(type);
                    }
                    changedPackages.add(getPackage(type));
                }

                for (String oldType : oldDigest.keySet()) {
                    if (!digest.containsKey(oldType)) {
                        addDependentsOf(oldType);
                    }
                    changedPackages.remove(getPackage(oldType));
                }

                for (String changedPackage : changedPackages) {
                    addDependentsOf(changedPackage);
                }

                enqueueAffectedSources();
            }
        }

        private String getPackage(String type) {
            int idx = type.lastIndexOf('.');
            return idx > 0 ? type.substring(0, idx) : null;
        }

        @Override
        public void addGeneratedSource(Output<File> generatedSource) {
            sources.put(generatedSource.getResource(), generatedSource);
            processedQueue.add(generatedSource.getResource());
        }
    }

    private class FullCompilationStrategy extends CompilationStrategy {

        @Override
        public boolean setSources(List<ResourceMetadata<File>> sources) throws IOException {
            for (ResourceMetadata<File> source : sources) {
                File sourceFile = source.getResource();
                CompilerJdt.this.sources.put(sourceFile, source);
                compileQueue.put(sourceFile, newSourceFile(sourceFile));
            }

            deleteOrphanedOutputs();

            return true;
        }

        @Override
        public void enqueueAffectedSources(HashMap<String, byte[]> digest, Map<String, byte[]> oldDigest)
                throws IOException {
            // full strategy compiles all sources in one pass
        }

        @Override
        public void enqueueAllSources() throws IOException {
            // full strategy compiles all sources in one pass
        }

        @Override
        public void addDependentsOf(String string) {
            // full strategy compiles all sources in one pass
        }

        @Override
        protected void addDependentsOf(File resource) {
            // full strategy compiles all sources in one pass
        }

        @Override
        public int compile(Classpath namingEnvironment, Compiler compiler) throws IOException {
            if (!compileQueue.isEmpty()) {
                processSources();

                ICompilationUnit[] compilationUnits = compileQueue.values()
                        .toArray(new ICompilationUnit[compileQueue.size()]);
                compiler.compile(compilationUnits);

                deleteStaleOutputs();
            }

            return compileQueue.size();
        }

        @Override
        public void addGeneratedSource(Output<File> generatedSource) {
            // full strategy compiles all sources in one pass
        }
    }

    private CompilationStrategy strategy;

    @Inject
    public CompilerJdt(CompilerBuildContext context, ClasspathEntryCache classpathCache,
            ClasspathDigester classpathDigester, ProjectClasspathDigester processorpathDigester) {
        super(context);
        this.classpathCache = classpathCache;
        this.classpathDigester = classpathDigester;
        this.processorpathDigester = processorpathDigester;

        this.strategy = context.isEscalated() ? new FullCompilationStrategy()
                : new IncrementalCompilationStrategy();
    }

    @Override
    public int compile() throws MojoExecutionException, IOException {
        Map<String, String> args = new HashMap<String, String>();
        // XXX figure out how to reuse source/target check from jdt
        // org.eclipse.jdt.internal.compiler.batch.Main.validateOptions(boolean)
        args.put(CompilerOptions.OPTION_TargetPlatform, getTarget()); // support 5/6/7 aliases
        args.put(CompilerOptions.OPTION_Compliance, getTarget()); // support 5/6/7 aliases
        args.put(CompilerOptions.OPTION_Source, getSource()); // support 5/6/7 aliases
        args.put(CompilerOptions.OPTION_ReportForbiddenReference, CompilerOptions.ERROR);
        Set<Debug> debug = getDebug();
        if (debug == null || debug.contains(Debug.all)) {
            args.put(CompilerOptions.OPTION_LocalVariableAttribute, CompilerOptions.GENERATE);
            args.put(CompilerOptions.OPTION_LineNumberAttribute, CompilerOptions.GENERATE);
            args.put(CompilerOptions.OPTION_SourceFileAttribute, CompilerOptions.GENERATE);
        } else if (debug.contains(Debug.none)) {
            args.put(CompilerOptions.OPTION_LocalVariableAttribute, CompilerOptions.DO_NOT_GENERATE);
            args.put(CompilerOptions.OPTION_LineNumberAttribute, CompilerOptions.DO_NOT_GENERATE);
            args.put(CompilerOptions.OPTION_SourceFileAttribute, CompilerOptions.DO_NOT_GENERATE);
        } else {
            args.put(CompilerOptions.OPTION_LocalVariableAttribute, CompilerOptions.DO_NOT_GENERATE);
            args.put(CompilerOptions.OPTION_LineNumberAttribute, CompilerOptions.DO_NOT_GENERATE);
            args.put(CompilerOptions.OPTION_SourceFileAttribute, CompilerOptions.DO_NOT_GENERATE);
            for (Debug keyword : debug) {
                switch (keyword) {
                case lines:
                    args.put(CompilerOptions.OPTION_LineNumberAttribute, CompilerOptions.GENERATE);
                    break;
                case source:
                    args.put(CompilerOptions.OPTION_SourceFileAttribute, CompilerOptions.GENERATE);
                    break;
                case vars:
                    args.put(CompilerOptions.OPTION_LocalVariableAttribute, CompilerOptions.GENERATE);
                    break;
                default:
                    throw new IllegalArgumentException();
                }
            }
        }

        class _CompilerOptions extends CompilerOptions {
            public void setShowWarnings(boolean showWarnings) {
                if (showWarnings) {
                    warningThreshold = IrritantSet.COMPILER_DEFAULT_WARNINGS;
                } else {
                    warningThreshold = new IrritantSet(0);
                }
            }
        }
        _CompilerOptions compilerOptions = new _CompilerOptions();

        compilerOptions.set(args);
        compilerOptions.performMethodsFullRecovery = false;
        compilerOptions.performStatementsRecovery = false;
        compilerOptions.verbose = isVerbose();
        compilerOptions.suppressWarnings = true;
        compilerOptions.setShowWarnings(isShowWarnings());
        compilerOptions.docCommentSupport = true;

        if (isProcEscalate() && strategy instanceof IncrementalCompilationStrategy) {
            strategy.enqueueAllSources();
            strategy = new FullCompilationStrategy();
        }

        Classpath namingEnvironment = strategy.createClasspath();
        IErrorHandlingPolicy errorHandlingPolicy = DefaultErrorHandlingPolicies.exitAfterAllProblems();
        IProblemFactory problemFactory = ProblemFactory.getProblemFactory(Locale.getDefault());
        Compiler compiler = new Compiler(namingEnvironment, errorHandlingPolicy, compilerOptions, this,
                problemFactory);
        compiler.options.produceReferenceInfo = true;

        EclipseFileManager fileManager = null;
        try {
            if (!isProcNone()) {
                fileManager = new EclipseFileManager(null, getSourceEncoding());
                fileManager.setLocation(StandardLocation.ANNOTATION_PROCESSOR_PATH, dependencies);
                fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Collections.singleton(getOutputDirectory()));
                fileManager.setLocation(StandardLocation.SOURCE_OUTPUT,
                        Collections.singleton(getGeneratedSourcesDirectory()));

                ProcessingEnvImpl processingEnv = new ProcessingEnvImpl(context, fileManager,
                        getAnnotationProcessorOptions(), compiler, this);

                compiler.annotationProcessorManager = new AnnotationProcessorManager(processingEnv, fileManager,
                        getAnnotationProcessors());
                compiler.options.storeAnnotations = true;
            }

            return strategy.compile(namingEnvironment, compiler);
        } finally {
            if (fileManager != null) {
                fileManager.flush();
                fileManager.close();
            }
        }
    }

    @Override
    public boolean setSources(List<ResourceMetadata<File>> sources) throws IOException {
        return strategy.setSources(sources);
    }

    private CompilationUnit newSourceFile(File source) {
        final String fileName = source.getAbsolutePath();
        final String encoding = getSourceEncoding() != null ? getSourceEncoding().name() : null;
        return new CompilationUnit(null, fileName, encoding, getOutputDirectory().getAbsolutePath(), false);
    }

    private Classpath createClasspath(Collection<File> staleOutputs) throws IOException {
        final List<ClasspathEntry> entries = new ArrayList<ClasspathEntry>();
        final List<MutableClasspathEntry> mutableentries = new ArrayList<MutableClasspathEntry>();

        // XXX detect change!
        for (File file : JavaInstallation.getDefault().getClasspath()) {
            ClasspathEntry entry = classpathCache.get(file);
            if (entry != null) {
                entries.add(entry);
            }
        }

        if (!isProcOnly()) {
            OutputDirectoryClasspathEntry output = new OutputDirectoryClasspathEntry(getOutputDirectory(),
                    staleOutputs);
            entries.add(output);
            mutableentries.add(output);
        }

        entries.addAll(dependencypath.getEntries());

        return new Classpath(entries, mutableentries);
    }

    @Override
    public boolean setClasspath(List<File> dependencies, File mainClasses, Set<File> directDependencies)
            throws IOException {
        this.dependencies = dependencies;

        final List<ClasspathEntry> dependencypath = new ArrayList<ClasspathEntry>();
        final List<File> files = new ArrayList<File>();

        if (isProcOnly()) {
            DependencyClasspathEntry entry = ClasspathDirectory.create(getOutputDirectory());
            if (entry != null) {
                dependencypath.add(AccessRestrictionClasspathEntry.allowAll(entry));
                files.add(getOutputDirectory());
            }
        }

        if (mainClasses != null) {
            DependencyClasspathEntry entry = classpathCache.get(mainClasses);
            if (entry != null) {
                dependencypath.add(AccessRestrictionClasspathEntry.allowAll(entry));
                files.add(mainClasses);
            }
        }

        for (File dependency : dependencies) {
            DependencyClasspathEntry entry = classpathCache.get(dependency);
            if (entry != null) {
                if (getTransitiveDependencyReference() == AccessRulesViolation.error
                        && !directDependencies.contains(dependency)) {
                    dependencypath.add(AccessRestrictionClasspathEntry.forbidAll(entry));
                } else if (getPrivatePackageReference() == AccessRulesViolation.ignore) {
                    dependencypath.add(AccessRestrictionClasspathEntry.allowAll(entry));
                } else {
                    dependencypath.add(entry);
                }
                files.add(dependency);
            }
        }

        if (log.isDebugEnabled()) {
            StringBuilder msg = new StringBuilder();
            for (ClasspathEntry element : dependencypath) {
                msg.append("\n ").append(element.getEntryDescription());
            }
            log.debug("Compile classpath: {} entries{}", dependencies.size(), msg.toString());
        }

        this.dependencypath = new Classpath(dependencypath, null);

        Stopwatch stopwatch = Stopwatch.createStarted();
        long typecount = 0, packagecount = 0;

        HashMap<String, byte[]> digest = classpathDigester.digestDependencies(files);

        @SuppressWarnings("unchecked")
        Map<String, byte[]> oldDigest = (Map<String, byte[]>) context.setAttribute(ATTR_CLASSPATH_DIGEST, digest);

        log.debug("Digested {} types and {} packages in {} ms", typecount, packagecount,
                stopwatch.elapsed(TimeUnit.MILLISECONDS));

        strategy.enqueueAffectedSources(digest, oldDigest);

        return !compileQueue.isEmpty();
    }

    @Override
    public boolean setProcessorpath(List<File> processorpath) throws IOException {
        if (processorpath == null) {
            this.processorpath = dependencies;
        } else if (processorpath.isEmpty()) {
            this.processorpath = Collections.emptyList();
        } else {
            throw new IllegalArgumentException();
        }
        if (!isProcNone() && processorpathDigester.digestProcessorpath(this.processorpath)) {
            log.debug("Annotation processor path changed, recompiling all sources");
            strategy.enqueueAllSources();
        }
        return !compileQueue.isEmpty();
    }

    @Override
    public void setLenientProcOnly(boolean lenient) {
        this.lenientProcOnly = lenient;
    }

    @Override
    public void acceptResult(CompilationResult result) {
        if (result == null) {
            return; // ah?
        }
        final String sourceName = new String(result.getFileName());
        final File sourceFile = new File(sourceName);

        Resource<File> input = context.getProcessedSource(sourceFile);

        // track type references
        if (result.rootReferences != null && result.qualifiedReferences != null
                && result.simpleNameReferences != null) {
            context.setAttribute(input.getResource(), ATTR_REFERENCES, new ReferenceCollection(
                    result.rootReferences, result.qualifiedReferences, result.simpleNameReferences));
        }

        if (result.hasProblems() && (!isLenientProcOnly() || isShowWarnings())) {
            for (CategorizedProblem problem : result.getProblems()) {
                MessageSeverity severity = isLenientProcOnly() ? MessageSeverity.WARNING
                        : problem.isError() ? MessageSeverity.ERROR : MessageSeverity.WARNING;
                input.addMessage(problem.getSourceLineNumber(), ((DefaultProblem) problem).column,
                        problem.getMessage(), severity, null /* cause */);
            }
        }

        try {
            if (!result.hasErrors() && !isProcOnly()) {
                for (ClassFile classFile : result.getClassFiles()) {
                    char[] filename = classFile.fileName();
                    int length = filename.length;
                    char[] relativeName = new char[length + 6];
                    System.arraycopy(filename, 0, relativeName, 0, length);
                    System.arraycopy(SuffixConstants.SUFFIX_class, 0, relativeName, length, 6);
                    CharOperation.replace(relativeName, '/', File.separatorChar);
                    String relativeStringName = new String(relativeName);
                    writeClassFile(input, relativeStringName, classFile);
                }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        // XXX double check affected sources are recompiled when this source has errors
    }

    private boolean isProcOnly() {
        return getProc() == Proc.only || getProc() == Proc.onlyEX;
    }

    private boolean isProcNone() {
        return getProc() == Proc.none;
    }

    private boolean isProcEscalate() {
        return getProc() == Proc.procEX || getProc() == Proc.onlyEX;
    }

    private boolean isLenientProcOnly() {
        return lenientProcOnly && isProcOnly();
    }

    private void writeClassFile(Resource<File> input, String relativeStringName, ClassFile classFile)
            throws IOException {
        final byte[] bytes = classFile.getBytes();
        final File outputFile = new File(getOutputDirectory(), relativeStringName);
        final Output<File> output = context.associatedOutput(input, outputFile);

        boolean significantChange = digestClassFile(output, bytes);

        if (significantChange) {
            // find all sources that reference this type and put them into work queue
            strategy.addDependentsOf(CharOperation.toString(classFile.getCompoundName()));
        }

        final BufferedOutputStream os = new BufferedOutputStream(output.newOutputStream());
        try {
            os.write(bytes);
            os.flush();
        } finally {
            os.close();
        }
    }

    private boolean digestClassFile(Output<File> output, byte[] definition) {
        boolean significantChange = true;
        try {
            ClassFileReader reader = new ClassFileReader(definition,
                    output.getResource().getAbsolutePath().toCharArray());
            byte[] hash = digester.digest(reader);
            if (hash != null) {
                byte[] oldHash = (byte[]) context.setAttribute(output.getResource(), ATTR_CLASS_DIGEST, hash);
                significantChange = oldHash == null || !Arrays.equals(hash, oldHash);
            }
        } catch (ClassFormatException e) {
            // ignore this class
        }
        return significantChange;
    }

    public void addGeneratedSource(Output<File> generatedSource) {
        strategy.addGeneratedSource(generatedSource);
    }

}