package com.insightfullogic.honest_profiler.core.aggregation.result.straight; import static java.lang.Math.max; import static java.util.stream.Collectors.toList; import static java.util.stream.Stream.concat; import static java.util.stream.Stream.of; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Predicate; import java.util.stream.Stream; import com.insightfullogic.honest_profiler.core.aggregation.grouping.CombinedGrouping; import com.insightfullogic.honest_profiler.core.aggregation.result.Aggregation; import com.insightfullogic.honest_profiler.core.aggregation.result.Keyed; import com.insightfullogic.honest_profiler.core.profiles.lean.LeanNode; /** * Wrapper for {@link Entry} which allows organizing them into a tree structure. */ public class Node extends Entry { // Instance Properties private Map<String, Node> children; // Instance Constructors /** * Create an empty Node for the specified {@link Aggregation}. * <p> * @param <T> the type of the data items contained in the {@link Aggregation} * @param aggregation the {@link Aggregation} the created Node belongs to */ public <T extends Keyed<String>> Node(Aggregation<T> aggregation) { super(aggregation); children = new HashMap<>(); } /** * Create a Node based on an {@link Entry}. The information of the {@link Entry} is copied into this Node. * <p> * @param entry the {@link Entry} the Node is based on */ public Node(Entry entry) { this(entry.getAggregation()); entry.copyInto(this); } /** * Copy Constructor. * <p> * @param node the Node being copied * @param children the (new) children of the Node */ private Node(Node node, List<Node> children) { this(node.getAggregation()); node.copyInto(this); children.forEach(child -> this.children.put(child.getKey(), child)); } /** * Returns the children of the Node. * <p> * @return the children of the Node */ public List<Node> getChildren() { return new ArrayList<>(children.values()); } /** * Calculate the depth of the (sub)tree with this Node as root. Returns 0 if there are no children. * <p> * @return the depth of the (sub)tree with this Node as root, whereby an empty tree has depth 0 */ public int getDescendantDepth() { if (children.isEmpty()) { return 0; } int depth = 0; for (Node child : children.values()) { depth = max(depth, child.getDescendantDepth() + 1); } return depth; } /** * Adggregates a Node into the children of this Node. * <p> * @param child the Node to be aggregated as child * @return the Node resulting from the aggregation */ public Node addChild(Node child) { return children.compute(child.getKey(), (k, v) -> v == null ? child : v.combine(child)); } /** * Aggregates a {@link LeanNode} into the children of this Node, using the specified {@link CombinedGrouping} to * determine the aggregation key, and recursively aggregating the {@link LeanNode} descendants as well if specified. * <p> * @param child the {@link LeanNode} to be aggregated into the children of this Node * @param grouping the {@link CombinedGrouping} used for determining the aggregation key * @param recurse a boolean specifying whether the {@link LeanNode} descendants should be aggregated recursively */ public void addChild(LeanNode child, CombinedGrouping grouping, boolean recurse) { // Construct intermediate Node Node childNode = new Node(getAggregation()); childNode.add(child); childNode.setKey(grouping.apply(getAggregation().getSource(), child)); // Aggregate it into existing children Node newChild = addChild(childNode); if (recurse) { child.getChildren() .forEach(grandChild -> newChild.addChild(grandChild, grouping, true)); } } /** * Combines another Node into this one. The descendants will also be combined recursively. * <p> * @param other he Node to be combined into this Node * @return this Node */ public Node combine(Node other) { super.combine(other); other.children.values().forEach( child -> children .compute(child.getKey(), (k, v) -> v == null ? child.copy() : v.combine(child))); return this; } /** * Returns a copy of this Node. * <p> * @return a copy of this Node */ public Node copy() { List<Node> newChildren = children.values().stream().map(child -> child.copy()) .filter(child -> child != null).collect(toList()); return new Node(this, newChildren); } /** * Returns a copy of this Node applying a filter to itself and any descendants. The method returns null if no * children are accepted by the filter and the node itself isn't accepted either. * <p> * @param filter a {@link Predicate} for accepting Nodes * @return the filtered Node or null if the Node and none of its descendants are accepted */ public Node copyWithFilter(Predicate<Node> filter) { List<Node> newChildren = children.values().stream() .map(child -> child.copyWithFilter(filter)).filter(child -> child != null) .collect(toList()); return newChildren.size() > 0 || filter.test(this) ? new Node(this, newChildren) : null; } /** * Return a {@link Stream} of Nodes consisting of this Node and all its descendants. * <p> * @return a {@link Stream} of Nodes consisting of this Node and all its descendants */ public Stream<Node> flatten() { return concat(of(this), children.values().stream().flatMap(Node::flatten)); } /** * Return a {@link Stream} of Nodes consisting of all the descendants of this Node. This Node is not included. * <p> * @return a {@link Stream} of Nodes consisting of all the descendants of this Node */ public Stream<Node> flattenDescendants() { return children.values().stream().flatMap(Node::flatten); } @Override public String toString() { return toString(0); } public String toString(int level) { StringBuilder result = new StringBuilder(); for (int i = 0; i < level; i++) { result.append(" "); } result.append(super.toString()); getChildren() .forEach((Node child) -> result.append("\n").append(child.toString(level + 1))); return result.toString(); } }