/*
* 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.graph.MutableDirectedGraph;
import com.facebook.buck.ide.intellij.lang.java.JavaPackagePathCache;
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.SelfMergingOnlyFolder;
import com.facebook.buck.ide.intellij.model.folders.SourceFolder;
import com.facebook.buck.ide.intellij.model.folders.TestFolder;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.jvm.core.JavaPackageFinder;
import com.facebook.buck.util.MoreCollectors;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
/**
* Groups {@link IjFolder}s into sets which are of the same type and belong to the same package
* structure.
*/
public class IjSourceRootSimplifier {
private JavaPackageFinder javaPackageFinder;
public IjSourceRootSimplifier(JavaPackageFinder javaPackageFinder) {
this.javaPackageFinder = javaPackageFinder;
}
/**
* Merges {@link IjFolder}s of the same type and package prefix.
*
* @param simplificationLimit if a path has this many segments it will not be simplified further.
* @param folders set of {@link IjFolder}s to simplify.
* @return simplified set of {@link IjFolder}s.
*/
public ImmutableSet<IjFolder> simplify(int simplificationLimit, ImmutableSet<IjFolder> folders) {
PackagePathCache packagePathCache = new PackagePathCache(folders, javaPackageFinder);
BottomUpPathMerger walker =
new BottomUpPathMerger(folders, simplificationLimit, packagePathCache);
return walker.getMergedFolders();
}
private static class BottomUpPathMerger {
// Graph where edges represent the parent path -> child path relationship. We need this
// to efficiently look up children.
private MutableDirectedGraph<Path> tree;
// Keeps track of paths which actually have a folder attached to them. It's a bit simpler to
// use a map like this, especially that the folders then move up the tree as we merge them.
private Map<Path, IjFolder> mergePathsMap;
// Efficient package prefix lookup.
private PackagePathCache packagePathCache;
public BottomUpPathMerger(
Iterable<IjFolder> foldersToWalk, int limit, PackagePathCache packagePathCache) {
this.tree = new MutableDirectedGraph<>();
this.packagePathCache = packagePathCache;
this.mergePathsMap = new HashMap<>();
for (IjFolder folder : foldersToWalk) {
mergePathsMap.put(folder.getPath(), folder);
Path path = folder.getPath();
while (path.getNameCount() > limit) {
Path parent = path.getParent();
if (parent == null) {
break;
}
boolean isParentAndGrandParentAlreadyInTree = tree.containsNode(parent);
tree.addEdge(parent, path);
if (isParentAndGrandParentAlreadyInTree) {
break;
}
path = parent;
}
}
}
public ImmutableSet<IjFolder> getMergedFolders() {
for (Path topLevel : tree.getNodesWithNoIncomingEdges()) {
walk(topLevel);
}
return ImmutableSet.copyOf(mergePathsMap.values());
}
/**
* Walks the trie of paths attempting to merge all of the children of the current path into
* itself.
*
* <p>If a parent folder is present then the merge happens only for children folders that can be
* merged into a parent folder. Otherwise a parent folder is created and matching children
* folders are merged into it.
*
* @param currentPath current path
* @return Optional.of(a successfully merged folder) or absent if merging did not succeed.
*/
private Optional<IjFolder> walk(Path currentPath) {
ImmutableList<Optional<IjFolder>> children =
StreamSupport.stream(tree.getOutgoingNodesFor(currentPath).spliterator(), false)
.map(this::walk)
.collect(MoreCollectors.toImmutableList());
ImmutableSet<IjFolder> presentChildren =
children
.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(MoreCollectors.toImmutableSet());
IjFolder currentFolder = mergePathsMap.get(currentPath);
if (presentChildren.isEmpty()) {
return Optional.ofNullable(currentFolder);
}
boolean hasNonPresentChildren = presentChildren.size() != children.size();
return tryMergingParentAndChildren(
currentPath, currentFolder, presentChildren, hasNonPresentChildren);
}
/** Tries to merge children to a parent folder. */
private Optional<IjFolder> tryMergingParentAndChildren(
Path currentPath,
@Nullable IjFolder parentFolder,
ImmutableSet<IjFolder> children,
boolean hasNonPresentChildren) {
if (parentFolder == null) {
return mergeChildrenIntoNewParentFolder(currentPath, children);
}
if (parentFolder instanceof SelfMergingOnlyFolder) {
return Optional.of(parentFolder);
}
if ((parentFolder instanceof ExcludeFolder)) {
if (hasNonPresentChildren
|| children.stream().anyMatch(folder -> !ExcludeFolder.class.isInstance(folder))) {
return Optional.empty();
}
return mergeAndRemoveSimilarChildren(parentFolder, children);
}
// SourceFolder or TestFolder
if (parentFolder.getWantsPackagePrefix()) {
return mergeFoldersWithMatchingPackageIntoParent(parentFolder, children);
} else {
return mergeAndRemoveSimilarChildren(parentFolder, children);
}
}
/**
* Tries to find the best folder type to create using the types of the children.
*
* <p>The best type in this algorithm is the type with the maximum number of children.
*/
private FolderTypeWithPackageInfo findBestFolderType(ImmutableSet<IjFolder> children) {
if (children.size() == 1) {
return FolderTypeWithPackageInfo.fromFolder(children.iterator().next());
}
return children
.stream()
.collect(
Collectors.groupingBy(FolderTypeWithPackageInfo::fromFolder, Collectors.counting()))
.entrySet()
.stream()
.max(
(c1, c2) -> {
long count1 = c1.getValue();
long count2 = c2.getValue();
if (count1 == count2) {
return c2.getKey().ordinal() - c1.getKey().ordinal();
} else {
return (int) (count1 - count2);
}
})
.orElseThrow(() -> new IllegalStateException("Max count should exist"))
.getKey();
}
/**
* Creates a new parent folder and merges children into it.
*
* <p>The type of the result folder depends on the children.
*/
private Optional<IjFolder> mergeChildrenIntoNewParentFolder(
Path currentPath, ImmutableSet<IjFolder> children) {
ImmutableSet<IjFolder> childrenToMerge =
children
.stream()
.filter(
child ->
SourceFolder.class.isInstance(child) || TestFolder.class.isInstance(child))
.collect(MoreCollectors.toImmutableSet());
if (childrenToMerge.isEmpty()) {
return Optional.empty();
}
FolderTypeWithPackageInfo typeForMerging = findBestFolderType(childrenToMerge);
if (typeForMerging.wantsPackagePrefix()) {
return tryCreateNewParentFolderFromChildrenWithPackage(
typeForMerging, currentPath, childrenToMerge);
} else {
return tryCreateNewParentFolderFromChildrenWithoutPackages(
typeForMerging, currentPath, childrenToMerge);
}
}
/** Merges either SourceFolders or TestFolders without packages. */
private Optional<IjFolder> tryCreateNewParentFolderFromChildrenWithoutPackages(
FolderTypeWithPackageInfo typeForMerging,
Path currentPath,
ImmutableSet<IjFolder> children) {
Class<? extends IjFolder> folderClass = typeForMerging.getFolderTypeClass();
ImmutableSet<IjFolder> childrenToMerge =
children
.stream()
.filter(folderClass::isInstance)
.filter(folder -> !folder.getWantsPackagePrefix())
.collect(MoreCollectors.toImmutableSet());
if (childrenToMerge.isEmpty()) {
return Optional.empty();
}
IjFolder mergedFolder =
typeForMerging
.getFolderFactory()
.create(
currentPath,
false,
childrenToMerge
.stream()
.flatMap(folder -> folder.getInputs().stream())
.collect(MoreCollectors.toImmutableSortedSet()));
removeFolders(childrenToMerge);
mergePathsMap.put(currentPath, mergedFolder);
return Optional.of(mergedFolder);
}
/** Merges either SourceFolders or TestFolders with matching packages. */
private Optional<IjFolder> tryCreateNewParentFolderFromChildrenWithPackage(
FolderTypeWithPackageInfo typeForMerging,
Path currentPath,
ImmutableSet<IjFolder> children) {
Optional<Path> currentPackage = packagePathCache.lookup(currentPath);
if (!currentPackage.isPresent()) {
return Optional.empty();
}
Class<? extends IjFolder> folderClass = typeForMerging.getFolderTypeClass();
ImmutableSet<IjFolder> childrenToMerge =
children
.stream()
.filter(folderClass::isInstance)
.filter(IjFolder::getWantsPackagePrefix)
.filter(
child ->
canMergeWithKeepingPackage(
currentPath, currentPackage.get(), child, packagePathCache))
.collect(MoreCollectors.toImmutableSet());
if (childrenToMerge.isEmpty()) {
return Optional.empty();
}
IjFolder mergedFolder =
typeForMerging
.getFolderFactory()
.create(
currentPath,
true,
childrenToMerge
.stream()
.flatMap(folder -> folder.getInputs().stream())
.collect(MoreCollectors.toImmutableSortedSet()));
removeFolders(childrenToMerge);
mergePathsMap.put(currentPath, mergedFolder);
return Optional.of(mergedFolder);
}
/**
* Merges children that have package name matching the parent folder package.
*
* <p>For example:
*
* <pre>
* a/b/c (package com.facebook.test)
* +-----> d (package com.facebook.test.d)
* +-----> e (package com.facebook.test.f)
* </pre>
*
* <p>will be merged into:
*
* <pre>
* a/b/c (package com.facebook.test)
* +-----> e (package com.facebook.test.f)
* </pre>
*/
private Optional<IjFolder> mergeFoldersWithMatchingPackageIntoParent(
IjFolder parentFolder, ImmutableSet<IjFolder> children) {
ImmutableSet<IjFolder> childrenToMerge =
children
.stream()
.filter(child -> canMergeWithKeepingPackage(parentFolder, child, packagePathCache))
.collect(MoreCollectors.toImmutableSet());
IjFolder result = mergeFolders(parentFolder, childrenToMerge);
removeFolders(childrenToMerge);
mergePathsMap.put(parentFolder.getPath(), result);
return Optional.of(result);
}
/** Merges children that can be merged into a parent. */
private Optional<IjFolder> mergeAndRemoveSimilarChildren(
IjFolder parentFolder, ImmutableSet<IjFolder> children) {
ImmutableSet<IjFolder> childrenToMerge =
children
.stream()
.filter(folder -> folder.canMergeWith(parentFolder))
.collect(MoreCollectors.toImmutableSet());
IjFolder result = mergeFolders(parentFolder, childrenToMerge);
removeFolders(childrenToMerge);
mergePathsMap.put(result.getPath(), result);
return Optional.of(result);
}
private void removeFolders(Collection<IjFolder> folders) {
folders.stream().map(IjFolder::getPath).forEach(mergePathsMap::remove);
}
}
/**
* @return <code>true</code> if parent and child can be merged and they have correct package
* structure (child's package name matches parent's package + child's folder name).
*/
private static boolean canMergeWithKeepingPackage(
IjFolder parent, IjFolder child, PackagePathCache packagePathCache) {
Preconditions.checkArgument(child.getPath().startsWith(parent.getPath()));
if (!child.canMergeWith(parent)) {
return false;
}
Optional<Path> parentPackage = packagePathCache.lookup(parent);
if (!parentPackage.isPresent()) {
return false;
}
Optional<Path> childPackageOptional = packagePathCache.lookup(child);
if (!childPackageOptional.isPresent()) {
return false;
}
Path childPackage = childPackageOptional.get();
int pathDifference = child.getPath().getNameCount() - parent.getPath().getNameCount();
Preconditions.checkState(
pathDifference == 1,
"Path difference is wrong: %s and %s",
child.getPath(),
parent.getPath());
if (childPackage.getNameCount() == 0) {
return false;
}
return MorePaths.getParentOrEmpty(childPackage).equals(parentPackage.get());
}
private static boolean canMergeWithKeepingPackage(
Path currentPath, Path parentPackage, IjFolder child, PackagePathCache packagePathCache) {
Optional<Path> childPackageOptional = packagePathCache.lookup(child);
if (!childPackageOptional.isPresent()) {
return false;
}
Path childPackage = childPackageOptional.get();
int pathDifference = child.getPath().getNameCount() - currentPath.getNameCount();
Preconditions.checkState(pathDifference == 1);
if (childPackage.getNameCount() == 0) {
return false;
}
return MorePaths.getParentOrEmpty(childPackage).equals(parentPackage);
}
private static IjFolder mergeFolders(IjFolder destinationFolder, Iterable<IjFolder> folders) {
IjFolder result = destinationFolder;
for (IjFolder folder : folders) {
result = folder.merge(result);
}
return result;
}
/**
* Hierarchical path cache. If the path a/b/c/d has package c/d it assumes that a/b/c has the
* package c/.
*/
private static class PackagePathCache {
JavaPackagePathCache delegate;
public PackagePathCache(
ImmutableSet<IjFolder> startingFolders, JavaPackageFinder javaPackageFinder) {
delegate = new JavaPackagePathCache();
for (IjFolder startingFolder : startingFolders) {
if (!startingFolder.getWantsPackagePrefix()) {
continue;
}
Path path =
startingFolder.getInputs().stream().findFirst().orElse(lookupPath(startingFolder));
delegate.insert(path, javaPackageFinder.findJavaPackageFolder(path));
}
}
private Path lookupPath(IjFolder folder) {
return folder.getPath().resolve("notfound");
}
public Optional<Path> lookup(IjFolder folder) {
return delegate.lookup(lookupPath(folder));
}
public Optional<Path> lookup(Path path) {
return delegate.lookup(path.resolve("notfound"));
}
}
}