diff --git a/hutool-core/src/main/java/cn/hutool/core/map/ForestMap.java b/hutool-core/src/main/java/cn/hutool/core/map/ForestMap.java new file mode 100644 index 000000000..2fa21fe18 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/ForestMap.java @@ -0,0 +1,319 @@ +package cn.hutool.core.map; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Opt; +import cn.hutool.core.util.ObjectUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * 基于多个{@link TreeEntry}构成的、彼此平行的树结构构成的森林集合。 + * + * @param key类型 + * @param value类型 + * @author huangchengxing + * @see TreeEntry + */ +public interface ForestMap extends Map> { + + // ===================== Map接口方法的重定义 ===================== + + /** + * 添加一个节点,效果等同于 {@code putNode(key, node.getValue())} + *
    + *
  • 若key对应节点不存在,则以传入的键值创建一个新的节点;
  • + *
  • 若key对应节点存在,则将该节点的值替换为{@code node}指定的值;
  • + *
+ * + * @param key 节点的key值 + * @param node 节点 + * @return 节点,若key已有对应节点,则返回具有旧值的节点,否则返回null + * @see #putNode(Object, Object) + */ + @Override + default TreeEntry put(K key, TreeEntry node) { + return putNode(key, node.getValue()); + } + + /** + * 批量添加节点,若节点具有父节点或者子节点,则一并在当前实例中引入该关系 + * + * @param treeEntryMap 节点集合 + */ + @Override + default void putAll(Map> treeEntryMap) { + if (CollUtil.isEmpty(treeEntryMap)) { + return; + } + treeEntryMap.forEach((k, v) -> { + if (v.hasParent()) { + final TreeEntry parent = v.getDeclaredParent(); + putLinkedNodes(parent.getKey(), parent.getValue(), v.getKey(), v.getValue()); + } else { + putNode(v.getKey(), v.getValue()); + } + }); + } + + /** + * 将指定节点从当前{@link Map}中删除 + *
    + *
  • 若存在父节点或子节点,则将其断开其与父节点或子节点的引用关系;
  • + *
  • + * 若同时存在父节点或子节点,则会在删除后将让子节点直接成为父节点的子节点,比如:
    + * 现有引用关系 a -> b -> c,删除 b 后,将有 a -> c + *
  • + *
+ * + * @param key 节点的key + * @return 删除的节点,若key没有对应节点,则返回null + */ + @Override + TreeEntry remove(Object key); + + /** + * 将当前集合清空,并清除全部节点间的引用关系 + */ + @Override + void clear(); + + // ===================== 节点操作 ===================== + + /** + * 批量添加节点 + * + * @param values 要添加的值 + * @param keyGenerator 从值中获取key的方法 + * @param parentKeyGenerator 从值中获取父节点key的方法 + * @param ignoreNullNode 是否获取到的key为null的子节点/父节点 + */ + default > void putAllNode( + C values, Function keyGenerator, Function parentKeyGenerator, boolean ignoreNullNode) { + if (CollUtil.isEmpty(values)) { + return; + } + values.forEach(v -> { + final K key = keyGenerator.apply(v); + final K parentKey = parentKeyGenerator.apply(v); + + // 不忽略keu为null节点 + boolean hasKey = ObjectUtil.isNotNull(key); + boolean hasParentKey = ObjectUtil.isNotNull(parentKey); + if (!ignoreNullNode || (hasKey && hasParentKey)) { + linkNodes(parentKey, key); + get(key).setValue(v); + return; + } + + // 父子节点的key都为null + if (!hasKey && !hasParentKey) { + return; + } + + // 父节点key为null + if (hasKey) { + putNode(key, v); + return; + } + + // 子节点key为null + putNode(parentKey, null); + }); + } + + /** + * 添加一个节点 + *
    + *
  • 若key对应节点不存在,则以传入的键值创建一个新的节点;
  • + *
  • 若key对应节点存在,则将该节点的值替换为{@code node}指定的值;
  • + *
+ * + * @param key 节点的key + * @param value 节点的value + * @return 节点,若key已有对应节点,则返回具有旧值的节点,否则返回null + */ + TreeEntry putNode(K key, V value); + + /** + * 同时添加父子节点: + *
    + *
  • 若{@code parentKey}或{@code childKey}对应的节点不存在,则会根据键值创建一个对应的节点;
  • + *
  • 若{@code parentKey}或{@code childKey}对应的节点存在,则会更新对应节点的值;
  • + *
+ * 该操作等同于: + *
{@code
+	 *     putNode(parentKey, parentValue);
+	 *     putNode(childKey, childValue);
+	 *     linkNodes(parentKey, childKey);
+	 * }
+ * + * @param parentKey 父节点的key + * @param parentValue 父节点的value + * @param childKey 子节点的key + * @param childValue 子节点的值 + */ + default void putLinkedNodes(K parentKey, V parentValue, K childKey, V childValue) { + putNode(parentKey, parentValue); + putNode(childKey, childValue); + linkNodes(parentKey, childKey); + } + + /** + * 添加子节点,并为子节点指定父节点: + *
    + *
  • 若{@code parentKey}或{@code childKey}对应的节点不存在,则会根据键值创建一个对应的节点;
  • + *
  • 若{@code parentKey}或{@code childKey}对应的节点存在,则会更新对应节点的值;
  • + *
+ * + * @param parentKey 父节点的key + * @param childKey 子节点的key + * @param childValue 子节点的值 + */ + void putLinkedNodes(K parentKey, K childKey, V childValue); + + /** + * 为集合中的指定的节点建立父子关系 + * + * @param parentKey 父节点的key + * @param childKey 子节点的key + */ + default void linkNodes(K parentKey, K childKey) { + linkNodes(parentKey, childKey, null); + } + + /** + * 为集合中的指定的节点建立父子关系 + * + * @param parentKey 父节点的key + * @param childKey 子节点的key + * @param consumer 对父节点和子节点的操作,允许为null + */ + void linkNodes(K parentKey, K childKey, BiConsumer, TreeEntry> consumer); + + /** + * 若{@code parentKey}或{@code childKey}对应节点都存在,则移除指定该父节点与其直接关联的指定子节点间的引用关系 + * + * @param parentKey 父节点的key + * @param childKey 子节点 + */ + void unlinkNode(K parentKey, K childKey); + + // ===================== 父节点相关方法 ===================== + + /** + * 获取指定节点所在树结构的全部树节点
+ * 比如:存在 a -> b -> c 的关系,则输入 a/b/c 都将返回 a, b, c + * + * @param key 指定节点的key + * @return 节点 + */ + default Set> getTreeNodes(K key) { + final TreeEntry target = get(key); + if (ObjectUtil.isNull(target)) { + return Collections.emptySet(); + } + Set> results = CollUtil.newLinkedHashSet(target.getRoot()); + CollUtil.addAll(results, target.getRoot().getChildren().values()); + return results; + } + + /** + * 获取以指定节点作为叶子节点的树结构,然后获取该树结构的根节点
+ * 比如:存在 a -> b -> c 的关系,则输入 a/b/c 都将返回 a + * + * @param key 指定节点的key + * @return 节点 + */ + default TreeEntry getRootNode(K key) { + return Opt.ofNullable(get(key)) + .map(TreeEntry::getRoot) + .orElse(null); + } + + /** + * 获取指定节点的直接父节点
+ * 比如:若存在 a -> b -> c 的关系,此时输入 a 将返回 null,输入 b 将返回 a,输入 c 将返回 b + * + * @param key 指定节点的key + * @return 节点 + */ + default TreeEntry getDeclaredParentNode(K key) { + return Opt.ofNullable(get(key)) + .map(TreeEntry::getDeclaredParent) + .orElse(null); + } + + /** + * 获取以指定节点作为叶子节点的树结构,然后获取该树结构中指定节点的指定父节点 + * + * @param key 指定父节点的key + * @return 节点 + */ + default TreeEntry getParentNode(K key, K parentKey) { + return Opt.ofNullable(get(key)) + .map(t -> t.getParent(parentKey)) + .orElse(null); + } + + /** + * 获取以指定节点作为叶子节点的树结构,然后确认该树结构中当前节点是否存在指定父节点 + * + * @param key 指定节点的key + * @param parentKey 指定父节点的key + * @return 是否 + */ + default boolean containsParentNode(K key, K parentKey) { + return Opt.ofNullable(get(key)) + .map(m -> m.containsParent(parentKey)) + .orElse(false); + } + + // ===================== 子节点相关方法 ===================== + + /** + * 判断以该父节点作为根节点的树结构中是否具有指定子节点 + * + * @param parentKey 父节点 + * @param childKey 子节点 + * @return 是否 + */ + default boolean containsChildNode(K parentKey, K childKey) { + return Opt.ofNullable(get(parentKey)) + .map(m -> m.containsChild(childKey)) + .orElse(false); + } + + /** + * 获取指定父节点直接关联的子节点
+ * 比如:若存在 a -> b -> c 的关系,此时输入 b 将返回 c,输入 a 将返回 b + * + * @param key key + * @return 节点 + */ + default Collection> getDeclaredChildNodes(K key) { + return Opt.ofNullable(get(key)) + .map(TreeEntry::getDeclaredChildren) + .map(Map::values) + .orElseGet(Collections::emptyList); + } + + /** + * 获取指定父节点的全部子节点
+ * 比如:若存在 a -> b -> c 的关系,此时输入 b 将返回 c,输入 a 将返回 b,c + * + * @param key key + * @return 该节点的全部子节点 + */ + default Collection> getChildNodes(K key) { + return Opt.ofNullable(get(key)) + .map(TreeEntry::getChildren) + .map(Map::values) + .orElseGet(Collections::emptyList); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/LinkedForestMap.java b/hutool-core/src/main/java/cn/hutool/core/map/LinkedForestMap.java new file mode 100644 index 000000000..e030ae230 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/LinkedForestMap.java @@ -0,0 +1,727 @@ +package cn.hutool.core.map; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * {@link ForestMap}的基本实现。 + * + *

该集合可以被视为以{@link TreeEntryNode#getKey()}作为key,{@link TreeEntryNode}实例作为value的{@link LinkedHashMap}。
+ * 使用时,将每一对键与值对视为一个{@link TreeEntryNode}节点,节点的id即为{@link TreeEntryNode#getKey()}, + * 任何情况下使用相同的key都将会访问到同一个节点。
+ * + *

节点通过key形成父子关系,并最终构成多叉树结构,多组平行的多叉树将在当前集合中构成森林。 + * 使用者可以通过{@link ForestMap}本身的方法来对森林进行操作或访问, + * 也可以在获取到{@link TreeEntry}后,使用节点本身的方法对数进行操作或访问。 + * + * @param key类型 + * @author huangchengxing + */ +public class LinkedForestMap implements ForestMap { + + /** + * 节点集合 + */ + private final Map> nodes; + + /** + * 当指定节点已经与其他节点构成了父子关系,是否允许将该节点的父节点强制替换为指定节点 + */ + private final boolean allowOverrideParent; + + /** + * 构建{@link LinkedForestMap} + * + * @param allowOverrideParent 当指定节点已经与其他节点构成了父子关系,是否允许将该节点的父节点强制替换为指定节点 + */ + public LinkedForestMap(boolean allowOverrideParent) { + this.allowOverrideParent = allowOverrideParent; + this.nodes = new LinkedHashMap<>(); + } + + // ====================== Map接口实现 ====================== + + /** + * 获取当前实例中的节点个数 + * + * @return 节点个数 + */ + @Override + public int size() { + return nodes.size(); + } + + /** + * 当前实例是否为空 + * + * @return 是否 + */ + @Override + public boolean isEmpty() { + return nodes.isEmpty(); + } + + /** + * 当前实例中是否存在key对应的节点 + * + * @param key key + * @return 是否 + */ + @Override + public boolean containsKey(Object key) { + return nodes.containsKey(key); + } + + /** + * 当前实例中是否存在对应的{@link TreeEntry}实例 + * + * @param value {@link TreeEntry}实例 + * @return 是否 + */ + @Override + public boolean containsValue(Object value) { + return nodes.containsValue(value); + } + + /** + * 获取key对应的节点 + * + * @param key key + * @return 节点 + */ + @Override + public TreeEntry get(Object key) { + return nodes.get(key); + } + + /** + * 将指定节点从当前{@link Map}中删除 + *

    + *
  • 若存在父节点或子节点,则将其断开其与父节点或子节点的引用关系;
  • + *
  • + * 若同时存在父节点或子节点,则会在删除后将让子节点直接成为父节点的子节点,比如:
    + * 现有引用关系 a -> b -> c,删除 b 后,将有 a -> c + *
  • + *
+ * + * @param key 节点的key + * @return 删除的且引用关系已经改变的节点,若key没有对应节点,则返回null + */ + @Override + public TreeEntry remove(Object key) { + final TreeEntryNode target = nodes.remove(key); + if (ObjectUtil.isNull(target)) { + return null; + } + // 若存在父节点: + // 1.将该目标从父节点的子节点中移除 + // 2.将目标的子节点直接将目标的父节点作为父节点 + if (target.hasParent()) { + final TreeEntryNode parent = target.getDeclaredParent(); + final Map> targetChildren = target.getChildren(); + parent.removeDeclaredChild(target.getKey()); + target.clear(); + targetChildren.forEach((k, c) -> parent.addChild((TreeEntryNode)c)); + } + return target; + } + + /** + * 将当前集合清空,并清除全部节点间的引用关系 + */ + @Override + public void clear() { + nodes.values().forEach(TreeEntryNode::clear); + nodes.clear(); + } + + /** + * 返回当前实例中全部的key组成的{@link Set}集合 + * + * @return 集合 + */ + @Override + public Set keySet() { + return nodes.keySet(); + } + + /** + * 返回当前实例中全部{@link TreeEntry}组成的{@link Collection}集合 + * + * @return 集合 + */ + @Override + public Collection> values() { + return new ArrayList<>(nodes.values()); + } + + /** + * 由key与{@link TreeEntry}组成的键值对实体的{@link Set}集合。 + * 注意,返回集合中{@link Map.Entry#setValue(Object)}不支持调用。 + * + * @return 集合 + */ + @Override + public Set>> entrySet() { + return nodes.entrySet().stream() + .map(this::wrap) + .collect(Collectors.toSet()); + } + + /** + * 将{@link TreeEntryNode}包装为{@link EntryNodeWrapper} + */ + private Map.Entry> wrap(Map.Entry> nodeEntry) { + return new EntryNodeWrapper<>(nodeEntry.getValue()); + } + + // ====================== ForestMap接口实现 ====================== + + /** + * 添加一个节点 + *
    + *
  • 若key对应节点不存在,则以传入的键值创建一个新的节点;
  • + *
  • 若key对应节点存在,则将该节点的值替换为{@code node}指定的值;
  • + *
+ * + * @param key 节点的key + * @param value 节点的value + * @return 节点,若key已有对应节点,则返回具有旧值的节点,否则返回null + */ + @Override + public TreeEntryNode putNode(K key, V value) { + TreeEntryNode target = nodes.get(key); + if (ObjectUtil.isNotNull(target)) { + final V oldVal = target.getValue(); + target.setValue(value); + return target.copy(oldVal); + } + target = new TreeEntryNode<>(null, key, value); + nodes.put(key, target); + return null; + } + + /** + * 同时添加父子节点: + *
    + *
  • 若{@code parentKey}或{@code childKey}对应的节点不存在,则会根据键值创建一个对应的节点;
  • + *
  • 若{@code parentKey}或{@code childKey}对应的节点存在,则会更新对应节点的值;
  • + *
+ * 该操作等同于: + *
+	 *     TreeEntry  parent = putNode(parentKey, parentValue);
+	 *     TreeEntry  child = putNode(childKey, childValue);
+	 *     linkNodes(parentKey, childKey);
+	 * 
+ * + * @param parentKey 父节点的key + * @param parentValue 父节点的value + * @param childKey 子节点的key + * @param childValue 子节点的值 + */ + @Override + public void putLinkedNodes(K parentKey, V parentValue, K childKey, V childValue) { + linkNodes(parentKey, childKey, (parent, child) -> { + parent.setValue(parentValue); + child.setValue(childValue); + }); + } + + /** + * 添加子节点,并为子节点指定父节点: + *
    + *
  • 若{@code parentKey}或{@code childKey}对应的节点不存在,则会根据键值创建一个对应的节点;
  • + *
  • 若{@code parentKey}或{@code childKey}对应的节点存在,则会更新对应节点的值;
  • + *
+ * + * @param parentKey 父节点的key + * @param childKey 子节点的key + * @param childValue 子节点的值 + */ + @Override + public void putLinkedNodes(K parentKey, K childKey, V childValue) { + linkNodes(parentKey, childKey, (parent, child) -> child.setValue(childValue)); + } + + /** + * 为指定的节点建立父子关系,若{@code parentKey}或{@code childKey}对应节点不存在,则会创建一个对应的值为null的空节点 + * + * @param parentKey 父节点的key + * @param childKey 子节点的key + * @param consumer 对父节点和子节点的操作,允许为null + */ + @Override + public void linkNodes(K parentKey, K childKey, BiConsumer, TreeEntry> consumer) { + consumer = ObjectUtil.defaultIfNull(consumer, (parent, child) -> {}); + final TreeEntryNode parentNode = nodes.computeIfAbsent(parentKey, t -> new TreeEntryNode<>(null, t)); + TreeEntryNode childNode = nodes.get(childKey); + + // 1.子节点不存在 + if (ObjectUtil.isNull(childNode)) { + childNode = new TreeEntryNode<>(parentNode, childKey); + consumer.accept(parentNode, childNode); + nodes.put(childKey, childNode); + return; + } + + // 2.子节点存在,且已经是该父节点的子节点了 + if (ObjectUtil.equals(parentNode, childNode.getDeclaredParent())) { + consumer.accept(parentNode, childNode); + return; + } + + // 3.子节点存在,但是未与其他节点构成父子关系 + if (false == childNode.hasParent()) { + parentNode.addChild(childNode); + } + // 4.子节点存在,且已经与其他节点构成父子关系,但是允许子节点直接修改其父节点 + else if (allowOverrideParent) { + childNode.getDeclaredParent().removeDeclaredChild(childNode.getKey()); + parentNode.addChild(childNode); + } + // 5.子节点存在,且已经与其他节点构成父子关系,但是不允许子节点直接修改其父节点 + else{ + throw new IllegalArgumentException(StrUtil.format( + "[{}] has been used as child of [{}], can not be overwrite as child of [{}]", + childNode.getKey(), childNode.getDeclaredParent().getKey(), parentKey + )); + } + consumer.accept(parentNode, childNode); + } + + /** + * 移除指定父节点与其直接关联的子节点间的引用关系,但是不会将该节点从集合中删除 + * + * @param parentKey 父节点的key + * @param childKey 子节点 + */ + @Override + public void unlinkNode(K parentKey, K childKey) { + final TreeEntryNode childNode = nodes.get(childKey); + if (ObjectUtil.isNull(childNode)) { + return; + } + if (childNode.hasParent()) { + childNode.getDeclaredParent().removeDeclaredChild(childNode.getKey()); + } + } + + /** + * 树节点 + * + * @param key类型 + * @author huangchengxing + */ + public static class TreeEntryNode implements TreeEntry { + + /** + * 根节点 + */ + private TreeEntryNode root; + + /** + * 父节点 + */ + private TreeEntryNode parent; + + /** + * 权重,表示到根节点的距离 + */ + private int weight; + + /** + * 子节点 + */ + private final Map> children; + + /** + * key + */ + private final K key; + + /** + * 值 + */ + private V value; + + /** + * 创建一个节点 + * + * @param parent 节点的父节点 + * @param key 节点的key + */ + public TreeEntryNode(TreeEntryNode parent, K key) { + this(parent, key , null); + } + + /** + * 创建一个节点 + * + * @param parent 节点的父节点 + * @param key 节点的key + * @param value 节点的value + */ + public TreeEntryNode(TreeEntryNode parent, K key, V value) { + this.parent = parent; + this.key = key; + this.value = value; + this.children = new LinkedHashMap<>(); + if (ObjectUtil.isNull(parent)) { + this.root = this; + this.weight = 0; + } else { + parent.addChild(this); + this.weight = parent.weight + 1; + this.root = parent.root; + } + } + + /** + * 获取当前节点的key + * + * @return 节点的key + */ + @Override + public K getKey() { + return key; + } + + /** + * 获取当前节点与根节点的距离 + * + * @return 当前节点与根节点的距离 + */ + @Override + public int getWeight() { + return weight; + } + + /** + * 获取节点的value + * + * @return 节点的value + */ + @Override + public V getValue() { + return value; + } + + /** + * 设置节点的value + * + * @param value 节点的value + * @return 节点的旧value + */ + @Override + public V setValue(V value) { + final V oldVal = getValue(); + this.value = value; + return oldVal; + } + + // ================== 父节点的操作 ================== + + /** + * 从当前节点开始,向上递归当前节点的父节点 + * + * @param includeCurrent 是否处理当前节点 + * @param consumer 对节点的操作 + * @param breakTraverse 是否终止遍历 + * @return 遍历到的最后一个节点 + */ + TreeEntryNode traverseParentNodes( + boolean includeCurrent, Consumer> consumer, Predicate> breakTraverse) { + breakTraverse = ObjectUtil.defaultIfNull(breakTraverse, n -> false); + TreeEntryNode curr = includeCurrent ? this : this.parent; + while (ObjectUtil.isNotNull(curr)) { + consumer.accept(curr); + if (breakTraverse.test(curr)) { + break; + } + curr = curr.parent; + } + return curr; + } + + /** + * 当前节点是否为根节点 + * + * @return 当前节点是否为根节点 + */ + public boolean isRoot() { + return getRoot() == this; + } + + /** + * 获取以当前节点作为叶子节点的树结构,然后获取该树结构的根节点 + * + * @return 根节点 + */ + @Override + public TreeEntryNode getRoot() { + if (ObjectUtil.isNotNull(this.root)) { + return this.root; + } else { + this.root = traverseParentNodes(true, p -> {}, p -> !p.hasParent()); + } + return this.root; + } + + /** + * 获取当前节点直接关联的父节点 + * + * @return 父节点,当节点不存在对应父节点时返回null + */ + @Override + public TreeEntryNode getDeclaredParent() { + return parent; + } + + /** + * 获取以当前节点作为叶子节点的树结构,然后获取该树结构中当前节点的指定父节点 + * + * @param key 指定父节点的key + * @return 指定父节点,当不存在时返回null + */ + @Override + public TreeEntryNode getParent(K key) { + return traverseParentNodes(false, p -> {}, p -> p.equalsKey(key)); + } + + /** + * 获取以当前节点作为根节点的树结构,然后遍历所有节点 + * + * @param includeSelf 是否处理当前节点 + * @param nodeConsumer 对节点的处理 + */ + @Override + public void forEachChild(boolean includeSelf, Consumer> nodeConsumer) { + traverseChildNodes(includeSelf, (index, child) -> nodeConsumer.accept(child), null); + } + + /** + * 指定key与当前节点的key是否相等 + * + * @param key 要比较的key + */ + public boolean equalsKey(K key) { + return ObjectUtil.equal(getKey(), key); + } + + // ================== 子节点的操作 ================== + + /** + * 从当前节点开始,按广度优先向下遍历当前节点的所有子节点 + * + * @param includeCurrent 是否包含当前节点 + * @param consumer 对节点与节点和当前节点的距离的操作,当{code includeCurrent}为false时下标从1开始,否则从0开始 + * @param breakTraverse 是否终止遍历,为null时默认总是返回{@code true} + * @return 遍历到的最后一个节点 + */ + TreeEntryNode traverseChildNodes( + boolean includeCurrent, BiConsumer> consumer, BiPredicate> breakTraverse) { + breakTraverse = ObjectUtil.defaultIfNull(breakTraverse, (i, n) -> false); + final Deque>> keyNodeDeque = CollUtil.newLinkedList(CollUtil.newArrayList(this)); + boolean needProcess = includeCurrent; + int index = includeCurrent ? 0 : 1; + TreeEntryNode lastNode = null; + while (!keyNodeDeque.isEmpty()) { + final List> curr = keyNodeDeque.removeFirst(); + final List> next = new ArrayList<>(); + for (final TreeEntryNode node : curr) { + if (needProcess) { + consumer.accept(index, node); + if (breakTraverse.test(index, node)) { + return node; + } + } else { + needProcess = true; + } + CollUtil.addAll(next, node.children.values()); + } + if (!next.isEmpty()) { + keyNodeDeque.addLast(next); + } + lastNode = CollUtil.getLast(next); + index++; + } + return lastNode; + } + + + /** + * 添加子节点 + * + * @param child 子节点 + * @throws IllegalArgumentException 当要添加的子节点已经是其自身父节点时抛出 + */ + void addChild(TreeEntryNode child) { + if (containsChild(child.key)) { + return; + } + + // 检查循环引用 + traverseParentNodes(true, s -> Assert.notEquals( + s.key, child.key, + "circular reference between [{}] and [{}]!", + s.key, this.key + ), null); + + // 调整该节点的信息 + child.parent = this; + child.traverseChildNodes(true, (i, c) -> { + c.root = getRoot(); + c.weight = i + getWeight() + 1; + }, null); + + // 将该节点添加为当前节点的子节点 + children.put(child.key, child); + } + + /** + * 移除子节点 + * + * @param key 子节点 + */ + void removeDeclaredChild(K key) { + TreeEntryNode child = children.get(key); + if (ObjectUtil.isNull(child)) { + return; + } + + // 断开该节点与其父节点的关系 + this.children.remove(key); + + // 重置子节点及其下属节点的相关属性 + child.parent = null; + child.traverseChildNodes(true, (i, c) -> { + c.root = child; + c.weight = i; + }, null); + } + + /** + * 获取以当前节点作为根节点的树结构,然后获取该树结构中的当前节点的指定子节点 + * + * @param key 指定子节点的key + * @return 节点 + */ + @Override + public TreeEntryNode getChild(K key) { + return traverseChildNodes(false, (i, c) -> {}, (i, c) -> c.equalsKey(key)); + } + + /** + * 获取当前节点直接关联的子节点 + * + * @return 节点 + */ + @Override + public Map> getDeclaredChildren() { + return new LinkedHashMap<>(this.children); + } + + /** + * 获取以当前节点作为根节点的树结构,然后按广度优先获取该树结构中的当前节点的全部子节点 + * + * @return 节点 + */ + @Override + public Map> getChildren() { + final Map> childrenMap = new LinkedHashMap<>(); + traverseChildNodes(false, (i, c) -> childrenMap.put(c.getKey(), c), null); + return childrenMap; + } + + /** + * 移除对子节点、父节点与根节点的全部引用 + */ + void clear() { + this.root = null; + this.children.clear(); + this.parent = null; + } + + /** + * 比较目标对象与当前{@link TreeEntry}是否相等。
+ * 默认只要{@link TreeEntry#getKey()}的返回值相同,即认为两者相等 + * + * @param o 目标对象 + * @return 是否 + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass().equals(o.getClass()) || ClassUtil.isAssignable(this.getClass(), o.getClass())) { + return false; + } + final TreeEntry treeEntry = (TreeEntry)o; + return ObjectUtil.equals(this.getKey(), treeEntry.getKey()); + } + + /** + * 返回当前{@link TreeEntry}的哈希值。
+ * 默认总是返回{@link TreeEntry#getKey()}的哈希值 + * + * @return 哈希值 + */ + @Override + public int hashCode() { + return Objects.hash(getKey()); + } + + /** + * 复制一个当前节点 + * + * @param value 复制的节点的值 + * @return 节点 + */ + TreeEntryNode copy(V value) { + TreeEntryNode copiedNode = new TreeEntryNode<>(this.parent, this.key, ObjectUtil.defaultIfNull(value, this.value)); + copiedNode.children.putAll(children); + return copiedNode; + } + + } + + /** + * {@link java.util.Map.Entry}包装类 + * + * @param key类型 + * @param value类型 + * @param 包装的{@link TreeEntry}类型 + * @see #entrySet() + * @see #values() + */ + public static class EntryNodeWrapper> implements Map.Entry> { + private final N entryNode; + EntryNodeWrapper(N entryNode) { + this.entryNode = entryNode; + } + @Override + public K getKey() { + return entryNode.getKey(); + } + @Override + public TreeEntry getValue() { + return entryNode; + } + @Override + public TreeEntry setValue(TreeEntry value) { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/TreeEntry.java b/hutool-core/src/main/java/cn/hutool/core/map/TreeEntry.java new file mode 100644 index 000000000..a85856873 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/TreeEntry.java @@ -0,0 +1,143 @@ +package cn.hutool.core.map; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.util.Map; +import java.util.function.Consumer; + +/** + * 允许拥有一个父节点与多个子节点的{@link Map.Entry}实现, + * 表示一个以key作为唯一标识,并且可以挂载一个对应值的树节点, + * 提供一些基于该节点对其所在树结构进行访问的方法 + * + * @param 节点的key类型 + * @param 节点的value类型 + * @author huangchengxing + * @see ForestMap + */ +public interface TreeEntry extends Map.Entry { + + // ===================== Entry方法的重定义 ===================== + + /** + * 比较目标对象与当前{@link TreeEntry}是否相等。
+ * 默认只要{@link TreeEntry#getKey()}的返回值相同,即认为两者相等 + * + * @param o 目标对象 + * @return 是否 + */ + @Override + boolean equals(Object o); + + /** + * 返回当前{@link TreeEntry}的哈希值。
+ * 默认总是返回{@link TreeEntry#getKey()}的哈希值 + * + * @return 哈希值 + */ + @Override + int hashCode(); + + // ===================== 父节点相关方法 ===================== + + /** + * 获取以当前节点作为叶子节点的树结构,然后获取当前节点与根节点的距离 + * + * @return 当前节点与根节点的距离 + */ + int getWeight(); + + /** + * 获取以当前节点作为叶子节点的树结构,然后获取该树结构的根节点 + * + * @return 根节点 + */ + TreeEntry getRoot(); + + /** + * 当前节点是否存在直接关联的父节点 + * + * @return 是否 + */ + default boolean hasParent() { + return ObjectUtil.isNotNull(getDeclaredParent()); + } + + /** + * 获取当前节点直接关联的父节点 + * + * @return 父节点,当节点不存在对应父节点时返回null + */ + TreeEntry getDeclaredParent(); + + /** + * 获取以当前节点作为叶子节点的树结构,然后获取该树结构中当前节点的指定父节点 + * + * @param key 指定父节点的key + * @return 指定父节点,当不存在时返回null + */ + TreeEntry getParent(K key); + + /** + * 获取以当前节点作为叶子节点的树结构,然后确认该树结构中当前节点是否存在指定父节点 + * + * @param key 指定父节点的key + * @return 是否 + */ + default boolean containsParent(K key) { + return ObjectUtil.isNotNull(getParent(key)); + } + + // ===================== 子节点相关方法 ===================== + + /** + * 获取以当前节点作为根节点的树结构,然后遍历所有节点 + * + * @param includeSelf 是否处理当前节点 + * @param nodeConsumer 对节点的处理 + */ + void forEachChild(boolean includeSelf, Consumer> nodeConsumer); + + /** + * 获取当前节点直接关联的子节点 + * + * @return 节点 + */ + Map> getDeclaredChildren(); + + /** + * 获取以当前节点作为根节点的树结构,然后获取该树结构中的当前节点的全部子节点 + * + * @return 节点 + */ + Map> getChildren(); + + /** + * 当前节点是否有子节点 + * + * @return 是否 + */ + default boolean hasChildren() { + return CollUtil.isNotEmpty(getDeclaredChildren()); + } + + /** + * 获取以当前节点作为根节点的树结构,然后获取该树结构中的当前节点的指定子节点 + * + * @param key 指定子节点的key + * @return 节点 + */ + TreeEntry getChild(K key); + + /** + * 获取以当前节点作为根节点的树结构,然后确认该树结构中当前节点是否存在指定子节点 + * + * @param key 指定子节点的key + * @return 是否 + */ + default boolean containsChild(K key) { + return ObjectUtil.isNotNull(getChild(key)); + } + +} diff --git a/hutool-core/src/test/java/cn/hutool/core/map/LinkedForestMapTest.java b/hutool-core/src/test/java/cn/hutool/core/map/LinkedForestMapTest.java new file mode 100644 index 000000000..af13d4c5e --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/map/LinkedForestMapTest.java @@ -0,0 +1,365 @@ +package cn.hutool.core.map; + +import cn.hutool.core.collection.CollStreamUtil; +import cn.hutool.core.collection.CollUtil; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.*; + +public class LinkedForestMapTest { + + private final ForestMap treeNodeMap = new LinkedForestMap<>(false); + + @Before + public void beforeTest() { + // a -> b -> c -> d + treeNodeMap.putLinkedNodes("a", "b", "bbb"); + treeNodeMap.putLinkedNodes("b", "c", "ccc"); + treeNodeMap.putLinkedNodes("c", "d", "ddd"); + treeNodeMap.get("a").setValue("aaa"); + } + + @Test + public void testTreeEntry() { + TreeEntry parent = treeNodeMap.get("b"); + TreeEntry treeEntry = treeNodeMap.get("c"); + TreeEntry child = treeNodeMap.get("d"); + + // Entry相关 + Assert.assertEquals("c", treeEntry.getKey()); + Assert.assertEquals("ccc", treeEntry.getValue()); + + // 父节点相关方法 + Assert.assertEquals(2, treeEntry.getWeight()); + Assert.assertEquals(treeNodeMap.get("a"), treeEntry.getRoot()); + Assert.assertTrue(treeEntry.hasParent()); + Assert.assertEquals(parent, treeEntry.getDeclaredParent()); + Assert.assertEquals(treeNodeMap.get("a"), treeEntry.getParent("a")); + Assert.assertTrue(treeEntry.containsParent("a")); + + // 子节点相关方法 + List> nodes = new ArrayList<>(); + treeEntry.forEachChild(true, nodes::add); + Assert.assertEquals(CollUtil.newArrayList(treeEntry, child), nodes); + nodes.clear(); + treeEntry.forEachChild(false, nodes::add); + Assert.assertEquals(CollUtil.newArrayList(child), nodes); + + Assert.assertEquals(CollUtil.newLinkedHashSet(child), new LinkedHashSet<>(treeEntry.getDeclaredChildren().values())); + Assert.assertEquals(CollUtil.newLinkedHashSet(child), new LinkedHashSet<>(treeEntry.getChildren().values())); + Assert.assertTrue(treeEntry.hasChildren()); + Assert.assertEquals(treeNodeMap.get("d"), treeEntry.getChild("d")); + Assert.assertTrue(treeEntry.containsChild("d")); + } + + @Test + public void putTest() { + ForestMap map = new LinkedForestMap<>(false); + + TreeEntry treeEntry = new LinkedForestMap.TreeEntryNode<>(null, "a", "aaa"); + Assert.assertNull(map.put("a", treeEntry)); + Assert.assertNotEquals(map.get("a"), treeEntry); + Assert.assertEquals(map.get("a").getKey(), treeEntry.getKey()); + Assert.assertEquals(map.get("a").getValue(), treeEntry.getValue()); + + treeEntry = new LinkedForestMap.TreeEntryNode<>(null, "a", "aaaa"); + Assert.assertNotNull(map.put("a", treeEntry)); + Assert.assertNotEquals(map.get("a"), treeEntry); + Assert.assertEquals(map.get("a").getKey(), treeEntry.getKey()); + Assert.assertEquals(map.get("a").getValue(), treeEntry.getValue()); + } + + @Test + public void removeTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + map.linkNodes("b", "c"); + + TreeEntry a = map.get("a"); + TreeEntry b = map.get("b"); + TreeEntry c = map.get("c"); + + map.remove("b"); + Assert.assertNull(map.get("b")); + Assert.assertFalse(b.hasChildren()); + Assert.assertFalse(b.hasParent()); + Assert.assertEquals(a, c.getDeclaredParent()); + Assert.assertEquals(CollUtil.newArrayList(c), new ArrayList<>(a.getDeclaredChildren().values())); + } + + @Test + public void putAllTest() { + ForestMap source = new LinkedForestMap<>(false); + source.linkNodes("a", "b"); + source.linkNodes("b", "c"); + + ForestMap target = new LinkedForestMap<>(false); + target.putAll(source); + + TreeEntry a = target.get("a"); + TreeEntry b = target.get("b"); + TreeEntry c = target.get("c"); + + Assert.assertNotNull(a); + Assert.assertEquals("a", a.getKey()); + Assert.assertEquals(CollUtil.newArrayList(b, c), new ArrayList<>(a.getChildren().values())); + + Assert.assertNotNull(b); + Assert.assertEquals("b", b.getKey()); + Assert.assertEquals(CollUtil.newArrayList(c), new ArrayList<>(b.getChildren().values())); + + Assert.assertNotNull(c); + Assert.assertEquals("c", c.getKey()); + Assert.assertEquals(CollUtil.newArrayList(), new ArrayList<>(c.getChildren().values())); + + } + + @Test + public void clearTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + map.linkNodes("b", "c"); + + TreeEntry a = map.get("a"); + TreeEntry b = map.get("b"); + TreeEntry c = map.get("c"); + Assert.assertFalse(a.hasParent()); + Assert.assertTrue(a.hasChildren()); + Assert.assertTrue(b.hasParent()); + Assert.assertTrue(b.hasChildren()); + Assert.assertTrue(c.hasParent()); + Assert.assertFalse(c.hasChildren()); + + map.clear(); + Assert.assertTrue(map.isEmpty()); + Assert.assertFalse(a.hasParent()); + Assert.assertFalse(a.hasChildren()); + Assert.assertFalse(b.hasParent()); + Assert.assertFalse(b.hasChildren()); + Assert.assertFalse(c.hasParent()); + Assert.assertFalse(c.hasChildren()); + } + + @Test + public void putAllNodeTest() { + ForestMap> map = new LinkedForestMap<>(false); + + Map aMap = MapBuilder. create() + .put("pid", null) + .put("id", "a") + .build(); + Map bMap = MapBuilder. create() + .put("pid", "a") + .put("id", "b") + .build(); + Map cMap = MapBuilder. create() + .put("pid", "b") + .put("id", "c") + .build(); + map.putAllNode(Arrays.asList(aMap, bMap, cMap), m -> m.get("id"), m -> m.get("pid"), true); + + TreeEntry> a = map.get("a"); + Assert.assertNotNull(a); + TreeEntry> b = map.get("b"); + Assert.assertNotNull(b); + TreeEntry> c = map.get("c"); + Assert.assertNotNull(c); + + Assert.assertNull(a.getDeclaredParent()); + Assert.assertEquals(a, b.getDeclaredParent()); + Assert.assertEquals(b, c.getDeclaredParent()); + + Assert.assertEquals(aMap, a.getValue()); + Assert.assertEquals(bMap, b.getValue()); + Assert.assertEquals(cMap, c.getValue()); + } + + @Test + public void putNodeTest() { + ForestMap map = new LinkedForestMap<>(false); + + Assert.assertNull(map.get("a")); + + map.putNode("a", "aaa"); + Assert.assertNotNull(map.get("a")); + Assert.assertEquals("aaa", map.get("a").getValue()); + + map.putNode("a", "aaaa"); + Assert.assertNotNull(map.get("a")); + Assert.assertEquals("aaaa", map.get("a").getValue()); + } + + @Test + public void putLinkedNodesTest() { + ForestMap map = new LinkedForestMap<>(false); + + Assert.assertNull(map.get("a")); + Assert.assertNull(map.get("b")); + + map.putLinkedNodes("a", "b", "bbb"); + Assert.assertNotNull(map.get("a")); + Assert.assertNull(map.get("a").getValue()); + Assert.assertNotNull(map.get("b")); + Assert.assertEquals("bbb", map.get("b").getValue()); + + map.putLinkedNodes("a", "b", "bbbb"); + Assert.assertNotNull(map.get("a")); + Assert.assertNull(map.get("a").getValue()); + Assert.assertNotNull(map.get("b")); + Assert.assertEquals("bbbb", map.get("b").getValue()); + } + + @Test + public void putLinkedNodesTest2() { + ForestMap map = new LinkedForestMap<>(false); + + Assert.assertNull(map.get("a")); + Assert.assertNull(map.get("b")); + + map.putLinkedNodes("a", "aaa", "b", "bbb"); + Assert.assertNotNull(map.get("a")); + Assert.assertEquals("aaa", map.get("a").getValue()); + Assert.assertNotNull(map.get("b")); + Assert.assertEquals("bbb", map.get("b").getValue()); + + map.putLinkedNodes("a", "aaaa", "b", "bbbb"); + Assert.assertNotNull(map.get("a")); + Assert.assertEquals("aaaa", map.get("a").getValue()); + Assert.assertNotNull(map.get("b")); + Assert.assertEquals("bbbb", map.get("b").getValue()); + } + + @Test + public void linkNodesTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + + TreeEntry parent = map.get("a"); + TreeEntry child = map.get("b"); + + Assert.assertNotNull(parent); + Assert.assertEquals("a", parent.getKey()); + Assert.assertEquals(child, parent.getChild("b")); + + Assert.assertNotNull(child); + Assert.assertEquals("b", child.getKey()); + Assert.assertEquals(parent, child.getDeclaredParent()); + } + + @Test + public void unlinkNodeTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + TreeEntry parent = map.get("a"); + TreeEntry child = map.get("b"); + map.unlinkNode("a", "b"); + Assert.assertFalse(child.hasParent()); + Assert.assertFalse(parent.hasChildren()); + } + + @Test + public void getTreeNodesTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + map.linkNodes("b", "c"); + + List expected = CollUtil.newArrayList("a", "b", "c"); + List actual = CollStreamUtil.toList(map.getTreeNodes("a"), TreeEntry::getKey); + Assert.assertEquals(expected, actual); + actual = CollStreamUtil.toList(map.getTreeNodes("b"), TreeEntry::getKey); + Assert.assertEquals(expected, actual); + actual = CollStreamUtil.toList(map.getTreeNodes("c"), TreeEntry::getKey); + Assert.assertEquals(expected, actual); + } + + @Test + public void getRootNodeTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + map.linkNodes("b", "c"); + + TreeEntry root = map.get("a"); + Assert.assertEquals(root, map.getRootNode("a")); + Assert.assertEquals(root, map.getRootNode("b")); + Assert.assertEquals(root, map.getRootNode("c")); + } + + @Test + public void getDeclaredParentNodeTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + map.linkNodes("b", "c"); + TreeEntry a = map.get("a"); + TreeEntry b = map.get("b"); + Assert.assertEquals(a, map.getDeclaredParentNode("b")); + Assert.assertEquals(b, map.getDeclaredParentNode("c")); + } + + @Test + public void getParentNodeTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + map.linkNodes("b", "c"); + TreeEntry a = map.get("a"); + TreeEntry b = map.get("b"); + + Assert.assertEquals(a, map.getParentNode("c", "a")); + Assert.assertEquals(b, map.getParentNode("c", "b")); + Assert.assertEquals(a, map.getParentNode("b", "a")); + Assert.assertNull(map.getParentNode("a", "a")); + } + + @Test + public void containsParentNodeTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + map.linkNodes("b", "c"); + Assert.assertTrue(map.containsParentNode("c", "b")); + Assert.assertTrue(map.containsParentNode("c", "a")); + Assert.assertTrue(map.containsParentNode("b", "a")); + Assert.assertFalse(map.containsParentNode("a", "a")); + } + + @Test + public void containsChildNodeTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + map.linkNodes("b", "c"); + TreeEntry b = map.get("b"); + TreeEntry c = map.get("c"); + + Assert.assertTrue(map.containsChildNode("a", "b")); + Assert.assertTrue(map.containsChildNode("a", "c")); + Assert.assertTrue(map.containsChildNode("b", "c")); + Assert.assertFalse(map.containsChildNode("c", "c")); + } + + @Test + public void getDeclaredChildNodesTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + map.linkNodes("b", "c"); + TreeEntry b = map.get("b"); + TreeEntry c = map.get("c"); + + Assert.assertEquals(CollUtil.newArrayList(b), new ArrayList<>(map.getDeclaredChildNodes("a"))); + Assert.assertEquals(CollUtil.newArrayList(c), new ArrayList<>(map.getDeclaredChildNodes("b"))); + Assert.assertEquals(CollUtil.newArrayList(), new ArrayList<>(map.getDeclaredChildNodes("c"))); + } + + @Test + public void getChildNodesTest() { + ForestMap map = new LinkedForestMap<>(false); + map.linkNodes("a", "b"); + map.linkNodes("b", "c"); + TreeEntry b = map.get("b"); + TreeEntry c = map.get("c"); + + Assert.assertEquals(CollUtil.newArrayList(b, c), new ArrayList<>(map.getChildNodes("a"))); + Assert.assertEquals(CollUtil.newArrayList(c), new ArrayList<>(map.getChildNodes("b"))); + Assert.assertEquals(CollUtil.newArrayList(), new ArrayList<>(map.getChildNodes("c"))); + } + +}