/* * 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.ide.intellij; import com.facebook.buck.ide.intellij.lang.android.AndroidResourceFolder; import com.facebook.buck.ide.intellij.model.ContentRoot; import com.facebook.buck.ide.intellij.model.DependencyType; import com.facebook.buck.ide.intellij.model.IjLibrary; import com.facebook.buck.ide.intellij.model.IjModule; import com.facebook.buck.ide.intellij.model.IjModuleAndroidFacet; import com.facebook.buck.ide.intellij.model.IjModuleType; import com.facebook.buck.ide.intellij.model.IjProjectElement; import com.facebook.buck.ide.intellij.model.ModuleIndexEntry; import com.facebook.buck.ide.intellij.model.folders.ExcludeFolder; import com.facebook.buck.ide.intellij.model.folders.IjFolder; import com.facebook.buck.ide.intellij.model.folders.IjSourceFolder; import com.facebook.buck.ide.intellij.model.folders.TestFolder; import com.facebook.buck.io.MorePaths; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.jvm.core.JavaPackageFinder; import com.facebook.buck.util.MoreCollectors; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; 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.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; 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 JavaPackageFinder javaPackageFinder; private IjModuleGraph moduleGraph; private ProjectFilesystem projectFilesystem; private IjSourceRootSimplifier sourceRootSimplifier; private ImmutableSet<Path> referencedFolderPaths; private ImmutableSet<Path> filesystemTraversalBoundaryPaths; private ImmutableSet<IjModule> modulesToBeWritten; private ImmutableSet<IjLibrary> librariesToBeWritten; public IjProjectTemplateDataPreparer( JavaPackageFinder javaPackageFinder, IjModuleGraph moduleGraph, ProjectFilesystem projectFilesystem) { this.javaPackageFinder = javaPackageFinder; this.moduleGraph = moduleGraph; this.projectFilesystem = projectFilesystem; this.sourceRootSimplifier = new IjSourceRootSimplifier(javaPackageFinder); this.modulesToBeWritten = createModulesToBeWritten(moduleGraph); this.librariesToBeWritten = moduleGraph.getLibraries(); 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 static ImmutableSet<Path> createFilesystemTraversalBoundaryPathSet( ImmutableSet<IjModule> modules) { return Stream.concat( modules.stream().map(IjModule::getModuleBasePath), Stream.of(IjProjectPaths.IDEA_CONFIG_DIR)) .collect(MoreCollectors.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 static ImmutableSet<IjModule> createModulesToBeWritten(IjModuleGraph graph) { Path rootModuleBasePath = Paths.get(""); 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(MoreCollectors.toImmutableSet()); } public ImmutableSet<IjModule> getModulesToBeWritten() { return modulesToBeWritten; } public ImmutableSet<IjLibrary> getLibrariesToBeWritten() { return librariesToBeWritten; } private ContentRoot createContentRoot( final IjModule module, Path contentRootPath, ImmutableSet<IjFolder> folders, final Path moduleLocationBasePath) { String url = IjProjectPaths.toModuleDirRelativeString(contentRootPath, moduleLocationBasePath); ImmutableSet<IjFolder> simplifiedFolders = sourceRootSimplifier.simplify(contentRootPath.getNameCount(), folders); IjFolderToIjSourceFolderTransform transformToFolder = new IjFolderToIjSourceFolderTransform(module); ImmutableSortedSet<IjSourceFolder> sourceFolders = simplifiedFolders .stream() .map(transformToFolder::apply) .collect(MoreCollectors.toImmutableSortedSet(Ordering.natural())); return ContentRoot.builder().setUrl(url).setFolders(sourceFolders).build(); } public ImmutableSet<IjFolder> createExcludes(final IjModule module) throws IOException { final ImmutableSet.Builder<IjFolder> excludesBuilder = ImmutableSet.builder(); final Path moduleBasePath = module.getModuleBasePath(); projectFilesystem.walkRelativeFileTree( moduleBasePath, new FileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { // 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) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } }); 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 ContentRoot getContentRoot(IjModule module) throws IOException { Path moduleBasePath = module.getModuleBasePath(); Path moduleLocation = module.getModuleImlFilePath(); final Path moduleLocationBasePath = (moduleLocation.getParent() == null) ? Paths.get("") : moduleLocation.getParent(); ImmutableSet<IjFolder> sourcesAndExcludes = Stream.concat(module.getFolders().stream(), createExcludes(module).stream()) .collect(MoreCollectors.toImmutableSortedSet()); return createContentRoot(module, moduleBasePath, sourcesAndExcludes, moduleLocationBasePath); } public ImmutableSet<IjSourceFolder> getGeneratedSourceFolders(final IjModule module) { return module .getGeneratedSourceCodeFolders() .stream() .map(new IjFolderToIjSourceFolderTransform(module)::apply) .collect(MoreCollectors.toImmutableSortedSet()); } 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() { return modulesToBeWritten .stream() .map( module -> { Path moduleOutputFilePath = module.getModuleImlFilePath(); String fileUrl = IjProjectPaths.toProjectDirRelativeString(moduleOutputFilePath); // The root project module cannot belong to any group. String group = (module.getModuleBasePath().toString().isEmpty()) ? null : "modules"; return ModuleIndexEntry.builder() .setFileUrl(fileUrl) .setFilePath(moduleOutputFilePath) .setGroup(group) .build(); }) .collect(MoreCollectors.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); if (!isAndroidFacetPresent) { return androidProperties; } IjModuleAndroidFacet androidFacet = androidFacetOptional.get(); androidProperties.put("is_android_library_project", androidFacet.isAndroidLibrary()); androidProperties.put("autogenerate_sources", androidFacet.autogenerateSources()); Path basePath = module.getModuleBasePath(); addAndroidApkPaths(androidProperties, module, basePath, androidFacet); addAndroidAssetPaths(androidProperties, module, androidFacet); addAndroidGenPath(androidProperties, androidFacet, basePath); addAndroidManifestPath(androidProperties, basePath, androidFacet); addAndroidProguardPath(androidProperties, androidFacet); addAndroidResourcePaths(androidProperties, module, androidFacet); return androidProperties; } private void addAndroidApkPaths( Map<String, Object> androidProperties, IjModule module, Path moduleBasePath, IjModuleAndroidFacet androidFacet) { if (androidFacet.isAndroidLibrary()) { return; } Path apkPath = moduleBasePath .relativize(Paths.get("")) .resolve(IjAndroidHelper.getAndroidApkDir(projectFilesystem)) .resolve(Paths.get("").relativize(moduleBasePath)) .resolve(module.getName() + ".apk"); androidProperties.put(APK_PATH_TEMPLATE_PARAMETER, apkPath); } 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()); Path moduleBase = module.getModuleBasePath(); for (Path assetPath : assetPaths) { relativeAssetPaths.add(moduleBase.relativize(assetPath)); } androidProperties.put( ASSETS_FOLDER_TEMPLATE_PARAMETER, "/" + Joiner.on(";/").join(relativeAssetPaths)); } private void addAndroidGenPath( Map<String, Object> androidProperties, IjModuleAndroidFacet androidFacet, Path moduleBasePath) { Path genPath = moduleBasePath.relativize(androidFacet.getGeneratedSourcePath()); androidProperties.put("module_gen_path", "/" + MorePaths.pathWithUnixSeparators(genPath)); } private void addAndroidManifestPath( Map<String, Object> androidProperties, Path moduleBasePath, IjModuleAndroidFacet androidFacet) { Optional<Path> androidManifestPath = androidFacet.getManifestPath(); Path manifestPath; if (androidManifestPath.isPresent()) { manifestPath = projectFilesystem.resolve(moduleBasePath).relativize(androidManifestPath.get()); } else { manifestPath = moduleBasePath.relativize(Paths.get("").resolve("android_res/AndroidManifest.xml")); } if (!"AndroidManifest.xml".equals(manifestPath.toString())) { androidProperties.put(ANDROID_MANIFEST_TEMPLATE_PARAMETER, "/" + manifestPath); } } 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<Path> relativeResourcePaths = new HashSet<>(resourcePaths.size()); Path moduleBase = module.getModuleBasePath(); for (Path resourcePath : resourcePaths) { relativeResourcePaths.add(moduleBase.relativize(resourcePath)); } androidProperties.put( RESOURCES_RELATIVE_PATH_TEMPLATE_PARAMETER, "/" + Joiner.on(";/").join(relativeResourcePaths)); } } private class IjFolderToIjSourceFolderTransform implements Function<IjFolder, IjSourceFolder> { private Path moduleBasePath; private Optional<IjModuleAndroidFacet> androidFacet; IjFolderToIjSourceFolderTransform(IjModule module) { moduleBasePath = module.getModuleBasePath(); 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, moduleBasePath, packagePrefix); } private IjSourceFolder createSourceFolder( IjFolder folder, Path moduleLocationBasePath, @Nullable String packagePrefix) { return IjSourceFolder.builder() .setType(folder.getIjName()) .setUrl( IjProjectPaths.toModuleDirRelativeString(folder.getPath(), moduleLocationBasePath)) .setIsTestSource(folder instanceof TestFolder) .setIsAndroidResources(folder instanceof AndroidResourceFolder) .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; } } }