/*
* Copyright 2015 Lukas Krejci
*
* 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 org.revapi.java.compilation;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import org.revapi.Archive;
import org.revapi.java.AnalysisConfiguration;
import org.revapi.java.Timing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Lukas Krejci
* @since 0.1
*/
public final class Compiler {
private static final Logger LOG = LoggerFactory.getLogger(Compiler.class);
private final JavaCompiler compiler;
private final Writer output;
private final Iterable<? extends Archive> classPath;
private final Iterable<? extends Archive> additionalClassPath;
private final ExecutorService executor;
public Compiler(ExecutorService executor, Writer reportingOutput, Iterable<? extends Archive> classPath,
Iterable<? extends Archive> additionalClassPath) {
compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new AssertionError("Could not obtain the system compiler. Is tools.jar on the classpath?");
}
this.executor = executor;
this.output = reportingOutput;
this.classPath = classPath;
this.additionalClassPath = additionalClassPath;
}
public CompilationValve compile(final ProbingEnvironment environment,
final AnalysisConfiguration.MissingClassReporting missingClassReporting,
final boolean ignoreMissingAnnotations,
final InclusionFilter inclusionFilter)
throws Exception {
File targetPath = Files.createTempDirectory("revapi-java").toAbsolutePath().toFile();
File sourceDir = new File(targetPath, "sources");
sourceDir.mkdir();
File lib = new File(targetPath, "lib");
lib.mkdir();
// make sure the classpath is in the same order as passed in
int classPathSize = size(classPath);
int nofArchives = classPathSize + size(additionalClassPath);
int prefixLength = (int) Math.log10(nofArchives) + 1;
IdentityHashMap<Archive, File> classPathFiles = copyArchives(classPath, lib, 0, prefixLength);
IdentityHashMap<Archive, File> additionClassPathFiles = copyArchives(additionalClassPath, lib, classPathSize, prefixLength);
List<String> options = Arrays.asList(
"-d", sourceDir.toString(),
"-cp", composeClassPath(lib)
);
List<JavaFileObject> sources = Arrays.<JavaFileObject>asList(
new MarkerAnnotationObject(),
new ArchiveProbeObject()
);
//the locale and charset are actually not important, because the only sources we're providing
//are not file-based. The rest of the stuff the compiler will be touching is already compiled
//and therefore not affected by the charset.
StandardJavaFileManager fileManager = compiler
.getStandardFileManager(null, Locale.getDefault(), Charset.forName("UTF-8"));
final JavaCompiler.CompilationTask task = compiler
.getTask(output, fileManager, null, options, Collections.singletonList(ArchiveProbeObject.CLASS_NAME),
sources);
ProbingAnnotationProcessor processor = new ProbingAnnotationProcessor(environment);
task.setProcessors(Collections.singletonList(processor));
Future<Boolean> future = processor.submitWithCompilationAwareness(executor, task, () -> {
if (Timing.LOG.isDebugEnabled()) {
Timing.LOG.debug("About to crawl " + environment.getApi());
}
try {
new ClasspathScanner(fileManager, environment, classPathFiles, additionClassPathFiles,
missingClassReporting, ignoreMissingAnnotations, inclusionFilter).initTree();
} catch (IOException e) {
throw new IllegalStateException("Failed to scan the classpath.", e);
}
if (Timing.LOG.isDebugEnabled()) {
Timing.LOG.debug("Crawl finished for " + environment.getApi());
}
});
return new CompilationValve(future, targetPath, environment, fileManager);
}
private String composeClassPath(File classPathDir) {
StringBuilder bld = new StringBuilder();
File[] jars = classPathDir.listFiles();
if (jars == null || jars.length == 0) {
return "";
}
List<File> sortedJars = new ArrayList<>(Arrays.asList(jars));
Collections.sort(sortedJars, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return o1.getName().compareTo(o2.getName());
}
});
Iterator<File> it = sortedJars.iterator();
bld.append(it.next().getAbsolutePath());
while (it.hasNext()) {
bld.append(File.pathSeparator).append(it.next().getAbsolutePath());
}
return bld.toString();
}
private IdentityHashMap<Archive, File>
copyArchives(Iterable<? extends Archive> archives, File parentDir, int startIdx, int prefixLength) {
IdentityHashMap<Archive, File> ret = new IdentityHashMap<>();
if (archives == null) {
return ret;
}
for (Archive a : archives) {
String name = formatName(startIdx++, prefixLength, a.getName());
File f = new File(parentDir, name);
ret.put(a, f);
if (f.exists()) {
LOG.warn(
"File " + f.getAbsolutePath() + " with the data of archive '" + a.getName() + "' already exists." +
" Assume it already contains the bits we need.");
continue;
}
Path target = new File(parentDir, name).toPath();
try (InputStream data = a.openStream()) {
Files.copy(data, target);
} catch (IOException e) {
throw new IllegalStateException(
"Failed to copy class path element: " + a.getName() + " to " + f.getAbsolutePath(), e);
}
}
return ret;
}
private int size(Iterable<?> collection) {
if (collection == null) {
return 0;
}
int ret = 0;
Iterator<?> it = collection.iterator();
while (it.hasNext()) {
ret++;
it.next();
}
return ret;
}
private String formatName(int idx, int prefixLength, String rootName) {
try {
return String.format("%0" + prefixLength + "d-%s", idx, UUID.nameUUIDFromBytes(rootName.getBytes("UTF-8")));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("UTF-8 not supported.");
}
}
}