com.facebook.buck.features.project.intellij.IjProjectTemplateDataPreparer.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.features.project.intellij.IjProjectTemplateDataPreparer.java

Source

/*
 * Copyright 2015-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.features.project.intellij;

import static com.facebook.buck.features.project.intellij.IjProjectPaths.getUrl;

import com.facebook.buck.features.project.intellij.aggregation.AggregationMode;
import com.facebook.buck.features.project.intellij.lang.android.AndroidManifestParser;
import com.facebook.buck.features.project.intellij.lang.android.AndroidResourceFolder;
import com.facebook.buck.features.project.intellij.model.ContentRoot;
import com.facebook.buck.features.project.intellij.model.DependencyType;
import com.facebook.buck.features.project.intellij.model.IjLibrary;
import com.facebook.buck.features.project.intellij.model.IjModule;
import com.facebook.buck.features.project.intellij.model.IjModuleAndroidFacet;
import com.facebook.buck.features.project.intellij.model.IjModuleType;
import com.facebook.buck.features.project.intellij.model.IjProjectConfig;
import com.facebook.buck.features.project.intellij.model.IjProjectElement;
import com.facebook.buck.features.project.intellij.model.ModuleIndexEntry;
import com.facebook.buck.features.project.intellij.model.folders.ExcludeFolder;
import com.facebook.buck.features.project.intellij.model.folders.IjFolder;
import com.facebook.buck.features.project.intellij.model.folders.IjResourceFolderType;
import com.facebook.buck.features.project.intellij.model.folders.IjSourceFolder;
import com.facebook.buck.features.project.intellij.model.folders.ResourceFolder;
import com.facebook.buck.features.project.intellij.model.folders.TestFolder;
import com.facebook.buck.io.filesystem.ProjectFilesystem;
import com.facebook.buck.jvm.core.JavaPackageFinder;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.annotation.Nullable;

/**
 * Does the converting of abstract data structures to a format immediately consumable by the
 * StringTemplate-based templates employed by {@link IjProjectWriter}. This is a separate class
 * mainly for testing convenience.
 */
@VisibleForTesting
public class IjProjectTemplateDataPreparer {
    private static final String ANDROID_MANIFEST_TEMPLATE_PARAMETER = "android_manifest";
    private static final String APK_PATH_TEMPLATE_PARAMETER = "apk_path";
    private static final String ASSETS_FOLDER_TEMPLATE_PARAMETER = "asset_folder";
    private static final String PROGUARD_CONFIG_TEMPLATE_PARAMETER = "proguard_config";
    private static final String RESOURCES_RELATIVE_PATH_TEMPLATE_PARAMETER = "res";

    private static final String EMPTY_STRING = "";

    private final JavaPackageFinder javaPackageFinder;
    private final IjModuleGraph moduleGraph;
    private final ProjectFilesystem projectFilesystem;
    private final IjProjectConfig projectConfig;
    private final IjProjectPaths projectPaths;
    private final IjSourceRootSimplifier sourceRootSimplifier;
    private final AndroidManifestParser androidManifestParser;
    private final ImmutableSet<Path> referencedFolderPaths;
    private final ImmutableSet<Path> filesystemTraversalBoundaryPaths;
    private final ImmutableSet<IjModule> modulesToBeWritten;
    private final ImmutableSet<IjLibrary> librariesToBeWritten;

    public IjProjectTemplateDataPreparer(JavaPackageFinder javaPackageFinder, IjModuleGraph moduleGraph,
            ProjectFilesystem projectFilesystem, IjProjectConfig projectConfig,
            AndroidManifestParser androidManifestParser) {
        this.javaPackageFinder = javaPackageFinder;
        this.moduleGraph = moduleGraph;
        this.projectFilesystem = projectFilesystem;
        this.projectConfig = projectConfig;
        this.projectPaths = projectConfig.getProjectPaths();
        this.sourceRootSimplifier = new IjSourceRootSimplifier(javaPackageFinder);
        this.modulesToBeWritten = createModulesToBeWritten(moduleGraph);
        this.librariesToBeWritten = moduleGraph.getLibraries();
        this.androidManifestParser = androidManifestParser;
        this.filesystemTraversalBoundaryPaths = createFilesystemTraversalBoundaryPathSet(modulesToBeWritten);
        this.referencedFolderPaths = createReferencedFolderPathsSet(modulesToBeWritten);
    }

    private static void addPathAndParents(Set<Path> pathSet, Path path) {
        do {
            pathSet.add(path);
            path = path.getParent();
        } while (path != null && !pathSet.contains(path));
    }

    public static ImmutableSet<Path> createReferencedFolderPathsSet(ImmutableSet<IjModule> modules) {
        Set<Path> pathSet = new HashSet<>();
        for (IjModule module : modules) {
            addPathAndParents(pathSet, module.getModuleBasePath());
            for (IjFolder folder : module.getFolders()) {
                addPathAndParents(pathSet, folder.getPath());
            }
        }
        return ImmutableSet.copyOf(pathSet);
    }

    public ImmutableSet<Path> createFilesystemTraversalBoundaryPathSet(ImmutableSet<IjModule> modules) {
        return Stream.concat(modules.stream().map(IjModule::getModuleBasePath),
                Stream.of(projectPaths.getIdeaConfigDir())).collect(ImmutableSet.toImmutableSet());
    }

    public static ImmutableSet<Path> createPackageLookupPathSet(IjModuleGraph moduleGraph) {
        ImmutableSet.Builder<Path> builder = ImmutableSet.builder();

        for (IjModule module : moduleGraph.getModules()) {
            for (IjFolder folder : module.getFolders()) {
                if (!folder.getWantsPackagePrefix()) {
                    continue;
                }
                Optional<Path> firstJavaFile = folder.getInputs().stream()
                        .filter(input -> input.getFileName().toString().endsWith(".java")).findFirst();
                if (firstJavaFile.isPresent()) {
                    builder.add(firstJavaFile.get());
                }
            }
        }

        return builder.build();
    }

    private ImmutableSet<IjModule> createModulesToBeWritten(IjModuleGraph graph) {
        Path rootModuleBasePath = Paths.get(projectConfig.getProjectRoot());
        boolean hasRootModule = graph.getModules().stream()
                .anyMatch(module -> rootModuleBasePath.equals(module.getModuleBasePath()));

        ImmutableSet<IjModule> supplementalModules = ImmutableSet.of();
        if (!hasRootModule) {
            supplementalModules = ImmutableSet.of(IjModule.builder().setModuleBasePath(rootModuleBasePath)
                    .setTargets(ImmutableSet.of()).setModuleType(IjModuleType.UNKNOWN_MODULE).build());
        }

        return Stream.concat(graph.getModules().stream(), supplementalModules.stream())
                .collect(ImmutableSet.toImmutableSet());
    }

    public ImmutableSet<IjModule> getModulesToBeWritten() {
        return modulesToBeWritten;
    }

    public ImmutableSet<IjLibrary> getLibrariesToBeWritten() {
        return librariesToBeWritten;
    }

    private ImmutableList<ContentRoot> createContentRoots(IjModule module, ImmutableCollection<IjFolder> folders) {
        Path contentRootPath = module.getModuleBasePath();
        ImmutableListMultimap<Path, IjFolder> simplifiedFolders = sourceRootSimplifier.simplify(
                contentRootPath.toString().isEmpty() ? 0 : contentRootPath.getNameCount(), folders, contentRootPath,
                filesystemTraversalBoundaryPaths);

        IjFolderToIjSourceFolderTransform transformToFolder = new IjFolderToIjSourceFolderTransform(module);
        Map<String, List<IjSourceFolder>> sources = Maps.newTreeMap();
        sources.put(contentRootPath.toString(), Collections.emptyList());
        simplifiedFolders.asMap().forEach((contentRoot, contentRootFolders) -> {
            List<IjSourceFolder> sourceFolders = contentRootFolders.stream().map(transformToFolder).sorted()
                    .collect(ImmutableList.toImmutableList());
            sources.put(contentRoot.toString(), sourceFolders);
        });
        ImmutableList.Builder<ContentRoot> contentRootsBuilder = ImmutableList.builder();
        for (Map.Entry<String, List<IjSourceFolder>> entry : sources.entrySet()) {
            String url = getUrl(projectPaths.getModuleQualifiedPath(Paths.get(entry.getKey()), module));
            contentRootsBuilder.add(ContentRoot.builder().setUrl(url).setFolders(entry.getValue()).build());
        }
        return contentRootsBuilder.build();
    }

    public ImmutableCollection<IjFolder> createExcludes(IjModule module) throws IOException {
        Path moduleBasePath = module.getModuleBasePath();
        if (!projectFilesystem.exists(moduleBasePath)) {
            return ImmutableList.of();
        }
        ImmutableList.Builder<IjFolder> excludesBuilder = ImmutableList.builder();
        projectFilesystem.walkRelativeFileTree(moduleBasePath, new FileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                // This is another module that's nested in this one. The entire subtree will be handled
                // When we create excludes for that module.
                if (filesystemTraversalBoundaryPaths.contains(dir) && !moduleBasePath.equals(dir)) {
                    return FileVisitResult.SKIP_SUBTREE;
                }

                if (isRootAndroidResourceDirectory(module, dir)) {
                    return FileVisitResult.SKIP_SUBTREE;
                }

                if (!referencedFolderPaths.contains(dir)) {
                    excludesBuilder.add(new ExcludeFolder(dir));
                    return FileVisitResult.SKIP_SUBTREE;
                }

                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) {
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
                return FileVisitResult.CONTINUE;
            }
        }, false);
        return excludesBuilder.build();
    }

    private boolean isRootAndroidResourceDirectory(IjModule module, Path dir) {
        if (!module.getAndroidFacet().isPresent()) {
            return false;
        }

        for (Path resourcePath : module.getAndroidFacet().get().getResourcePaths()) {
            if (dir.equals(resourcePath)) {
                return true;
            }
        }

        return false;
    }

    public ImmutableList<ContentRoot> getContentRoots(IjModule module) throws IOException {
        ImmutableList<IjFolder> sourcesAndExcludes = Stream
                .concat(module.getFolders().stream(), createExcludes(module).stream()).sorted()
                .collect(ImmutableList.toImmutableList());
        return createContentRoots(module, sourcesAndExcludes);
    }

    public ImmutableSet<IjSourceFolder> getGeneratedSourceFolders(IjModule module) {
        return module.getGeneratedSourceCodeFolders().stream().map(new IjFolderToIjSourceFolderTransform(module))
                .collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural()));
    }

    public ImmutableSet<DependencyEntry> getDependencies(IjModule module) {
        ImmutableMap<IjProjectElement, DependencyType> deps = moduleGraph.getDepsFor(module);
        IjDependencyListBuilder dependencyListBuilder = new IjDependencyListBuilder();

        for (Map.Entry<IjProjectElement, DependencyType> entry : deps.entrySet()) {
            IjProjectElement element = entry.getKey();
            DependencyType dependencyType = entry.getValue();
            element.addAsDependency(dependencyType, dependencyListBuilder);
        }
        return dependencyListBuilder.build();
    }

    public Optional<String> getFirstResourcePackageFromDependencies(IjModule module) {
        ImmutableMap<IjModule, DependencyType> deps = moduleGraph.getDependentModulesFor(module);
        for (IjModule dep : deps.keySet()) {
            Optional<IjModuleAndroidFacet> facet = dep.getAndroidFacet();
            if (facet.isPresent()) {
                Optional<String> packageName = facet.get().getPackageName();
                if (packageName.isPresent()) {
                    return packageName;
                }
            }
        }
        return Optional.empty();
    }

    public ImmutableSortedSet<ModuleIndexEntry> getModuleIndexEntries() {
        String moduleGroupName = projectConfig.getModuleGroupName();
        boolean needToPutModuleToGroup = !moduleGroupName.isEmpty();
        return modulesToBeWritten.stream().map(module -> {
            Path moduleOutputFilePath = projectPaths.getModuleImlFilePath(module);
            String fileUrl = getUrl(projectPaths.getProjectQualifiedPath(moduleOutputFilePath));
            Path moduleOutputFileRelativePath = projectPaths.getProjectRelativePath(moduleOutputFilePath);
            // The root project module cannot belong to any group.
            String group = (module.getModuleBasePath().toString().isEmpty() || !needToPutModuleToGroup) ? null
                    : moduleGroupName;
            return ModuleIndexEntry.builder().setFileUrl(fileUrl).setFilePath(moduleOutputFileRelativePath)
                    .setGroup(group).build();
        }).collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural()));
    }

    public Map<String, Object> getAndroidProperties(IjModule module) {
        Map<String, Object> androidProperties = new HashMap<>();
        Optional<IjModuleAndroidFacet> androidFacetOptional = module.getAndroidFacet();

        boolean isAndroidFacetPresent = androidFacetOptional.isPresent();
        androidProperties.put("enabled", isAndroidFacetPresent);

        Path basePath = module.getModuleBasePath();

        Optional<Path> extraCompilerOutputPath = projectConfig.getExtraCompilerOutputModulesPath();
        if (isAndroidFacetPresent || (extraCompilerOutputPath.isPresent()
                && basePath.toString().contains(extraCompilerOutputPath.get().toString()))) {
            addAndroidCompilerOutputPath(androidProperties, module);
        }

        if (!isAndroidFacetPresent) {
            return androidProperties;
        }

        IjModuleAndroidFacet androidFacet = androidFacetOptional.get();

        androidProperties.put("is_android_library_project", androidFacet.isAndroidLibrary());
        androidProperties.put("project_type", androidFacet.getAndroidProjectType().getId());
        androidProperties.put("autogenerate_sources", androidFacet.autogenerateSources());
        androidProperties.put("disallow_user_configuration",
                projectConfig.isAggregatingAndroidResourceModulesEnabled()
                        && projectConfig.getAggregationMode() != AggregationMode.NONE);

        addAndroidApkPaths(androidProperties, module, androidFacet);
        addAndroidAssetPaths(androidProperties, module, androidFacet);
        addAndroidGenPath(androidProperties, androidFacet, module);
        addAndroidManifestPath(androidProperties, module, androidFacet);
        addAndroidProguardPath(androidProperties, androidFacet);
        addAndroidResourcePaths(androidProperties, module, androidFacet);

        return androidProperties;
    }

    private void addAndroidApkPaths(Map<String, Object> androidProperties, IjModule module,
            IjModuleAndroidFacet androidFacet) {
        if (androidFacet.isAndroidLibrary()) {
            return;
        }

        Path apkPath = Paths.get(IjAndroidHelper.getAndroidApkDir(projectFilesystem))
                .resolve(module.getModuleBasePath()).resolve(module.getName() + ".apk");
        androidProperties.put(APK_PATH_TEMPLATE_PARAMETER, projectPaths.getModuleRelativePath(apkPath, module));
    }

    private void addAndroidAssetPaths(Map<String, Object> androidProperties, IjModule module,
            IjModuleAndroidFacet androidFacet) {
        if (androidFacet.isAndroidLibrary()) {
            return;
        }
        ImmutableSet<Path> assetPaths = androidFacet.getAssetPaths();
        if (assetPaths.isEmpty()) {
            return;
        }
        Set<Path> relativeAssetPaths = new HashSet<>(assetPaths.size());
        for (Path assetPath : assetPaths) {
            relativeAssetPaths.add(projectPaths.getModuleRelativePath(assetPath, module));
        }
        androidProperties.put(ASSETS_FOLDER_TEMPLATE_PARAMETER, "/" + Joiner.on(";/").join(relativeAssetPaths));
    }

    private void addAndroidGenPath(Map<String, Object> androidProperties, IjModuleAndroidFacet androidFacet,
            IjModule module) {

        androidProperties.put("module_gen_path", IjProjectPaths.getAndroidFacetRelativePath(
                projectPaths.getModuleRelativePath(androidFacet.getGeneratedSourcePath(), module)));
        androidProperties.put("module_gen_url",
                getUrl(projectPaths.getModuleQualifiedPath(androidFacet.getGeneratedSourcePath(), module)));
    }

    private void addAndroidManifestPath(Map<String, Object> androidProperties, IjModule module,
            IjModuleAndroidFacet androidFacet) {
        Optional<Path> androidManifestPath = getAndroidManifestPath(androidFacet);

        if (!androidManifestPath.isPresent()) {
            return;
        }

        Path manifestPath = projectPaths
                .getModuleRelativePath(projectPaths.getProjectRelativePath(androidManifestPath.get()), module);

        if (!"AndroidManifest.xml".equals(manifestPath.toString())) {
            androidProperties.put(ANDROID_MANIFEST_TEMPLATE_PARAMETER,
                    IjProjectPaths.getAndroidFacetRelativePath(manifestPath));
        }
    }

    private Optional<Path> getAndroidManifestPath(IjModuleAndroidFacet androidFacet) {
        if (projectConfig.isGeneratingAndroidManifestEnabled()) {
            Optional<String> packageName = androidFacet.discoverPackageName(androidManifestParser);
            if (packageName.isPresent()) {
                return Optional.of(androidFacet.getGeneratedSourcePath()
                        .resolve(packageName.get().replace('.', '/')).resolve("AndroidManifest.xml"));
            }
        }

        Optional<Path> firstManifest = androidFacet.getFirstManifestPath();
        if (firstManifest.isPresent()) {
            return firstManifest;
        } else {
            return projectConfig.getAndroidManifest();
        }
    }

    private void addAndroidProguardPath(Map<String, Object> androidProperties, IjModuleAndroidFacet androidFacet) {
        androidFacet.getProguardConfigPath()
                .ifPresent(proguardPath -> androidProperties.put(PROGUARD_CONFIG_TEMPLATE_PARAMETER, proguardPath));
    }

    private void addAndroidResourcePaths(Map<String, Object> androidProperties, IjModule module,
            IjModuleAndroidFacet androidFacet) {
        ImmutableSet<Path> resourcePaths = androidFacet.getResourcePaths();
        if (resourcePaths.isEmpty()) {
            androidProperties.put(RESOURCES_RELATIVE_PATH_TEMPLATE_PARAMETER, EMPTY_STRING);
        } else {
            Set<String> relativeResourcePaths = new HashSet<>(resourcePaths.size());
            for (Path resourcePath : resourcePaths) {
                relativeResourcePaths
                        .add(IjProjectPaths.toRelativeString(resourcePath, module.getModuleBasePath()));
            }

            androidProperties.put(RESOURCES_RELATIVE_PATH_TEMPLATE_PARAMETER,
                    Joiner.on(";").join(relativeResourcePaths));
        }
    }

    /**
     * IntelliJ may not be able to find classes on the compiler output path if the jars are retrieved
     * from the network cache.
     */
    private void addAndroidCompilerOutputPath(Map<String, Object> androidProperties, IjModule module) {
        // The compiler output path is relative to the project root
        Optional<Path> compilerOutputPath = module.getCompilerOutputPath();
        if (compilerOutputPath.isPresent()) {
            androidProperties.put("compiler_output_path",
                    getUrl(projectPaths.getModuleQualifiedPath(compilerOutputPath.get(), module)));
        }
    }

    private class IjFolderToIjSourceFolderTransform implements Function<IjFolder, IjSourceFolder> {
        private IjModule module;
        private Optional<IjModuleAndroidFacet> androidFacet;

        IjFolderToIjSourceFolderTransform(IjModule module) {
            this.module = module;
            androidFacet = module.getAndroidFacet();
        }

        @Override
        public IjSourceFolder apply(IjFolder input) {
            String packagePrefix;
            if (input instanceof AndroidResourceFolder && androidFacet.isPresent()
                    && androidFacet.get().getPackageName().isPresent()) {
                packagePrefix = androidFacet.get().getPackageName().get();
            } else {
                packagePrefix = getPackagePrefix(input);
            }
            return createSourceFolder(input, packagePrefix);
        }

        private IjSourceFolder createSourceFolder(IjFolder folder, @Nullable String packagePrefix) {
            Path relativeOutputPath = null;
            IjResourceFolderType ijResourceFolderType = IjResourceFolderType.JAVA_RESOURCE;
            if (folder instanceof ResourceFolder) {
                ResourceFolder resourceFolder = (ResourceFolder) folder;
                relativeOutputPath = resourceFolder.getRelativeOutputPath();
                ijResourceFolderType = resourceFolder.getResourceFolderType();
            }

            return IjSourceFolder.builder().setType(folder.getIjName())
                    .setUrl(getUrl(projectPaths.getModuleQualifiedPath(folder.getPath(), module)))
                    .setPath(projectPaths.getModuleRelativePath(folder.getPath(), module))
                    .setIsTestSource(folder instanceof TestFolder).setIsResourceFolder(folder.isResourceFolder())
                    .setIjResourceFolderType(ijResourceFolderType).setRelativeOutputPath(relativeOutputPath)
                    .setPackagePrefix(packagePrefix).build();
        }

        @Nullable
        private String getPackagePrefix(IjFolder folder) {
            if (!folder.getWantsPackagePrefix()) {
                return null;
            }
            Path fileToLookupPackageIn;
            if (!folder.getInputs().isEmpty() && folder.getInputs().first().getParent().equals(folder.getPath())) {
                fileToLookupPackageIn = folder.getInputs().first();
            } else {
                fileToLookupPackageIn = folder.getPath().resolve("notfound");
            }
            String packagePrefix = javaPackageFinder.findJavaPackage(fileToLookupPackageIn);
            if (packagePrefix.isEmpty()) {
                // It doesn't matter either way, but an empty prefix looks confusing.
                return null;
            }
            return packagePrefix;
        }
    }
}