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();
+  }
 }