/* * Copyright 2015-present Facebook, Inc. * * 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.facebook.buck.jvm.java; import static javax.tools.StandardLocation.CLASS_OUTPUT; import com.facebook.buck.log.Logger; import com.facebook.buck.util.PatternsMatcher; import com.facebook.buck.zip.CustomZipEntry; import com.facebook.buck.zip.JarBuilder; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; /** * A {@link StandardJavaFileManager} that creates and writes the content of files directly into a * Jar output stream instead of writing the files to disk. */ public class JavaInMemoryFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> implements StandardJavaFileManager { private static final Logger LOG = Logger.get(JavaInMemoryFileManager.class); private Path jarPath; private StandardJavaFileManager delegate; private Set<String> directoryPaths; private Map<String, JarFileObject> fileForOutputPaths; private PatternsMatcher classesToRemoveFromJar; public JavaInMemoryFileManager( StandardJavaFileManager standardManager, Path jarPath, ImmutableSet<Pattern> classesToRemoveFromJar) { super(standardManager); this.delegate = standardManager; this.jarPath = jarPath; this.directoryPaths = new HashSet<>(); this.fileForOutputPaths = new HashMap<>(); this.classesToRemoveFromJar = new PatternsMatcher(classesToRemoveFromJar); } /** * Creates a ZipEntry for placing in the jar output stream. Sets the modification time to 0 for a * deterministic jar. * * @param name the name of the entry * @return the zip entry for the file specified */ public static ZipEntry createEntry(String name) { CustomZipEntry entry = new CustomZipEntry(name); // We want deterministic JARs, so avoid mtimes. entry.setFakeTime(); return entry; } private static String getPath(String className) { return className.replace('.', '/'); } private static String getPath(String className, JavaFileObject.Kind kind) { return className.replace('.', '/') + kind.extension; } private static String getPath(String packageName, String relativeName) { return !packageName.isEmpty() ? packageName.replace('.', '/') + '/' + relativeName : relativeName; } @Override public JavaFileObject getJavaFileForOutput( Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { // Use the normal FileObject that writes to the disk for source files. if (shouldDelegate(location)) { return delegate.getJavaFileForOutput(location, className, kind, sibling); } String path = getPath(className, kind); // If the class is to be removed from the Jar create a NoOp FileObject. if (classesToRemoveFromJar.hasPatterns() && classesToRemoveFromJar.matches(className.toString())) { LOG.info( "%s was excluded from the Jar because it matched a remove_classes pattern.", className.toString()); return getJavaNoOpFileObject(path, kind); } return getJavaMemoryFileObject(kind, path); } @Override public FileObject getFileForOutput( Location location, String packageName, String relativeName, FileObject sibling) throws IOException { if (shouldDelegate(location)) { return delegate.getFileForOutput(location, packageName, relativeName, sibling); } String path = getPath(packageName, relativeName); return getJavaMemoryFileObject(JavaFileObject.Kind.OTHER, path); } @Override public boolean isSameFile(FileObject a, FileObject b) { boolean aInMemoryJavaFileInstance = a instanceof JavaInMemoryFileObject; boolean bInMemoryJavaFileInstance = b instanceof JavaInMemoryFileObject; if (aInMemoryJavaFileInstance || bInMemoryJavaFileInstance) { return aInMemoryJavaFileInstance && bInMemoryJavaFileInstance && a.getName().equals(b.getName()); } return super.isSameFile(a, b); } @Override public Iterable<? extends JavaFileObject> getJavaFileObjectsFromFiles( Iterable<? extends File> files) { return delegate.getJavaFileObjectsFromFiles(files); } @Override public Iterable<? extends JavaFileObject> getJavaFileObjects(File... files) { return delegate.getJavaFileObjects(files); } @Override public Iterable<? extends JavaFileObject> getJavaFileObjectsFromStrings(Iterable<String> names) { return delegate.getJavaFileObjectsFromStrings(names); } @Override public Iterable<? extends JavaFileObject> getJavaFileObjects(String... names) { return delegate.getJavaFileObjects(names); } @Override public void setLocation(Location location, Iterable<? extends File> path) throws IOException { delegate.setLocation(location, path); } @Override public Iterable<? extends File> getLocation(Location location) { return delegate.getLocation(location); } @Override public Iterable<JavaFileObject> list( Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException { if (shouldDelegate(location)) { return delegate.list(location, packageName, kinds, recurse); } ArrayList<JavaFileObject> results = new ArrayList<>(); for (JavaFileObject fromSuper : delegate.list(location, packageName, kinds, recurse)) { results.add(fromSuper); } String packageDirPath = getPath(packageName) + '/'; for (String filepath : fileForOutputPaths.keySet()) { if (recurse && filepath.startsWith(packageDirPath)) { results.add(fileForOutputPaths.get(filepath)); } else if (!recurse && filepath.startsWith(packageDirPath) && filepath.substring(packageDirPath.length()).indexOf('/') < 0) { results.add(fileForOutputPaths.get(filepath)); } } return results; } public ImmutableSet<String> writeToJar(JarBuilder jarBuilder) throws IOException { for (JarFileObject fileObject : fileForOutputPaths.values()) { fileObject.writeToJar(jarBuilder, jarPath.toString()); } return ImmutableSet.copyOf(Sets.union(directoryPaths, fileForOutputPaths.keySet())); } private boolean shouldDelegate(Location location) { return location != CLASS_OUTPUT; } private JavaFileObject getJavaMemoryFileObject(JavaFileObject.Kind kind, String path) throws IOException { return fileForOutputPaths.computeIfAbsent( path, p -> new JavaInMemoryFileObject(getUriPath(p), p, kind)); } private JavaFileObject getJavaNoOpFileObject(String path, JavaFileObject.Kind kind) { return fileForOutputPaths.computeIfAbsent( path, p -> new JavaNoOpFileObject(getUriPath(p), p, kind)); } private String encodeURL(String path) { try { return URLEncoder.encode(path, "UTF-8").replace("%2F", "/"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private URI getUriPath(String relativePath) { return URI.create("jar:file:" + encodeURL(jarPath.toString()) + "!/" + encodeURL(relativePath)); } }