From 80287ec19f37953f3d3f2f406937b121324fa0b4 Mon Sep 17 00:00:00 2001 From: achao Date: Sat, 26 Nov 2022 22:43:38 +0800 Subject: [PATCH] =?UTF-8?q?:trollface:=20=E6=96=B0=E5=A2=9EBeanTree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/cn/hutool/core/tree/BeanTree.java | 207 ++++++++++++++++++ .../cn/hutool/core/tree/BeanTreeTest.java | 129 +++++++++++ 2 files changed, 336 insertions(+) create mode 100644 hutool-core/src/main/java/cn/hutool/core/tree/BeanTree.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/tree/BeanTreeTest.java diff --git a/hutool-core/src/main/java/cn/hutool/core/tree/BeanTree.java b/hutool-core/src/main/java/cn/hutool/core/tree/BeanTree.java new file mode 100644 index 000000000..bf5ac7aa1 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/tree/BeanTree.java @@ -0,0 +1,207 @@ +package cn.hutool.core.tree; + +import cn.hutool.core.lang.Opt; +import cn.hutool.core.lang.func.SerBiConsumer; +import cn.hutool.core.lang.func.SerConsumer; +import cn.hutool.core.lang.func.SerFunction; +import cn.hutool.core.lang.func.SerPredicate; +import cn.hutool.core.stream.EasyStream; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * 本类是用于构建树的工具类,特点是采取lambda,以及满足指定类型的Bean进行树操作 + * Bean需要满足三个属性: + * + * 本类的构建方法是通过{@code BeanTree.of} 进行构建,例如: + *
{@code final BeanTree beanTree = BeanTree.of(JavaBean::getId, JavaBean::getParentId, null, JavaBean::getChildren, JavaBean::setChildren);}
+ * 得到的BeanTree实例可以调用toTree方法,将集合转换为树,例如: + *
{@code final List javaBeanTree = beanTree.toTree(originJavaBeanList);}
+ * 也可以将已有的树转换为集合,例如: + *
{@code final List javaBeanList = beanTree.flat(originJavaBeanTree);}
+ * + * @author VampireAchao + * 最后,引用一句电影经典台词: 无处安放的双手,以及无处安放的灵魂。——《Hello!树先生》 + */ +public class BeanTree> { + + /** + * 主键getter + */ + private final SerFunction idGetter; + /** + * 外键getter + */ + private final SerFunction pidGetter; + /** + * 外键匹配值(保留此属性主要是性能较外键条件匹配稍微好一点) + */ + private final R pidValue; + /** + * 外键匹配条件 + */ + private final SerPredicate parentPredicate; + /** + * 子集getter + */ + private final SerFunction> childrenGetter; + /** + * 子集setter + */ + private final SerBiConsumer> childrenSetter; + + private BeanTree(final SerFunction idGetter, + final SerFunction pidGetter, + final R pidValue, + final SerPredicate parentPredicate, + final SerFunction> childrenGetter, + final SerBiConsumer> childrenSetter) { + this.idGetter = idGetter; + this.pidGetter = pidGetter; + this.pidValue = pidValue; + this.parentPredicate = parentPredicate; + this.childrenGetter = childrenGetter; + this.childrenSetter = childrenSetter; + } + + /** + * 构建BeanTree + * + * @param idGetter 主键getter,例如 {@code JavaBean::getId} + * @param pidGetter 外键getter,例如 {@code JavaBean::getParentId} + * @param pidValue 外键的值,例如 {@code null} + * @param childrenGetter 子集getter,例如 {@code JavaBean::getChildren} + * @param childrenSetter 子集setter,例如 {@code JavaBean::setChildren} + * @param Bean类型 + * @param 主键、外键类型 + * @return BeanTree + */ + public static > BeanTree of(final SerFunction idGetter, + final SerFunction pidGetter, + final R pidValue, + final SerFunction> childrenGetter, + final SerBiConsumer> childrenSetter) { + return new BeanTree<>(idGetter, pidGetter, pidValue, null, childrenGetter, childrenSetter); + } + + /** + * 构建BeanTree + * + * @param idGetter 主键getter,例如 {@code JavaBean::getId} + * @param pidGetter 外键getter,例如 {@code JavaBean::getParentId} + * @param parentPredicate 父节点判断条件,例如 {@code o -> Objects.isNull(o.getParentId())} + * @param childrenGetter 子集getter,例如 {@code JavaBean::getChildren} + * @param childrenSetter 子集setter,例如 {@code JavaBean::setChildren} + * @param Bean类型 + * @param 主键、外键类型 + * @return BeanTree + */ + public static > BeanTree ofMatch(final SerFunction idGetter, + final SerFunction pidGetter, + final SerPredicate parentPredicate, + final SerFunction> childrenGetter, + final SerBiConsumer> childrenSetter) { + return new BeanTree<>(idGetter, pidGetter, null, parentPredicate, childrenGetter, childrenSetter); + } + + /** + * 将集合转换为树 + * + * @param list 集合 + * @return 转换后的树 + */ + public List toTree(final List list) { + if (Objects.isNull(parentPredicate)) { + final Map> pIdValuesMap = EasyStream.of(list).filter(e -> Objects.nonNull(idGetter.apply(e))).group(pidGetter); + final List parents = pIdValuesMap.getOrDefault(pidValue, new ArrayList<>()); + getChildrenFromMapByPidAndSet(pIdValuesMap); + return parents; + } + final List parents = new ArrayList<>(list.size()); + final Map> pIdValuesMap = EasyStream.of(list).filter(e -> { + if (parentPredicate.test(e)) { + parents.add(e); + } + return Objects.nonNull(idGetter.apply(e)); + }).group(pidGetter); + getChildrenFromMapByPidAndSet(pIdValuesMap); + return parents; + } + + /** + * 将树扁平化为集合,相当于将树里的所有节点都放到一个集合里 + * + * @param tree 树 + * @return 集合 + */ + @SuppressWarnings("unchecked") + public List flat(final List tree) { + final AtomicReference>> recursiveRef = new AtomicReference<>(); + final Function> recursive = e -> EasyStream.of(childrenGetter.apply(e)).flat(recursiveRef.get()).unshift(e); + recursiveRef.set(recursive); + return EasyStream.of(tree).flat(recursive).peek(e -> childrenSetter.accept(e, null)).toList(); + } + + /** + * 树的过滤操作,本方法一般适用于寻找某人所在部门以及所有上级部门类似的逻辑 + * 通过{@link SerPredicate}指定的过滤规则,本节点或子节点满足过滤条件,则保留当前节点,否则抛弃节点及其子节点 + * + * @param tree 树 + * @param condition 节点过滤规则函数,只需处理本级节点本身即可,{@link SerPredicate#test(Object)}为{@code true}保留 + * @return 过滤后的树 + */ + public List filter(final List tree, final SerPredicate condition) { + final AtomicReference> recursiveRef = new AtomicReference<>(); + final Predicate recursive = SerPredicate.multiOr(condition::test, + e -> Opt.ofEmptyAble(childrenGetter.apply(e)) + .map(children -> EasyStream.of(children).filter(recursiveRef.get()).toList()) + .peek(children -> childrenSetter.accept(e, children)) + .filter(s -> !s.isEmpty()).isPresent()); + recursiveRef.set(recursive); + return EasyStream.of(tree).filter(recursive).toList(); + } + + /** + * 树节点遍历操作 + * + * @param tree 数 + * @param action 操作 + * @return 树 + */ + public List forEach(final List tree, final SerConsumer action) { + final AtomicReference> recursiveRef = new AtomicReference<>(); + final Consumer recursive = SerConsumer.multi(action::accept, + e -> Opt.ofEmptyAble(childrenGetter.apply(e)) + .peek(children -> EasyStream.of(children).forEach(recursiveRef.get()))); + recursiveRef.set(recursive); + EasyStream.of(tree).forEach(recursive); + return tree; + } + + /** + * 内联函数,获取子集并设置到父节点 + * + * @param pIdValuesMap 父id与子集的映射 + */ + private void getChildrenFromMapByPidAndSet(final Map> pIdValuesMap) { + EasyStream.of(pIdValuesMap.values()).flat(Function.identity()) + .forEach(value -> { + final List children = pIdValuesMap.get(idGetter.apply(value)); + if (children != null) { + childrenSetter.accept(value, children); + } + }); + } + +} diff --git a/hutool-core/src/test/java/cn/hutool/core/tree/BeanTreeTest.java b/hutool-core/src/test/java/cn/hutool/core/tree/BeanTreeTest.java new file mode 100644 index 000000000..5db467bd9 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/tree/BeanTreeTest.java @@ -0,0 +1,129 @@ +package cn.hutool.core.tree; + +import cn.hutool.core.stream.EasyStream; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.Tolerate; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Comparator; +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; + +/** + * TreeHelperTest + * + * @author VampireAchao + */ +public class BeanTreeTest { + + @Data + @Builder + private static class JavaBean { + @Tolerate + public JavaBean() { + // this is an accessible parameterless constructor. + } + + private String name; + private Integer age; + private Long id; + private Long parentId; + private List children; + private Boolean matchParent; + } + + List originJavaBeanList; + List originJavaBeanTree; + BeanTree beanTree; + + @Before + public void setUp() { + originJavaBeanList = EasyStream + .of( + JavaBean.builder().id(1L).name("dromara").matchParent(true).build(), + JavaBean.builder().id(2L).name("baomidou").matchParent(true).build(), + JavaBean.builder().id(3L).name("hutool").parentId(1L).build(), + JavaBean.builder().id(4L).name("sa-token").parentId(1L).build(), + JavaBean.builder().id(5L).name("mybatis-plus").parentId(2L).build(), + JavaBean.builder().id(6L).name("looly").parentId(3L).build(), + JavaBean.builder().id(7L).name("click33").parentId(4L).build(), + JavaBean.builder().id(8L).name("jobob").parentId(5L).build() + ).toList(); + originJavaBeanTree = asList( + JavaBean.builder().id(1L).name("dromara").matchParent(true) + .children(asList( + JavaBean.builder().id(3L).name("hutool").parentId(1L) + .children(singletonList(JavaBean.builder().id(6L).name("looly").parentId(3L).build())) + .build(), + JavaBean.builder().id(4L).name("sa-token").parentId(1L) + .children(singletonList(JavaBean.builder().id(7L).name("click33").parentId(4L).build())) + .build())) + .build(), + JavaBean.builder().id(2L).name("baomidou").matchParent(true) + .children(singletonList( + JavaBean.builder().id(5L).name("mybatis-plus").parentId(2L) + .children(singletonList( + JavaBean.builder().id(8L).name("jobob").parentId(5L).build() + )) + .build())) + .build() + ); + beanTree = BeanTree.of(JavaBean::getId, JavaBean::getParentId, null, JavaBean::getChildren, JavaBean::setChildren); + } + + @Test + public void testToTree() { + final List javaBeanTree = beanTree.toTree(originJavaBeanList); + Assert.assertEquals(originJavaBeanTree, javaBeanTree); + final BeanTree conditionBeanTree = BeanTree.ofMatch(JavaBean::getId, JavaBean::getParentId, s -> Boolean.TRUE.equals(s.getMatchParent()), JavaBean::getChildren, JavaBean::setChildren); + Assert.assertEquals(originJavaBeanTree, conditionBeanTree.toTree(originJavaBeanList)); + } + + @Test + public void testFlat() { + final List javaBeanList = beanTree.flat(originJavaBeanTree); + javaBeanList.sort(Comparator.comparing(JavaBean::getId)); + Assert.assertEquals(originJavaBeanList, javaBeanList); + } + + @Test + public void testFilter() { + final List javaBeanTree = beanTree.filter(originJavaBeanTree, s -> "looly".equals(s.getName())); + Assert.assertEquals(singletonList( + JavaBean.builder().id(1L).name("dromara").matchParent(true) + .children(singletonList(JavaBean.builder().id(3L).name("hutool").parentId(1L) + .children(singletonList(JavaBean.builder().id(6L).name("looly").parentId(3L).build())) + .build())) + .build()), + javaBeanTree); + } + + @Test + public void testForeach() { + final List javaBeanList = beanTree.forEach(originJavaBeanTree, s -> s.setName("【open source】" + s.getName())); + Assert.assertEquals(asList( + JavaBean.builder().id(1L).name("【open source】dromara").matchParent(true) + .children(asList(JavaBean.builder().id(3L).name("【open source】hutool").parentId(1L) + .children(singletonList(JavaBean.builder().id(6L).name("【open source】looly").parentId(3L).build())) + .build(), + JavaBean.builder().id(4L).name("【open source】sa-token").parentId(1L) + .children(singletonList(JavaBean.builder().id(7L).name("【open source】click33").parentId(4L).build())) + .build())) + .build(), + JavaBean.builder().id(2L).name("【open source】baomidou").matchParent(true) + .children(singletonList( + JavaBean.builder().id(5L).name("【open source】mybatis-plus").parentId(2L) + .children(singletonList( + JavaBean.builder().id(8L).name("【open source】jobob").parentId(5L).build() + )) + .build())) + .build() + ), javaBeanList); + } + +}