/*
* Copyright 2013-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 com.facebook.buck.event.ThrowableConsoleEvent;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.jvm.java.classes.ClasspathTraversal;
import com.facebook.buck.jvm.java.classes.DefaultClasspathTraverser;
import com.facebook.buck.jvm.java.classes.FileLike;
import com.facebook.buck.jvm.java.classes.FileLikes;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.StepExecutionResult;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteSource;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* {@link Step} that takes a directory or zip of {@code .class} files and traverses it to get the
* total set of {@code .class} files included by the directory or zip.
*/
public class AccumulateClassNamesStep implements Step {
/**
* In the generated {@code classes.txt} file, each line will contain the path to a {@code .class}
* file (without its suffix) and the SHA-1 hash of its contents, separated by this separator.
*/
static final String CLASS_NAME_HASH_CODE_SEPARATOR = " ";
private static final Splitter CLASS_NAME_AND_HASH_SPLITTER =
Splitter.on(CLASS_NAME_HASH_CODE_SEPARATOR);
private final ProjectFilesystem filesystem;
private final Optional<Path> pathToJarOrClassesDirectory;
private final Path whereClassNamesShouldBeWritten;
/**
* @param pathToJarOrClassesDirectory Where to look for .class files. If absent, then an empty
* file will be written to {@code whereClassNamesShouldBeWritten}.
* @param whereClassNamesShouldBeWritten Path to a file where an alphabetically sorted list of
* class files and corresponding SHA-1 hashes of their contents will be written.
*/
public AccumulateClassNamesStep(
ProjectFilesystem filesystem,
Optional<Path> pathToJarOrClassesDirectory,
Path whereClassNamesShouldBeWritten) {
this.filesystem = filesystem;
this.pathToJarOrClassesDirectory = pathToJarOrClassesDirectory;
this.whereClassNamesShouldBeWritten = whereClassNamesShouldBeWritten;
}
@Override
public StepExecutionResult execute(ExecutionContext context) {
ImmutableSortedMap<String, HashCode> classNames;
if (pathToJarOrClassesDirectory.isPresent()) {
Optional<ImmutableSortedMap<String, HashCode>> classNamesOptional =
calculateClassHashes(
context, filesystem, filesystem.resolve(pathToJarOrClassesDirectory.get()));
if (classNamesOptional.isPresent()) {
classNames = classNamesOptional.get();
} else {
return StepExecutionResult.ERROR;
}
} else {
classNames = ImmutableSortedMap.of();
}
try {
filesystem.writeLinesToPath(
Iterables.transform(
classNames.entrySet(),
entry -> entry.getKey() + CLASS_NAME_HASH_CODE_SEPARATOR + entry.getValue()),
whereClassNamesShouldBeWritten);
} catch (IOException e) {
context
.getBuckEventBus()
.post(
ThrowableConsoleEvent.create(
e,
"There was an error writing the list of .class files to %s.",
whereClassNamesShouldBeWritten));
return StepExecutionResult.ERROR;
}
return StepExecutionResult.SUCCESS;
}
@Override
public String getShortName() {
return "get_class_names";
}
@Override
public String getDescription(ExecutionContext context) {
String sourceString = pathToJarOrClassesDirectory.map(Object::toString).orElse("null");
return String.format("get_class_names %s > %s", sourceString, whereClassNamesShouldBeWritten);
}
/** @return an Optional that will be absent if there was an error. */
public static Optional<ImmutableSortedMap<String, HashCode>> calculateClassHashes(
ExecutionContext context, ProjectFilesystem filesystem, Path path) {
final Map<String, HashCode> classNames = new HashMap<>();
ClasspathTraversal traversal =
new ClasspathTraversal(Collections.singleton(path), filesystem) {
@Override
public void visit(final FileLike fileLike) throws IOException {
// When traversing a JAR file, it may have resources or directory entries that do not
// end in .class, which should be ignored.
if (!FileLikes.isClassFile(fileLike)) {
return;
}
String key = FileLikes.getFileNameWithoutClassSuffix(fileLike);
ByteSource input =
new ByteSource() {
@Override
public InputStream openStream() throws IOException {
return fileLike.getInput();
}
};
HashCode value = input.hash(Hashing.sha1());
HashCode existing = classNames.putIfAbsent(key, value);
if (existing != null && !existing.equals(value)) {
throw new IllegalArgumentException(
String.format(
"Multiple entries with same key but differing values: %1$s=%2$s and %1$s=%3$s",
key, value, existing));
}
}
};
try {
new DefaultClasspathTraverser().traverse(traversal);
} catch (IOException e) {
context.logError(e, "Error accumulating class names for %s.", path);
return Optional.empty();
}
return Optional.of(ImmutableSortedMap.copyOf(classNames, Ordering.natural()));
}
/**
* @param lines that were written in the same format output by {@link #execute(ExecutionContext)}.
*/
public static ImmutableSortedMap<String, HashCode> parseClassHashes(List<String> lines) {
final Map<String, HashCode> classNames = new HashMap<>();
for (String line : lines) {
List<String> parts = CLASS_NAME_AND_HASH_SPLITTER.splitToList(line);
Preconditions.checkState(parts.size() == 2);
String key = parts.get(0);
HashCode value = HashCode.fromString(parts.get(1));
HashCode existing = classNames.putIfAbsent(key, value);
if (existing != null && !existing.equals(value)) {
throw new IllegalArgumentException(
String.format(
"Multiple entries with same key but differing values: %1$s=%2$s and %1$s=%3$s",
key, value, existing));
}
}
return ImmutableSortedMap.copyOf(classNames, Ordering.natural());
}
}