/* * 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); } } }