Add a ClassPath binder that adapts a StandardJavaFileManager
PiperOrigin-RevId: 335223650
diff --git a/java/com/google/turbine/binder/FileManagerClassBinder.java b/java/com/google/turbine/binder/FileManagerClassBinder.java
new file mode 100644
index 0000000..42a8162
--- /dev/null
+++ b/java/com/google/turbine/binder/FileManagerClassBinder.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2020 Google Inc. All Rights Reserved.
+ *
+ * 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.turbine.binder;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.ByteStreams;
+import com.google.turbine.binder.bound.ModuleInfo;
+import com.google.turbine.binder.bytecode.BytecodeBoundClass;
+import com.google.turbine.binder.env.Env;
+import com.google.turbine.binder.env.SimpleEnv;
+import com.google.turbine.binder.lookup.LookupKey;
+import com.google.turbine.binder.lookup.LookupResult;
+import com.google.turbine.binder.lookup.PackageScope;
+import com.google.turbine.binder.lookup.Scope;
+import com.google.turbine.binder.lookup.TopLevelIndex;
+import com.google.turbine.binder.sym.ClassSymbol;
+import com.google.turbine.binder.sym.ModuleSymbol;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import javax.tools.FileObject;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Binds a {@link StandardJavaFileManager} to an {@link ClassPath}. This can be used to share a
+ * filemanager (and associated IO costs) between turbine and javac when running both in the same
+ * process.
+ */
+public final class FileManagerClassBinder {
+
+ public static ClassPath adapt(StandardJavaFileManager fileManager, StandardLocation location) {
+ PackageLookup packageLookup = new PackageLookup(fileManager, location);
+ Env<ClassSymbol, BytecodeBoundClass> env =
+ new Env<ClassSymbol, BytecodeBoundClass>() {
+ @Override
+ public BytecodeBoundClass get(ClassSymbol sym) {
+ return packageLookup.getPackage(this, sym.packageName()).get(sym);
+ }
+ };
+ SimpleEnv<ModuleSymbol, ModuleInfo> moduleEnv = new SimpleEnv<>(ImmutableMap.of());
+ TopLevelIndex tli = new FileManagerTopLevelIndex(env, packageLookup);
+ return new ClassPath() {
+ @Override
+ public Env<ClassSymbol, BytecodeBoundClass> env() {
+ return env;
+ }
+
+ @Override
+ public Env<ModuleSymbol, ModuleInfo> moduleEnv() {
+ return moduleEnv;
+ }
+
+ @Override
+ public TopLevelIndex index() {
+ return tli;
+ }
+
+ @Override
+ public Supplier<byte[]> resource(String path) {
+ return packageLookup.resource(path);
+ }
+ };
+ }
+
+ private static class PackageLookup {
+
+ private final Map<String, Map<ClassSymbol, BytecodeBoundClass>> packages = new HashMap<>();
+ private final StandardJavaFileManager fileManager;
+ private final StandardLocation location;
+
+ private PackageLookup(StandardJavaFileManager fileManager, StandardLocation location) {
+ this.fileManager = fileManager;
+ this.location = location;
+ }
+
+ private ImmutableMap<ClassSymbol, BytecodeBoundClass> listPackage(
+ Env<ClassSymbol, BytecodeBoundClass> env, String packageName) throws IOException {
+ Map<ClassSymbol, BytecodeBoundClass> result = new HashMap<>();
+ for (JavaFileObject jfo :
+ fileManager.list(
+ location,
+ packageName.replace('/', '.'),
+ EnumSet.of(JavaFileObject.Kind.CLASS),
+ false)) {
+ String binaryName = fileManager.inferBinaryName(location, jfo);
+ ClassSymbol sym = new ClassSymbol(binaryName.replace('.', '/'));
+ result.putIfAbsent(
+ sym,
+ new BytecodeBoundClass(
+ sym,
+ new Supplier<byte[]>() {
+ @Override
+ public byte[] get() {
+ try {
+ return ByteStreams.toByteArray(jfo.openInputStream());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ },
+ env,
+ /* jarFile= */ null));
+ }
+ return ImmutableMap.copyOf(result);
+ }
+
+ private Map<ClassSymbol, BytecodeBoundClass> getPackage(
+ Env<ClassSymbol, BytecodeBoundClass> env, String key) {
+ return packages.computeIfAbsent(
+ key,
+ k -> {
+ try {
+ return listPackage(env, key);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+ }
+
+ public Supplier<byte[]> resource(String resource) {
+ String dir;
+ String name;
+ int idx = resource.lastIndexOf('/');
+ if (idx != -1) {
+ dir = resource.substring(0, idx + 1);
+ name = resource.substring(idx + 1, resource.length());
+ } else {
+ dir = "";
+ name = resource;
+ }
+ FileObject fileObject;
+ try {
+ fileObject = fileManager.getFileForInput(location, dir, name);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ if (fileObject == null) {
+ return null;
+ }
+ return new Supplier<byte[]>() {
+ @Override
+ public byte[] get() {
+ try {
+ return ByteStreams.toByteArray(fileObject.openInputStream());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ };
+ }
+ }
+
+ private static class FileManagerTopLevelIndex implements TopLevelIndex {
+ private final Env<ClassSymbol, BytecodeBoundClass> env;
+ private final PackageLookup packageLookup;
+
+ public FileManagerTopLevelIndex(
+ Env<ClassSymbol, BytecodeBoundClass> env, PackageLookup packageLookup) {
+ this.env = env;
+ this.packageLookup = packageLookup;
+ }
+
+ @Override
+ public Scope scope() {
+ return new Scope() {
+ @Override
+ public @Nullable LookupResult lookup(LookupKey lookupKey) {
+ for (int i = lookupKey.simpleNames().size(); i > 0; i--) {
+ String p = Joiner.on('/').join(lookupKey.simpleNames().subList(0, i));
+ ClassSymbol sym = new ClassSymbol(p);
+ BytecodeBoundClass r = env.get(sym);
+ if (r != null) {
+ return new LookupResult(
+ sym,
+ new LookupKey(
+ lookupKey.simpleNames().subList(i - 1, lookupKey.simpleNames().size())));
+ }
+ }
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public PackageScope lookupPackage(Iterable<String> names) {
+ String packageName = Joiner.on('/').join(names);
+ Map<ClassSymbol, BytecodeBoundClass> pkg = packageLookup.getPackage(env, packageName);
+ if (pkg.isEmpty()) {
+ return null;
+ }
+ return new PackageScope() {
+ @Override
+ public Iterable<ClassSymbol> classes() {
+ return pkg.keySet();
+ }
+
+ @Override
+ public @Nullable LookupResult lookup(LookupKey lookupKey) {
+ String className = lookupKey.first().value();
+ if (!packageName.isEmpty()) {
+ className = packageName + "/" + className;
+ }
+ ClassSymbol sym = new ClassSymbol(className);
+ if (!pkg.containsKey(sym)) {
+ return null;
+ }
+ return new LookupResult(sym, lookupKey);
+ }
+ };
+ }
+ }
+
+ private FileManagerClassBinder() {}
+}
diff --git a/javatests/com/google/turbine/binder/ClassPathBinderTest.java b/javatests/com/google/turbine/binder/ClassPathBinderTest.java
index c11d814..5093f6a 100644
--- a/javatests/com/google/turbine/binder/ClassPathBinderTest.java
+++ b/javatests/com/google/turbine/binder/ClassPathBinderTest.java
@@ -16,15 +16,19 @@
package com.google.turbine.binder;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.getLast;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
import static com.google.turbine.testing.TestClassPaths.TURBINE_BOOTCLASSPATH;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Locale.ENGLISH;
import static org.junit.Assert.fail;
import com.google.common.base.VerifyException;
+import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import com.google.common.io.MoreFiles;
@@ -34,6 +38,7 @@
import com.google.turbine.binder.env.Env;
import com.google.turbine.binder.lookup.LookupKey;
import com.google.turbine.binder.lookup.LookupResult;
+import com.google.turbine.binder.lookup.PackageScope;
import com.google.turbine.binder.lookup.Scope;
import com.google.turbine.binder.sym.ClassSymbol;
import com.google.turbine.binder.sym.FieldSymbol;
@@ -46,36 +51,97 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Arrays;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class ClassPathBinderTest {
+ @Parameterized.Parameters
+ public static ImmutableCollection<Object[]> parameters() {
+ Object[] testCases = {
+ TURBINE_BOOTCLASSPATH,
+ FileManagerClassBinder.adapt(
+ ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, ENGLISH, UTF_8),
+ StandardLocation.PLATFORM_CLASS_PATH),
+ };
+ return Arrays.stream(testCases).map(x -> new Object[] {x}).collect(toImmutableList());
+ }
+
+ private final ClassPath classPath;
+
+ public ClassPathBinderTest(ClassPath classPath) {
+ this.classPath = classPath;
+ }
+
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+ private static Ident ident(String string) {
+ return new Ident(/* position= */ -1, string);
+ }
+
@Test
- public void classPathLookup() throws IOException {
+ public void classPathLookup() {
- Scope javaLang = TURBINE_BOOTCLASSPATH.index().lookupPackage(ImmutableList.of("java", "lang"));
+ Scope javaLang = classPath.index().lookupPackage(ImmutableList.of("java", "lang"));
- LookupResult result = javaLang.lookup(new LookupKey(ImmutableList.of(new Ident(-1, "String"))));
+ final String string = "String";
+ LookupResult result = javaLang.lookup(new LookupKey(ImmutableList.of(ident(string))));
assertThat(result.remaining()).isEmpty();
assertThat(result.sym()).isEqualTo(new ClassSymbol("java/lang/String"));
- result = javaLang.lookup(new LookupKey(ImmutableList.of(new Ident(-1, "Object"))));
+ result = javaLang.lookup(new LookupKey(ImmutableList.of(ident("Object"))));
assertThat(result.remaining()).isEmpty();
assertThat(result.sym()).isEqualTo(new ClassSymbol("java/lang/Object"));
}
@Test
- public void classPathClasses() throws IOException {
- Env<ClassSymbol, BytecodeBoundClass> env = TURBINE_BOOTCLASSPATH.env();
+ public void packageScope() {
+
+ PackageScope result = classPath.index().lookupPackage(ImmutableList.of("java", "nosuch"));
+ assertThat(result).isNull();
+
+ result = classPath.index().lookupPackage(ImmutableList.of("java", "lang"));
+ assertThat(result.classes()).contains(new ClassSymbol("java/lang/String"));
+
+ assertThat(result.lookup(new LookupKey(ImmutableList.of(ident("NoSuch"))))).isNull();
+ }
+
+ @Test
+ public void scope() {
+ Scope scope = classPath.index().scope();
+ LookupResult result;
+
+ result =
+ scope.lookup(
+ new LookupKey(
+ ImmutableList.of(ident("java"), ident("util"), ident("Map"), ident("Entry"))));
+ assertThat(result.sym()).isEqualTo(new ClassSymbol("java/util/Map"));
+ assertThat(result.remaining().stream().map(Ident::value)).containsExactly("Entry");
+
+ result =
+ scope.lookup(new LookupKey(ImmutableList.of(ident("java"), ident("util"), ident("Map"))));
+ assertThat(result.sym()).isEqualTo(new ClassSymbol("java/util/Map"));
+ assertThat(result.remaining()).isEmpty();
+
+ result =
+ scope.lookup(
+ new LookupKey(ImmutableList.of(ident("java"), ident("util"), ident("NoSuch"))));
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void classPathClasses() {
+ Env<ClassSymbol, BytecodeBoundClass> env = classPath.env();
TypeBoundClass c = env.get(new ClassSymbol("java/util/Map$Entry"));
assertThat(c.owner()).isEqualTo(new ClassSymbol("java/util/Map"));
@@ -96,7 +162,7 @@
@Test
public void interfaces() {
- Env<ClassSymbol, BytecodeBoundClass> env = TURBINE_BOOTCLASSPATH.env();
+ Env<ClassSymbol, BytecodeBoundClass> env = classPath.env();
TypeBoundClass c = env.get(new ClassSymbol("java/lang/annotation/Retention"));
assertThat(c.interfaceTypes()).hasSize(1);
@@ -114,7 +180,7 @@
@Test
public void annotations() {
- Env<ClassSymbol, BytecodeBoundClass> env = TURBINE_BOOTCLASSPATH.env();
+ Env<ClassSymbol, BytecodeBoundClass> env = classPath.env();
TypeBoundClass c = env.get(new ClassSymbol("java/lang/annotation/Retention"));
AnnoInfo anno =
@@ -178,4 +244,21 @@
assertThat(new String(classPath.resource("foo/bar/hello.txt").get(), UTF_8)).isEqualTo("hello");
assertThat(classPath.resource("foo/bar/Baz.class")).isNull();
}
+
+ @Test
+ public void resourcesFileManager() throws Exception {
+ Path path = temporaryFolder.newFile("tmp.jar").toPath();
+ try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(path))) {
+ jos.putNextEntry(new JarEntry("foo/bar/hello.txt"));
+ jos.write("hello".getBytes(UTF_8));
+ jos.putNextEntry(new JarEntry("foo/bar/Baz.class"));
+ jos.write("goodbye".getBytes(UTF_8));
+ }
+ StandardJavaFileManager fileManager =
+ ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, ENGLISH, UTF_8);
+ fileManager.setLocation(StandardLocation.CLASS_PATH, ImmutableList.of(path.toFile()));
+ ClassPath classPath = FileManagerClassBinder.adapt(fileManager, StandardLocation.CLASS_PATH);
+ assertThat(new String(classPath.resource("foo/bar/hello.txt").get(), UTF_8)).isEqualTo("hello");
+ assertThat(classPath.resource("foo/bar/NoSuch.class")).isNull();
+ }
}