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

Java tutorial

Introduction

Here is the source code for com.facebook.buck.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.java;

import com.facebook.buck.event.ThrowableLogEvent;
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.ByteStreams;
import com.google.common.io.InputSupplier;

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 = Preconditions.checkNotNull(pathToJarOrClassesDirectory);
        this.whereClassNamesShouldBeWritten = Preconditions.checkNotNull(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(ThrowableLogEvent.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.
     */
    private 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);
                InputSupplier<InputStream> input = new InputSupplier<InputStream>() {
                    @Override
                    public InputStream getInput() throws IOException {
                        return fileLike.getInput();
                    }
                };
                HashCode value = ByteStreams.hash(input, Hashing.sha1());
                classNamesBuilder.put(key, value);
            }
        };

        try {
            new DefaultClasspathTraverser().traverse(traversal);
        } catch (IOException e) {
            context.logError(e, "Error accumulating class names for %s.", pathToJarOrClassesDirectory);
            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();
    }

}