/* * Copyright 2012-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.classes; import com.facebook.buck.io.MorePaths; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.util.ZipFileTraversal; import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.io.InputStream; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.Collection; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.annotation.Nullable; /** * Traversal strategy for traversing a set of paths that themselves are traversed. The provided * paths can point to zip/jar files, directories of resource/class files, or individual files * themselves. * * <p>For example, given the input paths of { foo.zip, foo/, and foo.txt }, traverse would first * expand foo.zip and traverse its contents, then list the files recursively in foo/, and finally * visit the single file foo.txt. */ public abstract class ClasspathTraversal { private final Iterable<Path> paths; private final ProjectFilesystem filesystem; public ClasspathTraversal(Collection<Path> paths, ProjectFilesystem filesystem) { this.paths = paths; this.filesystem = filesystem; } public abstract void visit(FileLike fileLike) throws IOException; /** * Subclasses can override this method to return a value of any type. This often represents some * sort of cumulative value that is computed as a result of the traversal. */ // TODO(mbolin): Change this from Object to a generic <T>. @Nullable public Object getResult() { return null; } public final void traverse() throws IOException { for (Path path : paths) { ClasspathTraverser adapter = createTraversalAdapter(filesystem.getPathForRelativePath(path)); adapter.traverse(this); } } private ClasspathTraverser createTraversalAdapter(Path path) { String extension = MorePaths.getFileExtension(path); if (filesystem.isDirectory(path)) { return new DirectoryTraversalAdapter(filesystem, path); } else if (filesystem.isFile(path)) { if (extension.equalsIgnoreCase("jar") || extension.equalsIgnoreCase("zip")) { return new ZipFileTraversalAdapter(path); } else { return new FileTraversalAdapter(path); } } else { throw new IllegalArgumentException("Unsupported classpath traversal input: " + path); } } private static class ZipFileTraversalAdapter implements ClasspathTraverser { private final Path file; public ZipFileTraversalAdapter(Path file) { this.file = file; } @Override public void traverse(final ClasspathTraversal traversal) throws IOException { ZipFileTraversal impl = new ZipFileTraversal(file) { @Override public void visit(ZipFile zipFile, ZipEntry zipEntry) throws IOException { traversal.visit(new FileLikeInZip(file, zipFile, zipEntry)); } }; impl.traverse(); } private static class FileLikeInZip extends AbstractFileLike { private final Path container; private final ZipFile zipFile; private final ZipEntry entry; public FileLikeInZip(Path container, ZipFile zipFile, ZipEntry entry) { this.container = container; this.zipFile = zipFile; this.entry = entry; } @Override public Path getContainer() { return container; } @Override public String getRelativePath() { return entry.getName(); } @Override public long getSize() { return entry.getSize(); } @Override public InputStream getInput() throws IOException { return zipFile.getInputStream(entry); } } } private static class DirectoryTraversalAdapter implements ClasspathTraverser { private final ProjectFilesystem filesystem; private final Path directory; public DirectoryTraversalAdapter(ProjectFilesystem filesystem, Path directory) { this.filesystem = filesystem; this.directory = directory; } @Override public void traverse(final ClasspathTraversal traversal) throws IOException { filesystem.walkFileTree( directory, ImmutableSet.of(FileVisitOption.FOLLOW_LINKS), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String relativePath = MorePaths.pathWithUnixSeparators(MorePaths.relativize(directory, file)); traversal.visit(new FileLikeInDirectory(file, relativePath)); return FileVisitResult.CONTINUE; } }); } } private static class FileTraversalAdapter implements ClasspathTraverser { private final Path file; public FileTraversalAdapter(Path file) { this.file = file; } @Override public void traverse(ClasspathTraversal traversal) throws IOException { traversal.visit(new FileLikeInDirectory(file, file.getFileName().toString())); } } private static class FileLikeInDirectory extends AbstractFileLike { private final Path file; private final String relativePath; public FileLikeInDirectory(Path file, String relativePath) { // Currently, the only instances of FileLikeInDirectory appear to be the .class files // generated from an R.java in Android. The only exception is in unit tests. this.file = file; this.relativePath = relativePath; } @Override public Path getContainer() { return file; } @Override public String getRelativePath() { return relativePath; } @Override public long getSize() throws IOException { return Files.size(file); } @Override public InputStream getInput() throws IOException { return Files.newInputStream(file); } } }