All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.takari.maven.plugins.compile.jdt.CompilerJdt Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
package io.takari.maven.plugins.compile.jdt;

import io.takari.incrementalbuild.BuildContext;
import io.takari.incrementalbuild.BuildContext.InputMetadata;
import io.takari.incrementalbuild.spi.DefaultBuildContext;
import io.takari.incrementalbuild.spi.DefaultInput;
import io.takari.incrementalbuild.spi.DefaultInputMetadata;
import io.takari.incrementalbuild.spi.DefaultOutput;
import io.takari.incrementalbuild.spi.DefaultOutputMetadata;
import io.takari.maven.plugins.compile.AbstractCompiler;
import io.takari.maven.plugins.compile.jdt.classpath.Classpath;
import io.takari.maven.plugins.compile.jdt.classpath.ClasspathEntry;
import io.takari.maven.plugins.compile.jdt.classpath.JavaInstallation;
import io.takari.maven.plugins.compile.jdt.classpath.MutableClasspathEntry;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
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 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.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.problem.DefaultProblem;
import org.eclipse.jdt.internal.compiler.util.SuffixConstants;
import org.eclipse.jdt.internal.core.builder.ProblemFactory;

import com.google.common.base.Stopwatch;

/**
 * @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 Classpath dependencypath;

  /**
   * Set of ICompilationUnit to be compiled.
   */
  private final Set compileQueue = new LinkedHashSet();

  /**
   * Set of File that have already been added to the compile queue.
   */
  private final Set processedSources = new LinkedHashSet();

  private final Set rootNames = new LinkedHashSet();

  private final Set qualifiedNames = new LinkedHashSet();

  private final Set simpleNames = new LinkedHashSet();

  private final ClassfileDigester digester = new ClassfileDigester();

  private final ClasspathEntryCache classpathCache;

  private final ClasspathDigester classpathDigester;

  @Inject
  public CompilerJdt(DefaultBuildContext context, ClasspathEntryCache classpathCache,
      ClasspathDigester classpathDigester) {
    super(context);
    this.classpathCache = classpathCache;
    this.classpathDigester = classpathDigester;
  }

  @Override
  public void compile() throws MojoExecutionException, IOException {
    IErrorHandlingPolicy errorHandlingPolicy = DefaultErrorHandlingPolicies.exitAfterAllProblems();
    Map args = new HashMap();
    // 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
    CompilerOptions compilerOptions = new CompilerOptions(args);
    compilerOptions.performMethodsFullRecovery = false;
    compilerOptions.performStatementsRecovery = false;
    compilerOptions.verbose = isVerbose();
    compilerOptions.suppressWarnings = true;
    IProblemFactory problemFactory = ProblemFactory.getProblemFactory(Locale.getDefault());
    Classpath namingEnvironment = createClasspath();
    Compiler compiler =
        new Compiler(namingEnvironment, errorHandlingPolicy, compilerOptions, this, problemFactory);
    compiler.options.produceReferenceInfo = true;

    // TODO optimize full build.
    // there is no need to track processed inputs during full build,
    // which saves memory and GC cycles
    // also, if number of sources in the previous build is known, it may be more efficient to
    // rebuild everything after certain % of sources is modified

    // keep calling the compiler while there are sources in the queue
    while (!compileQueue.isEmpty()) {
      ICompilationUnit[] sourceFiles =
          compileQueue.toArray(new ICompilationUnit[compileQueue.size()]);
      compileQueue.clear();
      compiler.compile(sourceFiles);
      namingEnvironment.reset();
      enqueueAffectedSources();
    }
  }

  @Override
  public boolean setSources(List sources) throws IOException {
    enqueue(context.registerAndProcessInputs(sources));

    // remove stale outputs and rebuild all sources that reference them
    for (DefaultOutputMetadata output : context.deleteStaleOutputs(false)) {
      addDependentsOf(getJavaType(output));
    }

    enqueueAffectedSources();

    return !compileQueue.isEmpty();
  }

  private String getJavaType(DefaultOutputMetadata output) {
    String outputDirectory = getOutputDirectory().getAbsolutePath();
    String path = output.getResource().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, '.');
  }

  private void enqueue(Iterable> sources) {
    for (DefaultInput source : sources) {
      enqueue(source.getResource());
    }
  }

  private void enqueueAffectedSources() throws IOException {
    for (InputMetadata input : context.getRegisteredInputs(File.class)) {
      final File resource = input.getResource();
      if (!processedSources.contains(resource) && resource.canRead()) {
        ReferenceCollection references = input.getValue(ATTR_REFERENCES, ReferenceCollection.class);
        if (references != null && references.includes(qualifiedNames, simpleNames, rootNames)) {
          enqueue(resource);
        }
      }
    }

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

  private void enqueue(File sourceFile) {
    if (processedSources.add(sourceFile)) {
      compileQueue.add(newSourceFile(sourceFile));
    }
  }

  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() throws IOException {
    final List entries = new ArrayList();
    final List mutableentries = new ArrayList();

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

    CompileQueueClasspathEntry queueEntry = new CompileQueueClasspathEntry(compileQueue);
    entries.add(queueEntry);
    mutableentries.add(queueEntry);

    OutputDirectoryClasspathEntry output = new OutputDirectoryClasspathEntry(getOutputDirectory());
    entries.add(output);
    mutableentries.add(output);

    entries.addAll(dependencypath.getEntries());

    return new Classpath(entries, mutableentries);
  }

  @Override
  public boolean setClasspath(List dependencies) throws IOException {
    final List dependencypath = new ArrayList();
    final List files = new ArrayList();

    for (File dependency : dependencies) {
      ClasspathEntry entry = classpathCache.get(dependency);
      if (entry != null) {
        dependencypath.add(entry);
        files.add(dependency);
      }
    }

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

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

    HashMap digest = classpathDigester.digestDependencies(files);

    DefaultInputMetadata metadata = context.registerInput(getPom());
    @SuppressWarnings("unchecked")
    Map oldDigest =
        (Map) metadata.getValue(ATTR_CLASSPATH_DIGEST, Serializable.class);

    boolean changed = false;

    if (oldDigest != null) {
      Set changedPackages = new HashSet();

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

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

      for (String changedPackage : changedPackages) {
        addDependentsOf(changedPackage);
      }
    } else {
      changed = true;
    }

    if (changed) {
      metadata.process().setValue(ATTR_CLASSPATH_DIGEST, digest);
    }

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

    enqueueAffectedSources();

    return !compileQueue.isEmpty();
  }

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

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

    processedSources.add(sourceFile);

    // JDT may decide to compile more sources than it was asked to in some cases
    // always register and process sources with build context
    DefaultInput input = context.registerInput(sourceFile).process();

    // track type references
    input.setValue(ATTR_REFERENCES, new ReferenceCollection(result.rootReferences,
        result.qualifiedReferences, result.simpleNameReferences));

    if (result.hasProblems()) {
      for (CategorizedProblem problem : result.getProblems()) {
        input.addMessage(problem.getSourceLineNumber(), ((DefaultProblem) problem).column, problem
            .getMessage(), problem.isError()
            ? BuildContext.Severity.ERROR
            : BuildContext.Severity.WARNING, null);
      }
    }

    if (!result.hasErrors()) {
      for (ClassFile classFile : result.getClassFiles()) {
        try {
          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 void writeClassFile(DefaultInput input, String relativeStringName,
      ClassFile classFile) throws IOException {
    final byte[] bytes = classFile.getBytes();
    final File outputFile = new File(getOutputDirectory(), relativeStringName);
    final DefaultOutput output = input.associateOutput(outputFile);

    boolean significantChange = digestClassFile(output, bytes);

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

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

  private boolean digestClassFile(DefaultOutput 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[]) output.setValue(ATTR_CLASS_DIGEST, hash);
        significantChange = oldHash == null || !Arrays.equals(hash, oldHash);
      }
    } catch (ClassFormatException e) {
      // ignore this class
    }
    return significantChange;
  }

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

  @Override
  public void skipCompilation() {
    // unlike javac, jdt compiler tracks input-output association
    // this allows BuildContext to automatically carry-over output metadata
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy