com.facebook.buck.ide.intellij.projectview.ProjectView.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.ide.intellij.projectview.ProjectView.java

Source

/*
 * Copyright 2017-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.ide.intellij.projectview;

import static com.facebook.buck.ide.intellij.projectview.Patterns.capture;
import static com.facebook.buck.ide.intellij.projectview.Patterns.noncapture;
import static com.facebook.buck.ide.intellij.projectview.Patterns.optional;

import com.facebook.buck.config.Config;
import com.facebook.buck.graph.AbstractBreadthFirstTraversal;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.jvm.java.JavaLibrary;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.ActionGraphAndResolver;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.CommonDescriptionArg;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.rules.TargetNodes;
import com.facebook.buck.util.DirtyPrintStreamDecorator;
import com.google.common.collect.ImmutableSet;
import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;

public class ProjectView {

    // region Public API

    public static int run(DirtyPrintStreamDecorator stderr, boolean dryRun, boolean withTests, String viewPath,
            TargetGraph targetGraph, ImmutableSet<BuildTarget> buildTargets, ActionGraphAndResolver actionGraph,
            Config config) {
        return new ProjectView(stderr, dryRun, withTests, viewPath, targetGraph, buildTargets, actionGraph, config)
                .run();
    }

    // endregion Public API

    // region Private implementation

    private static final String ANDROID_MANIFEST = "AndroidManifest.xml";
    private static final String ANDROID_RES = "android_res";
    private static final String ASSETS = "assets";
    private static final String CODE_STYLE_SETTINGS = "codeStyleSettings.xml";
    private static final String DOT_IDEA = ".idea";
    private static final String DOT_XML = ".xml";
    private static final String FONTS = "fonts";
    private static final String RES = "res";

    private final DirtyPrintStreamDecorator stdErr;
    private final String viewPath;
    private final boolean dryRun;
    private final boolean withTests;
    private final TargetGraph targetGraph;
    private final ImmutableSet<BuildTarget> buildTargets;
    private final Config config;

    private final ActionGraphAndResolver actionGraph;

    private final Set<BuildTarget> testTargets = new HashSet<>();
    /** {@code Sets.union(buildTargets, allTargets)} */
    private final Set<BuildTarget> allTargets = new HashSet<>();

    private final String repository = new File("").getAbsolutePath();

    private ProjectView(DirtyPrintStreamDecorator stdErr, boolean dryRun, boolean withTests, String viewPath,
            TargetGraph targetGraph, ImmutableSet<BuildTarget> buildTargets, ActionGraphAndResolver actionGraph,
            Config config) {
        this.stdErr = stdErr;
        this.viewPath = viewPath;
        this.dryRun = dryRun;
        this.withTests = withTests;

        this.targetGraph = targetGraph;
        this.buildTargets = buildTargets;
        this.actionGraph = actionGraph;

        this.config = config;
    }

    private int run() {
        if (viewPathIsUnderRepository()) {
            stderr("\nView directory %s is under the repo directory %s\n", viewPath, repository);
            return 1;
        }

        getTestTargets();

        List<String> inputs = getPrunedInputs();

        scanExistingView();

        List<String> sourceFiles = new ArrayList<>();
        for (String input : inputs) {
            if (input.startsWith("android_res/")) {
                linkResourceFile(input);
            } else {
                sourceFiles.add(input);
            }
        }
        Set<String> roots = generateRoots(sourceFiles);
        buildRootLinks(roots);

        writeRootDotIml(sourceFiles, roots, buildDotIdeaFolder(inputs));

        buildAllDirectoriesAndSymlinks();

        return 0;
    }

    private boolean viewPathIsUnderRepository() {
        Path view = Paths.get(viewPath).toAbsolutePath();
        Path repo = Paths.get(repository).toAbsolutePath();
        return view.startsWith(repo);
    }

    // region getTestTargets

    private void getTestTargets() {
        if (withTests) {
            AbstractBreadthFirstTraversal.<TargetNode<?, ?>>traverse(targetGraph.getAll(buildTargets), node -> {
                testTargets.addAll(TargetNodes.getTestTargetsForNode(node));
                return targetGraph.getAll(node.getBuildDeps());
            });
        }

        allTargets.addAll(buildTargets);
        allTargets.addAll(testTargets);
    }

    // endregion getTestTargets

    // region getPrunedInputs()

    private List<String> getPrunedInputs() {
        return pruneInputs(getAllInputs());
    }

    private Collection<String> getAllInputs() {

        Set<String> inputs = new HashSet<>();

        for (TargetNode<?, ?> node : targetGraph.getNodes()) {
            node.getInputs().forEach(input -> inputs.add(input.toString()));
        }

        return inputs.stream()
                //ignore non-english strings
                .filter(input -> !(input.contains("/res/values-") && input.endsWith("strings.xml")))
                .collect(Collectors.toList());
    }

    private List<String> pruneInputs(Collection<String> allInputs) {
        Pattern resource = Pattern.compile("/res/(?!(?:values(?:-[^/]+)?)/)");

        List<String> result = new ArrayList<>();
        Map<String, List<String>> resources = new HashMap<>();

        for (String input : allInputs) {
            Matcher matcher = resource.matcher(input);
            if (matcher.find()) {
                String basename = basename(input);
                List<String> candidates = resources.get(basename);
                if (candidates == null) {
                    resources.put(basename, candidates = new ArrayList<>());
                }
                candidates.add(input);
            } else {
                result.add(input);
            }
        }

        for (Map.Entry<String, List<String>> mapping : resources.entrySet()) {
            List<String> candidateList = mapping.getValue();
            Stream<String> candidateStream = candidateList.stream();
            if (candidateList.size() > 1) {
                candidateStream = candidateStream.sorted();
            }
            result.add(candidateStream.findFirst().get());
        }

        return result;
    }

    // endregion getPrunedInputs()

    // region linkResourceFile

    private static final String DASH_PART = "-[^/]+";
    private static final String NONCAPTURE_DASH_PART = optional(noncapture(DASH_PART));

    private static final Patterns SIMPLE_RESOURCE_PATTERNS = Patterns.builder()
            // These are ordered based on the frequency in two large Android projects.
            // This ordering will not be ideal for every project, but it's probably not too far off.
            .add("/res/", capture("drawable", NONCAPTURE_DASH_PART), "/")
            .add("/res/", capture("layout", NONCAPTURE_DASH_PART), "/")
            .add("/res/", capture("raw", NONCAPTURE_DASH_PART), "/")
            .add("/res/", capture("anim", NONCAPTURE_DASH_PART), "/")
            .add("/res/", capture("xml", NONCAPTURE_DASH_PART), "/")
            .add("/res/", capture("menu", NONCAPTURE_DASH_PART), "/").add("/res/", capture("animator"), "/")
            .build();

    private static final String CAPTURE_ALL = capture(".*");
    private static final String CAPTURE_DASH_PART = optional(capture(DASH_PART));

    private static final Patterns MANGLED_RESOURCE_PATTERNS = Patterns.builder()
            // These are also ordered based on the frequency in the same two large Android projects.
            .add("^android_res/", CAPTURE_ALL, "res/(values)", CAPTURE_DASH_PART, "/")
            .add("^android_res/", CAPTURE_ALL, "res/(color)", CAPTURE_DASH_PART, "/").build();

    // Group 1 has any path under ...//assets/ while group 2 has the filename
    private static final Patterns ASSETS_RES = Patterns.build("/assets/", capture(noncapture("[^/]+/"), "*"),
            CAPTURE_ALL);

    private static final Patterns FONTS_RES = Patterns.build("/fonts/", capture(".*\\.\\w+"));

    private void linkResourceFile(String input) {
        // TODO(shemitz) Convert (say) "res/drawable-hdpi/" to "res/drawable/"

        if (SIMPLE_RESOURCE_PATTERNS.onAnyMatch(input, this::simpleResourceLink)) {
            return;
        }

        if (MANGLED_RESOURCE_PATTERNS.onAnyMatch(input, this::mangledResourceLink)) {
            return;
        }

        if (ASSETS_RES.onAnyMatch(input, this::assetsLink)) {
            return;
        }

        if (FONTS_RES.onAnyMatch(input, this::fontsLink)) {
            return;
        }

        if (input.contains(".")) {
            stderr("Can't handle %s\n", input);
        }
    }

    private void simpleResourceLink(Matcher match, String input) {
        String name = basename(input);

        String directory = fileJoin(viewPath, RES, flattenResourceDirectoryName(match.group(1)));
        mkdir(directory);

        symlink(fileJoin(repository, input), fileJoin(directory, name));
    }

    private void mangledResourceLink(Matcher match, String input) {
        String fileName = basename(input);
        //its safe to assume input is .xml file
        String name = fileName.substring(0, fileName.length() - DOT_XML.length());

        String path = match.group(1).replace('/', '_');

        String configQualifier = match.groupCount() > 2 ? match.group(3) : "";

        String directory = fileJoin(viewPath, RES, match.group(2));
        mkdir(directory);

        symlink(fileJoin(repository, input), fileJoin(directory, path + name + configQualifier + DOT_XML));
    }

    private static String flattenResourceDirectoryName(String name) {
        int dash = name.indexOf('-');
        return dash < 0 ? name : name.substring(0, dash);
    }

    private void assetsLink(Matcher match, String input) {
        String inside = match.group(1); // everything between .../assets/ and filename
        String name = match.group(2); // basename(input)

        String directory = fileJoin(viewPath, ASSETS, inside);
        mkdir(directory);

        symlink(fileJoin(repository, input), fileJoin(directory, name));
    }

    private void fontsLink(Matcher match, String input) {
        String target = fileJoin(viewPath, FONTS, match.group(1));
        String path = dirname(target);
        mkdir(path);
        symlink(fileJoin(repository, input), target);
    }

    // endregion linkResourceFile

    // region roots

    private Set<String> generateRoots(List<String> sourceFiles) {
        final Set<String> roots = new HashSet<>();
        final RootsHelper helper = new RootsHelper();

        for (String sourceFile : sourceFiles) {
            String path = dirname(sourceFile);
            if (!isNullOrEmpty(path)) {
                helper.addSourcePath(path);
            }
        }
        final List<String> paths = helper.getSortedSourcePaths();

        for (int index = 0, size = paths.size(); index < size; /*increment in loop*/ ) {
            final String path = paths.get(index);

            // This folder could be a root, but so could any of its parents. The best root is the one that
            // requires the fewest excludedFolder tags
            int lowestCost = helper.excludesUnder(path);
            String bestRoot = path;
            String parent = dirname(path);
            while (!isNullOrEmpty(parent)) {
                int cost = helper.excludesUnder(parent);
                if (cost < lowestCost) {
                    lowestCost = cost;
                    bestRoot = parent;
                }
                parent = dirname(parent);
            }
            roots.add(bestRoot);

            index += 1;
            String prefix = guaranteeEndsWithFileSeparator(bestRoot);
            while (index < size && paths.get(index).startsWith(prefix)) {
                index += 1;
            }
        }

        return roots;
    }

    private void buildRootLinks(Set<String> roots) {
        for (String root : roots) {
            symlink(fileJoin(repository, root), fileJoin(viewPath, root));
        }
    }

    /** Maintains a set of source pathes, and a map of paths -> excludes */
    private class RootsHelper {
        private final Set<String> sourcePaths = new HashSet<>();
        private final Map<String, Integer> excludes = new HashMap<>();

        void addSourcePath(String sourcePath) {
            sourcePaths.add(sourcePath);
        }

        boolean isSourcePath(String sourcePath) {
            return sourcePaths.contains(sourcePath);
        }

        List<String> getSortedSourcePaths() {
            return sourcePaths.stream().sorted().collect(Collectors.toList());
        }

        int excludesUnder(String path) {
            if (excludes.containsKey(path)) {
                return excludes.get(path);
            }

            int sum = 0;
            File absolute = new File(repository, path);
            String[] files = absolute.list(neitherDotOrDotDot);
            if (files != null) {
                for (String entry : files) {
                    String child = fileJoin(path, entry);
                    if (isDirectory(fileJoin(repository, child))) {
                        if (!isSourcePath(child)) {
                            sum += 1;
                        }
                        sum += excludesUnder(child);
                    }
                }
            }
            excludes.put(path, sum);
            return sum;
        }
    }

    // endregion roots

    // region .idea folder

    private static final String BUCK_OUT = "buck-out";
    private static final String COMPONENT = "component";
    private static final String CONTENT = "content";
    private static final String EXCLUDE_FOLDER = "excludeFolder";
    private static final String IS_TEST_SOURCE = "isTestSource";
    private static final String LIBRARY = "library";
    private static final String MODULES = "modules";
    private static final String NAME = "name";
    private static final String OPTION = "option";
    private static final String ORDER_ENTRY = "orderEntry";
    private static final String ROOT_IML = "root.iml";
    private static final String SOURCE_FOLDER = "sourceFolder";
    private static final String TYPE = "type";
    private static final String URL = "url";
    private static final String VALUE = "value";
    private static final String VERSION = "version";

    private static final String MODULE_DIR = "$MODULE_DIR$";
    private static final String FILE_MODULE_DIR = "file://" + MODULE_DIR;

    // region XML utilities

    private enum XML {
        DECLARATION, NO_DECLARATION
    }

    private static Document newDocument(Element root) {
        return new Document(root);
    }

    private void saveDocument(String path, String filename, XML mode, Document document) {
        if (path != null) {
            filename = fileJoin(path, filename);
        }

        if (dryRun) {
            stderr("Writing %s\n", filename);
            return;
        }

        Format prettyFormat = Format.getPrettyFormat();
        prettyFormat.setOmitDeclaration(mode == XML.NO_DECLARATION);
        XMLOutputter outputter = new XMLOutputter(prettyFormat);
        try (Writer writer = new FileWriter(filename)) {
            outputter.output(document, writer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void saveDocument(String path, String filename, XML mode, Element root) {
        saveDocument(path, filename, mode, newDocument(root));
    }

    private static Element addElement(Element parent, String name, Attribute... attributes) {
        Element child = newElement(name, attributes);
        parent.addContent(child);
        return child;
    }

    private static Element addElement(Element parent, String name, List<Attribute> attributes) {
        return addElement(parent, name, attributes.toArray(new Attribute[attributes.size()]));
    }

    private static Attribute attribute(String name, String value) {
        return new Attribute(name, value);
    }

    private static Attribute attribute(String name, Object value) {
        return attribute(name, value.toString());
    }

    private static Element newElement(String name, Attribute attribute) {
        Element element = new Element(name);
        element.setAttribute(attribute);
        return element;
    }

    private static Element newElement(String name, Attribute... attributes) {
        Element element = new Element(name);
        if (attributes != null) {
            for (Attribute attribute : attributes) {
                element.setAttribute(attribute);
            }
        }
        return element;
    }

    // endregion XML utilities

    private List<String> buildDotIdeaFolder(List<String> inputs) {
        String dotIdea = fileJoin(viewPath, DOT_IDEA);
        immediateMkdir(dotIdea);

        writeModulesXml(dotIdea);
        writeMiscXml(dotIdea);

        symlink(fileJoin(repository, DOT_IDEA, CODE_STYLE_SETTINGS), fileJoin(dotIdea, CODE_STYLE_SETTINGS));

        return buildDotIdeaDotLibrariesFolder(dotIdea, inputs);
    }

    private void writeModulesXml(String dotIdea) {
        String filepath = fileJoin("$PROJECT_DIR$", ROOT_IML);
        String fileurl = "file://" + filepath;

        Element project = newElement("project", attribute(VERSION, 4));
        Element component = addElement(project, COMPONENT, attribute(NAME, "ProjectModuleManager"));
        Element modules = addElement(component, MODULES);
        addElement(modules, "module", attribute("fileurl", fileurl), attribute("filepath", filepath),
                attribute("group", MODULES));

        saveDocument(dotIdea, "modules.xml", XML.DECLARATION, project);
    }

    private void writeMiscXml(String dotIdea) {
        Element project = newElement("project", attribute(VERSION, 4));
        addElement(project, COMPONENT, attribute(NAME, "FrameworkDetectionExcludesConfiguration"),
                attribute("detection-enabled", false));

        String languageLevel = getIntellijSectionValue(INTELLIJ_LANGUAGE_LEVEL, "JDK_1_7");
        String jdkName = getIntellijSectionValue(INTELLIJ_JDK_NAME, "Android API 23 Platform");
        String jdkType = getIntellijSectionValue(INTELLIJ_JDK_TYPE, "Android SDK");

        addElement(project, COMPONENT, attribute(NAME, "ProjectRootManager"), attribute("VERSION", 2),
                attribute("languageLevel", languageLevel), attribute("assert-keyword", true),
                attribute("jdk-15", jdk15(languageLevel)), attribute("project-jdk-name", jdkName),
                attribute("project-jdk-type", jdkType));

        saveDocument(dotIdea, "misc.xml", XML.DECLARATION, project);
    }

    // region .buckconfig wrappers

    /** All the values we currently read come from the [intellij] section of the .buckconfig file */
    private static final String INTELLIJ_SECTION = "intellij";

    // Values in the [intellij] section
    private static final String INTELLIJ_LANGUAGE_LEVEL = "language_level";
    private static final String INTELLIJ_JDK_NAME = "jdk_name";
    private static final String INTELLIJ_JDK_TYPE = "jdk_type";

    private Optional<String> getIntellijSectionValue(String propertyName) {
        return config.getValue(INTELLIJ_SECTION, propertyName);
    }

    private String getIntellijSectionValue(String propertyName, String defaultValue) {
        return getIntellijSectionValue(propertyName).orElse(defaultValue);
    }

    /**
     * Tries to parse the language level. Will always return {@code false} if the string doesn't look
     * like {@code /JDK_\d_\d/}. Otherwise, will return {@code true} iff language level {@literal >=}
     * 1.5
     */
    private static boolean jdk15(String languageLevel) {
        if (languageLevel.length() < 7 || !languageLevel.startsWith("JDK_")) {
            return false;
        }
        if (languageLevel.charAt(3) != '_' || languageLevel.charAt(5) != '_') {
            return false;
        }
        char major = languageLevel.charAt(4);
        char minor = languageLevel.charAt(6);
        if (!Character.isDigit(major) || !Character.isDigit(minor)) {
            return false;
        }

        // If we get here, languageLevel looks like /JDK_\d_\d/
        int majorValue = Character.getNumericValue(major);
        int minorValue = Character.getNumericValue(minor);
        if (majorValue < 1 || minorValue < 1) {
            // We passed the isDigit() tests, but either MIGHT be 0 ...
            return false;
        }
        if (majorValue > 1) {
            return true;
        }
        // majorValue == 1
        return minorValue >= 5;
    }

    // endregion .buckconfig wrappers

    private List<String> buildDotIdeaDotLibrariesFolder(String dotIdea, List<String> inputs) {
        String libraries = fileJoin(dotIdea, "libraries");
        immediateMkdir(libraries);

        Map<String, List<String>> directories = new HashMap<>();
        inputs.stream().filter((input) -> input.endsWith(".jar")).forEach(jar -> {
            String dirname = dirname(jar);
            String basename = basename(jar);
            List<String> basenames = directories.get(dirname);
            if (basenames == null) {
                basenames = new ArrayList<>();
                directories.put(dirname, basenames);
            }
            basenames.add(basename);
        });

        List<String> libraryXmls = new ArrayList<>();
        for (Map.Entry<String, List<String>> entry : directories.entrySet()) {
            libraryXmls.add(buildLibraryFile(libraries, entry.getKey(), entry.getValue()));
        }
        return libraryXmls;
    }

    private String buildLibraryFile(String libraries, String directory, List<String> jars) {
        String filename = "library_" + directory.replace('-', '_').replace('/', '_');
        List<String> urls = jars.stream().map((jar) -> fileJoin("jar://$PROJECT_DIR$", directory, jar) + "!/")
                .collect(Collectors.toList());

        Element component = newElement(COMPONENT, attribute(NAME, "libraryTable"));
        Element library = addElement(component, LIBRARY, attribute(NAME, filename));
        Element classes = addElement(library, "CLASSES"); // believe it or not, case matters, here!
        for (String url : urls) {
            addElement(classes, "root", attribute(URL, url));
        }
        addElement(library, "JAVADOC");
        addElement(library, "SOURCES");

        saveDocument(libraries, filename + ".xml", XML.NO_DECLARATION, component);

        return filename;
    }

    private void writeRootDotIml(List<String> sourceFiles, Set<String> roots, List<String> libraries) {
        String buckOut = fileJoin(viewPath, BUCK_OUT);
        symlink(fileJoin(repository, BUCK_OUT), buckOut);

        String apkPath = null;
        Map<BuildTarget, String> outputs = getOutputs();
        // Find the 1st target that has output
        for (BuildTarget target : buildTargets) {
            String output = outputs.get(target);
            if (output != null && output.endsWith(".apk")) {
                apkPath = File.separator + output;

                break;
            }
        }

        String manifestPath = fileJoin(File.separator, RES, ANDROID_MANIFEST);
        symlink(fileJoin(repository, ANDROID_RES, ANDROID_MANIFEST), fileJoin(viewPath, manifestPath));

        Element module = newElement("module", attribute(TYPE, "JAVA_MODULE"), attribute(VERSION, 4));

        Element facetManager = addElement(module, COMPONENT, attribute(NAME, "FacetManager"));
        Element facet = addElement(facetManager, "facet", attribute(TYPE, "android"), attribute(NAME, "Android"));

        Element configuration = addElement(facet, "configuration");

        String genFolder = fileJoin(File.separator, BUCK_OUT, "gen");
        addElement(configuration, OPTION, attribute(NAME, "GEN_FOLDER_RELATIVE_PATH_APT"),
                attribute(VALUE, genFolder));
        addElement(configuration, OPTION, attribute(NAME, "GEN_FOLDER_RELATIVE_PATH_AIDL"),
                attribute(VALUE, fileJoin(genFolder, "aidl")));

        addElement(configuration, OPTION, attribute(NAME, "MANIFEST_FILE_RELATIVE_PATH"),
                attribute(VALUE, manifestPath));
        addElement(configuration, OPTION, attribute(NAME, "RES_FOLDERS_RELATIVE_PATH"), attribute(VALUE, "/res"));
        if (apkPath != null) {
            addElement(configuration, OPTION, attribute(NAME, "APK_PATH"), attribute(VALUE, apkPath));
        }
        addElement(configuration, OPTION, attribute(NAME, "ENABLE_SOURCES_AUTOGENERATION"), attribute(VALUE, true));
        addElement(configuration, "includeAssetsFromLibraries").addContent("true");

        Element rootManager = addElement(module, COMPONENT, attribute(NAME, "NewModuleRootManager"),
                attribute("inherit-compiler-output", true));
        addElement(rootManager, "exclude-output");

        Element folders = addElement(rootManager, CONTENT, attribute(URL, FILE_MODULE_DIR));

        Set<String> sourceFolders = sourceFiles.stream().map((folder) -> dirname(folder))
                .collect(Collectors.toSet());
        sourceFolders.remove(null);

        for (String source : sortSourceFolders(sourceFolders)) {
            List<Attribute> attributes = new ArrayList<>(3);
            attributes.add(attribute(URL, fileJoin(FILE_MODULE_DIR, source)));
            attributes.add(attribute(IS_TEST_SOURCE, false));

            String packagePrefix = getPackage(fileJoin(repository, source));
            if (packagePrefix != null) {
                attributes.add(attribute("packagePrefix", packagePrefix));
            }
            addElement(folders, SOURCE_FOLDER, attributes);
        }

        for (String excluded : getExcludedFolders(sourceFolders, roots)) {
            addElement(folders, EXCLUDE_FOLDER, attribute(URL, fileJoin(FILE_MODULE_DIR, excluded)));
        }

        addElement(rootManager, ORDER_ENTRY, attribute(TYPE, "inheritedJdk"));
        addElement(rootManager, ORDER_ENTRY, attribute(TYPE, SOURCE_FOLDER), attribute("forTests", false));

        for (String library : libraries) {
            addElement(rootManager, ORDER_ENTRY, attribute(TYPE, LIBRARY), attribute(NAME, library),
                    attribute("level", "project"));
        }

        for (String relativeFolder : getAnnotationAndGeneratedFolders()) {
            String folder = fileJoin(FILE_MODULE_DIR, relativeFolder);
            Attribute url = attribute(URL, folder);
            Element content = addElement(rootManager, CONTENT, url);
            addElement(content, SOURCE_FOLDER, url.clone(), attribute(IS_TEST_SOURCE, false),
                    attribute("generated", true));
        }

        saveDocument(viewPath, ROOT_IML, XML.DECLARATION, module);
    }

    private Map<BuildTarget, String> getOutputs() {
        Map<BuildTarget, String> outputs = new HashMap<>(buildTargets.size());

        BuildRuleResolver ruleResolver = actionGraph.getResolver();
        SourcePathResolver pathResolver = new SourcePathResolver(new SourcePathRuleFinder(ruleResolver));

        for (BuildTarget target : buildTargets) {
            BuildRule rule = ruleResolver.getRule(target);
            SourcePath sourcePathToOutput = rule.getSourcePathToOutput();
            if (sourcePathToOutput == null) {
                continue;
            }
            Path outputPath = pathResolver.getRelativePath(sourcePathToOutput);
            outputs.put(target, outputPath.toString());
        }

        return outputs;
    }

    private List<String> sortSourceFolders(Set<String> sourceFolders) {
        return sourceFolders.stream().sorted().collect(Collectors.toList());
    }

    private Set<String> getExcludedFolders(Set<String> sourceFolders, Set<String> roots) {
        Set<String> rootFolders = allFoldersUnder(roots);

        // Remove any folder that's explicitly a source folder
        rootFolders.removeAll(sourceFolders);

        // Remove any folder that's the parent of a source folder. (IntelliJ can handle a sourceFolder
        // under an excludeFolder; Android Studio can not.) This is a quadratic operation, but in
        // practice only adds a couple of seconds on a large project
        rootFolders = rootFolders.stream().filter(root -> {
            String probe = guaranteeEndsWithFileSeparator(root);
            return !sourceFolders.stream().anyMatch(source -> source.startsWith(probe));
        }).collect(Collectors.toSet());

        return rootFolders;
    }

    private static Set<String> allFoldersUnder(Set<String> roots) {
        Set<String> result = new HashSet<>();
        for (String root : roots) {
            result.addAll(foldersUnder(root));
        }
        return result;
    }

    private static Set<String> foldersUnder(String root) {
        // TODO(shemitz) This really should use Files.find() ...
        Set<String> result = new HashSet<>();
        File directory = new File(root);
        if (directory.isDirectory()) {
            String[] children = directory.list(neitherDotOrDotDot);
            if (children != null) {
                for (String child : children) {
                    String qualified = fileJoin(root, child);
                    if (isDirectory(qualified)) {
                        result.add(qualified);
                        result.addAll(foldersUnder(qualified));
                    }
                }
            }
        }
        return result;
    }

    private static final Pattern PACKAGE = Pattern.compile("package\\s+([\\w\\.]+);");

    @Nullable
    private static String getPackage(String path) {
        File folder = new File(path);
        File[] files = folder.listFiles((child) -> child.isFile() && child.getName().endsWith(".java"));
        if (files != null) {
            for (File file : files) {
                String text;
                try {
                    text = new String(Files.readAllBytes(file.toPath()));
                } catch (IOException e) {
                    continue;
                }
                Matcher matcher = PACKAGE.matcher(text);
                if (matcher.find()) {
                    return matcher.group(1);
                }
            }
        }
        return null;
    }

    private Collection<String> getAnnotationAndGeneratedFolders() {
        Collection<String> folders = new HashSet<>();

        getAnnotationFolders(folders);
        getGeneratedFolders(folders);

        return folders.stream().sorted().collect(Collectors.toList());
    }

    private void getAnnotationFolders(Collection<String> folders) {
        for (BuildRule buildRule : actionGraph.getActionGraph().getNodes()) {
            if (buildRule instanceof JavaLibrary) {
                Optional<Path> generatedSourcePath = ((JavaLibrary) buildRule).getGeneratedSourcePath();
                if (generatedSourcePath.isPresent()) {
                    folders.add(generatedSourcePath.get().toString());
                }
            }
        }
    }

    private void getGeneratedFolders(Collection<String> folders) {
        Map<String, String> labelToGeneratedSourcesMap = config.getMap(INTELLIJ_SECTION,
                "generated_sources_label_map");
        Pattern name = Pattern.compile("%name%");

        AbstractBreadthFirstTraversal.<TargetNode<?, ?>>traverse(targetGraph.getAll(allTargets), node -> {
            ProjectFilesystem filesystem = node.getFilesystem();
            Set<BuildTarget> buildDeps = node.getBuildDeps();
            for (BuildTarget buildTarget : buildDeps) {
                Object constructorArg = node.getConstructorArg();
                if (constructorArg instanceof CommonDescriptionArg) {
                    CommonDescriptionArg commonDescriptionArg = (CommonDescriptionArg) constructorArg;
                    folders.addAll(commonDescriptionArg.getLabels().stream().map(labelToGeneratedSourcesMap::get)
                            .filter(Objects::nonNull)
                            .map(pattern -> name.matcher(pattern)
                                    .replaceAll(buildTarget.getShortNameAndFlavorPostfix()))
                            .map((String path) -> BuildTargets.getGenPath(filesystem, buildTarget, path).toString())
                            .collect(Collectors.toSet()));
                }
            }
            return targetGraph.getAll(buildDeps);
        });
    }

    // endregion .idea folder

    // region symlinks, mkdir, and other file utilities

    /**
     * This is <em>not</em> all directories in the view; this is all 'terminals' that have symlinks.
     * That is, if we have {@code foo/bar/baz/link}, we will record {@code foo/bar/baz} but not {@code
     * foo/bar} or {@code foo}.
     */
    private final Set<Path> existingDirectories = new HashSet<>();
    /** basefile -> link */
    private final Map<Path, Path> existingSymlinks = new HashMap<>();

    private final Set<Path> directoriesToMake = new HashSet<>();
    /** basefile -> link */
    private final Map<Path, Path> symlinksToCreate = new HashMap<>();

    private void scanExistingView() {
        Path root = Paths.get(viewPath);
        if (!Files.exists(root)) {
            return;
        }
        try {
            Files.find(root, Integer.MAX_VALUE, (Path ignored,
                    BasicFileAttributes attributes) -> attributes.isDirectory() || attributes.isSymbolicLink())
                    .forEach(path -> {
                        if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
                            if (hasSymbolicLink(path)) {
                                existingDirectories.add(path.toAbsolutePath());
                            }
                        } else if (Files.isSymbolicLink(path)) {
                            try {
                                existingSymlinks.put(Files.readSymbolicLink(path), path);
                            } catch (IOException e) {
                                stderr("'%s' reading %s", e.getMessage(), path);
                            }
                        }
                    });
        } catch (IOException e) {
            stderr("'%s' scanning the existing links and directories\n", e.getMessage());
        }
    }

    private boolean hasSymbolicLink(Path path) {
        try {
            return Files.list(path).anyMatch(p -> Files.isSymbolicLink(p));
        } catch (IOException e) {
            stderr("'%s' enumerating %s", e.getMessage(), path);
            return true;
        }
    }

    private void buildAllDirectoriesAndSymlinks() {
        Set<Path> deletedDirectories = new HashSet<>();

        // Delete any directories that should no longer exist
        for (Path path : existingDirectories) {
            if (!directoriesToMake.contains(path)) {
                if (dryRun) {
                    stderr("rm -rf %s\n", path);
                } else {
                    deleteAll(path);
                    deletedDirectories.add(path);
                }
            }
        }

        // Delete any symlinks that should no longer exist; remove existing links from Map
        existingSymlinks.forEach((filePath, linkPath) -> {
            if (linkPath.equals(symlinksToCreate.get(filePath))) {
                symlinksToCreate.remove(filePath);
            } else {
                if (dryRun) {
                    stderr("rm %s\n", linkPath);
                } else {
                    try {
                        Files.delete(linkPath);
                    } catch (IOException e) {
                        if (!linkInDeletedDirectories(deletedDirectories, linkPath)) {
                            stderr("'%s' deleting symlink %s\n", e.getMessage(), linkPath);
                        }
                    }
                }
            }
        });

        // Create any symlinks that don't already exist
        symlinksToCreate.forEach((filePath, linkPath) -> {
            if (dryRun) {
                stderr("symlink(%s, %s)\n", filePath, linkPath);
            } else {
                createSymbolicLink(filePath, linkPath);
            }
        });
    }

    private boolean linkInDeletedDirectories(Set<Path> deletedDirectories, Path linkPath) {
        Path linkDirectory = linkPath;
        while ((linkDirectory = linkDirectory.getParent()) != null) {
            if (deletedDirectories.contains(linkDirectory)) {
                return true;
            }
        }
        return false;
    }

    private static String basename(File file) {
        return file.getName();
    }

    private static String basename(String filename) {
        return basename(new File(filename));
    }

    private static String dirname(File file) {
        return file.getParent();
    }

    private static String dirname(String filename) {
        return dirname(new File(filename));
    }

    private static String fileJoin(String... components) {
        StringBuilder join = new StringBuilder();
        if (components != null) {
            for (String component : components) {
                if (needSeparator(join, component)) {
                    join.append(File.separatorChar);
                }
                join.append(component);
            }
        }
        return join.toString();
    }

    private static boolean needSeparator(StringBuilder join, String next) {
        int length = join.length();
        if (length == 0) {
            return false;
        }
        if (join.charAt(length - 1) == File.separatorChar) {
            return false;
        }
        return !next.startsWith(File.separator);
    }

    private void mkdir(String name) {
        directoriesToMake.add(Paths.get(name));
    }

    private void immediateMkdir(String path) {
        immediateMkdir(Paths.get(path));
    }

    private void immediateMkdir(Path path) {
        if (dryRun) {
            stderr("mkdir(%s)\n", path);
        } else {
            try {
                Files.createDirectories(path);
            } catch (IOException e) {
                stderr("'%s' creating directory %s\n", e.getMessage(), path);
            }
        }
    }

    private void symlink(String filename, String linkname) {
        File link = new File(linkname);
        Path linkPath = link.toPath();
        mkdir(dirname(link));

        Path filePath = Paths.get(filename);

        symlinksToCreate.put(filePath, linkPath);
    }

    /** Parameter order is compatible with Ruby library code, for porting transparency */
    private void createSymbolicLink(Path oldPath, Path newPath) {
        Path directory = newPath.getParent();
        if (directory != null && Files.notExists(directory)) {
            immediateMkdir(directory);
        }

        try {
            Files.createSymbolicLink(newPath, oldPath);
        } catch (IOException e) {
            stderr("createSymbolicLink(%s, %s)\n%s:\n%s\n\n", oldPath, newPath, e.getClass().getSimpleName(),
                    e.getMessage());
        }
    }

    private void deleteAll(Path root) {
        try {
            Files.walk(root).sorted(Comparator.reverseOrder()) // foo/bar before foo
                    .forEach(p -> {
                        try {
                            Files.delete(p);
                        } catch (IOException e) {
                            stderr("'%s' deleting %s\n", e.getMessage(), p);
                        }
                    });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static boolean isDirectory(String name) {
        File file = new File(name);
        return file.isDirectory();
    }

    private static final FilenameFilter neitherDotOrDotDot = new FilenameFilter() {
        @Override
        public boolean accept(File dir, String name) {
            return !(name.equals(".") || name.equals(".."));
        }
    };

    private static String guaranteeEndsWithFileSeparator(String name) {
        return name.endsWith(File.separator) ? name : name + File.separator;
    }

    // endregion symlinks, mkdir, and other file utilities

    // region Console IO

    private void stderr(String pattern, Object... parameters) {
        stdErr.format(pattern, parameters);
    }

    // endregion Console IO

    // region Random crap

    private static boolean isNullOrEmpty(String s) {
        return s == null || s.isEmpty();
    }

    // endregion Random crap

    // endregion Private implementation
}