/*
* 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.aggregation;
import com.facebook.buck.graph.AcyclicDepthFirstPostOrderTraversal;
import com.facebook.buck.graph.GraphTraversable;
import com.facebook.buck.ide.intellij.model.IjModuleType;
import com.facebook.buck.log.Logger;
import com.facebook.buck.util.MoreCollectors;
import com.google.common.collect.ImmutableSet;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
* A tree that is responsible for managing and aggregating modules.
*
* <p>The tree accepts an arbitrary number of modules each of which is located at a specific
* location.
*
* <p>The tree can aggregate the modules by merging modules with the same aggregation tag into one
* module. The aggregation happens at some specific level.
*
* <p>This data structure is based on a <a href="https://en.wikipedia.org/wiki/Trie">prefix
* tree</a>.
*/
public class AggregationTree implements GraphTraversable<AggregationTreeNode> {
private static final Logger LOG = Logger.get(AggregationTree.class);
private final AggregationTreeNode rootNode;
public AggregationTree(AggregationModule module) {
this.rootNode = new AggregationTreeNode(module);
}
public void addModule(Path moduleBasePath, AggregationModule module) {
rootNode.addChild(moduleBasePath, module);
}
@Override
public Iterator<AggregationTreeNode> findChildren(AggregationTreeNode node) {
return node.getChildren().iterator();
}
public void aggregateModules(int minimumPathDepth) {
LOG.verbose("Aggregating modules with depth %s", minimumPathDepth);
createStartingNodes(rootNode, minimumPathDepth);
try {
Iterable<AggregationTreeNode> nodesToTraverse =
new AcyclicDepthFirstPostOrderTraversal<>(this)
.traverse(collectStartingNodes(minimumPathDepth));
for (AggregationTreeNode node : nodesToTraverse) {
aggregateModules(node);
}
} catch (AcyclicDepthFirstPostOrderTraversal.CycleException e) {
throw new IllegalStateException("Cycle detected despite using a tree", e);
}
}
/**
* Make sure aggregation nodes are present at the starting locations of aggregation.
*
* <p>Creates artificial nodes if some nodes are not present. These nodes contain only module
* paths.
*/
private void createStartingNodes(AggregationTreeNode node, int depth) {
if (depth <= 0) {
return;
}
for (Path childPath : node.getChildrenPaths()) {
if (childPath.getNameCount() > depth) {
createStartingNode(node, depth, childPath);
} else if (childPath.getNameCount() < depth) {
createStartingNodes(node.getChild(childPath), depth - childPath.getNameCount());
}
}
}
private void createStartingNode(AggregationTreeNode node, int depth, Path childPath) {
Path newChildPath = childPath.subpath(0, depth);
node.addChild(newChildPath, null, node.getModuleBasePath().resolve(newChildPath));
}
/**
* Analyzes node's children and returns the best aggregation tag (the one with the maximum number
* of modules).
*
* <p>The following modules are excluded from this logic: - modules with UNKNOWN type because it
* can be aggregated into all other modules, - modules with types that explicitly prohibit
* aggregation.
*/
private @Nullable String findBestAggregationTag(AggregationTreeNode node) {
Map<String, Integer> typeCount = new HashMap<>();
node.getChildren()
.forEach(
n -> {
AggregationModule module = n.getModule();
if (module == null) {
return;
}
if (IjModuleType.UNKNOWN_MODULE.equals(module.getModuleType())
|| !module.getModuleType().canBeAggregated()) {
return;
}
String aggregationTag = module.getAggregationTag();
Integer count = typeCount.get(aggregationTag);
typeCount.put(aggregationTag, count == null ? 1 : count + 1);
});
if (typeCount.isEmpty()) {
return null;
}
return Collections.max(typeCount.entrySet(), Comparator.comparingInt(Map.Entry::getValue))
.getKey();
}
private Collection<AggregationTreeNode> collectStartingNodes(int minimumPathDepth) {
return rootNode.collectNodes(minimumPathDepth);
}
private void aggregateModules(AggregationTreeNode parentNode) {
if (parentNode.getChildren().isEmpty()) {
return;
}
AggregationModule nodeModule = parentNode.getModule();
if (nodeModule != null && !nodeModule.getModuleType().canBeAggregated()) {
return;
}
Path moduleBasePath = parentNode.getModuleBasePath();
LOG.verbose("Aggregating module at %s: %s", moduleBasePath, nodeModule);
String aggregationTag;
IjModuleType rootModuleType;
if (nodeModule == null) {
aggregationTag = findBestAggregationTag(parentNode);
rootModuleType = null;
} else {
aggregationTag = nodeModule.getAggregationTag();
rootModuleType = nodeModule.getModuleType();
}
ImmutableSet<Path> modulePathsToAggregate;
if (aggregationTag == null) {
modulePathsToAggregate = parentNode.getChildrenPathsByModuleType(IjModuleType.UNKNOWN_MODULE);
if (modulePathsToAggregate.isEmpty()) {
return;
}
rootModuleType = IjModuleType.UNKNOWN_MODULE;
} else {
modulePathsToAggregate =
parentNode.getChildrenPathsByModuleTypeOrTag(IjModuleType.UNKNOWN_MODULE, aggregationTag);
if (rootModuleType == null) {
rootModuleType =
parentNode
.getChild(modulePathsToAggregate.iterator().next())
.getModule()
.getModuleType();
}
}
ImmutableSet<Path> excludes = findExcludes(parentNode, modulePathsToAggregate);
List<AggregationModule> modulesToAggregate =
modulePathsToAggregate
.stream()
.map(parentNode::getChild)
.map(AggregationTreeNode::getModule)
.collect(Collectors.toList());
modulePathsToAggregate.forEach(parentNode::removeChild);
if (nodeModule == null) {
parentNode.setModule(
ModuleAggregator.aggregate(
moduleBasePath,
rootModuleType,
aggregationTag == null
? modulesToAggregate.iterator().next().getAggregationTag()
: aggregationTag,
modulesToAggregate,
excludes));
} else {
parentNode.setModule(ModuleAggregator.aggregate(nodeModule, modulesToAggregate, excludes));
}
LOG.verbose("Module after aggregation: %s", parentNode.getModule());
}
private ImmutableSet<Path> findExcludes(
AggregationTreeNode parentNode, ImmutableSet<Path> modulePathsToAggregate) {
return parentNode
.getChildrenPaths()
.stream()
.filter(path -> !modulePathsToAggregate.contains(path))
.collect(MoreCollectors.toImmutableSet());
}
public Collection<AggregationModule> getModules() {
try {
List<AggregationModule> result = new ArrayList<>();
new AcyclicDepthFirstPostOrderTraversal<>(this)
.traverse(Collections.singleton(rootNode))
.forEach(
node -> {
if (node.getModule() != null) {
result.add(node.getModule());
}
});
return result;
} catch (AcyclicDepthFirstPostOrderTraversal.CycleException e) {
throw new IllegalStateException("Cycle detected despite using a tree", e);
}
}
}