com.facebook.buck.jvm.java.AccumulateClassNamesStep.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.jvm.java.AccumulateClassNamesStep.java

Source

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

}