/* * 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.test; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.NestingKind; import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; import javax.tools.ToolProvider; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; /** * @author Lukas Krejci * @since 0.2 */ public class Jar implements TestRule { private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); private List<EnvironmentImpl> compiledStuff = new ArrayList<>(); private ExecutorService compileProcess = Executors.newCachedThreadPool(); @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { compiledStuff.clear(); try { base.evaluate(); } finally { cleanUp(); } } }; } public Environment compile(String... sources) throws Exception { File dir = Files.createTempDirectory("revapi-java-spi").toFile(); List<JavaFileObject> sourceObjects = new ArrayList<>(); sourceObjects.add(new MarkerAnnotationObject()); sourceObjects.add(new ArchiveProbeObject()); for (String s : sources) { sourceObjects.add(new SourceInClassLoader(s)); } List<String> options = Arrays.asList("-d", dir.getAbsolutePath()); final JavaCompiler.CompilationTask task = compiler .getTask(null, null, null, options, Arrays.asList(ArchiveProbeObject.CLASS_NAME), sourceObjects); final Semaphore cleanUpSemaphore = new Semaphore(0); final Semaphore initSemaphore = new Semaphore(0); final EnvironmentImpl ret = new EnvironmentImpl(); task.setProcessors(Arrays.asList(new AbstractProcessor() { @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.RELEASE_7; } @Override public Set<String> getSupportedAnnotationTypes() { return new HashSet<>(Arrays.asList(MarkerAnnotationObject.CLASS_NAME)); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (roundEnv.processingOver()) { ret.elements = processingEnv.getElementUtils(); ret.types = processingEnv.getTypeUtils(); initSemaphore.release(); try { cleanUpSemaphore.acquire(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return true; } return false; } })); compileProcess.submit(new Runnable() { @Override public void run() { task.call(); } }); initSemaphore.acquire(); ret.semaphore = cleanUpSemaphore; ret.dir = dir; return ret; } @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) private void cleanUp() { for (EnvironmentImpl e : compiledStuff) { e.semaphore.release(); for (File f : e.dir.listFiles()) { f.delete(); } e.dir.delete(); } } public interface Environment { Elements getElementUtils(); Types getTypeUtils(); } private static class EnvironmentImpl implements Environment { private Elements elements; private Types types; private Semaphore semaphore; private File dir; @Override public Elements getElementUtils() { return elements; } @Override public Types getTypeUtils() { return types; } } private static class SourceInClassLoader extends SimpleJavaFileObject { URL url; SourceInClassLoader(String path) { super(getName(path), Kind.SOURCE); url = getClass().getClassLoader().getResource(path); } private static URI getName(String path) { return URI.create(path.substring(path.lastIndexOf('/') + 1)); } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { StringBuilder bld = new StringBuilder(); Reader rdr = openReader(ignoreEncodingErrors); char[] buffer = new char[512]; //our source files are small for (int cnt; (cnt = rdr.read(buffer)) != -1; ) { bld.append(buffer, 0, cnt); } rdr.close(); return bld; } @Override public NestingKind getNestingKind() { return NestingKind.TOP_LEVEL; } @Override public InputStream openInputStream() throws IOException { return url.openStream(); } @Override public Reader openReader(boolean ignoreEncodingErrors) throws IOException { return new InputStreamReader(openInputStream(), "UTF-8"); } } private static class MarkerAnnotationObject extends SimpleJavaFileObject { public static final String CLASS_NAME = "__RevapiMarkerAnnotation"; private static final String SOURCE = "public @interface " + CLASS_NAME + " {}"; public MarkerAnnotationObject() throws URISyntaxException { super(new URI(CLASS_NAME + ".java"), Kind.SOURCE); } @Override public NestingKind getNestingKind() { return NestingKind.TOP_LEVEL; } @Override public InputStream openInputStream() throws IOException { return new ByteArrayInputStream(SOURCE.getBytes()); } @Override public Reader openReader(boolean ignoreEncodingErrors) throws IOException { return new StringReader(SOURCE); } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return SOURCE; } } private static class ArchiveProbeObject extends SimpleJavaFileObject { public static final String CLASS_NAME = "Probe"; private String source; public ArchiveProbeObject() { super(getSourceFileName(), Kind.SOURCE); } private static URI getSourceFileName() { try { return new URI(CLASS_NAME + ".java"); } catch (URISyntaxException e) { //doesn't happen return null; } } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { generateIfNeeded(); return source; } @Override public NestingKind getNestingKind() { return NestingKind.TOP_LEVEL; } @Override public InputStream openInputStream() throws IOException { generateIfNeeded(); return new ByteArrayInputStream(source.getBytes()); } @Override public Reader openReader(boolean ignoreEncodingErrors) throws IOException { generateIfNeeded(); return new StringReader(source); } private void generateIfNeeded() throws IOException { if (source != null) { return; } //notice that we don't actually need to generate any complicated code. Having the classes on the classpath //is enough for them to be present in the model captured during the annotation processing. source = "@" + MarkerAnnotationObject.CLASS_NAME + "\npublic class " + CLASS_NAME + "\n{}\n"; } } }