| /* |
| * 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.java; |
| |
| import com.facebook.buck.event.ThrowableConsoleEvent; |
| import com.facebook.buck.java.classes.ClasspathTraversal; |
| import com.facebook.buck.java.classes.DefaultClasspathTraverser; |
| import com.facebook.buck.java.classes.FileLike; |
| import com.facebook.buck.java.classes.FileLikes; |
| import com.facebook.buck.step.ExecutionContext; |
| import com.facebook.buck.step.Step; |
| import com.google.common.base.Function; |
| import com.google.common.base.Functions; |
| import com.google.common.base.Optional; |
| 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.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.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| /** |
| * {@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 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(Optional<Path> pathToJarOrClassesDirectory, |
| Path whereClassNamesShouldBeWritten) { |
| this.pathToJarOrClassesDirectory = pathToJarOrClassesDirectory; |
| this.whereClassNamesShouldBeWritten = whereClassNamesShouldBeWritten; |
| } |
| |
| @Override |
| public int execute(ExecutionContext context) { |
| ImmutableSortedMap<String, HashCode> classNames; |
| if (pathToJarOrClassesDirectory.isPresent()) { |
| Optional<ImmutableSortedMap<String, HashCode>> classNamesOptional = calculateClassHashes( |
| context, |
| context.getProjectFilesystem().resolve(pathToJarOrClassesDirectory.get())); |
| if (classNamesOptional.isPresent()) { |
| classNames = classNamesOptional.get(); |
| } else { |
| return 1; |
| } |
| } else { |
| classNames = ImmutableSortedMap.of(); |
| } |
| |
| try { |
| context.getProjectFilesystem().writeLinesToPath( |
| Iterables.transform(classNames.entrySet(), |
| new Function<Map.Entry<String, HashCode>, String>() { |
| @Override |
| public String apply(Entry<String, HashCode> entry) { |
| return 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 1; |
| } |
| |
| return 0; |
| } |
| |
| @Override |
| public String getShortName() { |
| return "get_class_names"; |
| } |
| |
| @Override |
| public String getDescription(ExecutionContext context) { |
| String sourceString = pathToJarOrClassesDirectory |
| .transform(Functions.toStringFunction()) |
| .or("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, Path path) { |
| final ImmutableSortedMap.Builder<String, HashCode> classNamesBuilder = |
| ImmutableSortedMap.naturalOrder(); |
| ClasspathTraversal traversal = |
| new ClasspathTraversal(Collections.singleton(path), context.getProjectFilesystem()) { |
| @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()); |
| classNamesBuilder.put(key, value); |
| } |
| }; |
| |
| try { |
| new DefaultClasspathTraverser().traverse(traversal); |
| } catch (IOException e) { |
| context.logError(e, "Error accumulating class names for %s.", path); |
| return Optional.absent(); |
| } |
| |
| return Optional.of(classNamesBuilder.build()); |
| } |
| |
| /** |
| * @param lines that were written in the same format output by {@link #execute(ExecutionContext)}. |
| */ |
| public static ImmutableSortedMap<String, HashCode> parseClassHashes(List<String> lines) { |
| ImmutableSortedMap.Builder<String, HashCode> classNamesBuilder = |
| ImmutableSortedMap.naturalOrder(); |
| |
| 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)); |
| classNamesBuilder.put(key, value); |
| } |
| |
| return classNamesBuilder.build(); |
| } |
| |
| } |