diff --git a/src/main/java/run/halo/app/theme/finders/impl/MenuFinderImpl.java b/src/main/java/run/halo/app/theme/finders/impl/MenuFinderImpl.java index 60a2689a6..95e819283 100644 --- a/src/main/java/run/halo/app/theme/finders/impl/MenuFinderImpl.java +++ b/src/main/java/run/halo/app/theme/finders/impl/MenuFinderImpl.java @@ -1,7 +1,8 @@ package run.halo.app.theme.finders.impl; +import java.time.Instant; +import java.util.Collection; import java.util.Comparator; -import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -9,6 +10,7 @@ import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; import org.springframework.util.CollectionUtils; +import org.springframework.util.comparator.Comparators; import run.halo.app.core.extension.Menu; import run.halo.app.core.extension.MenuItem; import run.halo.app.extension.ReactiveExtensionClient; @@ -51,7 +53,6 @@ public class MenuFinderImpl implements MenuFinder { return menuVos.get(0); } - List listAll() { return client.list(Menu.class, null, null) .map(MenuVo::from) @@ -60,7 +61,7 @@ public class MenuFinderImpl implements MenuFinder { } List listAsTree() { - List menuItemVos = populateParentName(listAllMenuItem()); + Collection menuItemVos = populateParentName(listAllMenuItem()); List treeList = listToTree(menuItemVos); Map nameItemRootNodeMap = treeList.stream() .collect(Collectors.toMap(item -> item.getMetadata().getName(), Function.identity())); @@ -74,62 +75,65 @@ public class MenuFinderImpl implements MenuFinder { List menuItems = menuItemNames.stream() .map(nameItemRootNodeMap::get) .filter(Objects::nonNull) + .sorted(defaultTreeNodeComparator()) .toList(); return menuVo.withMenuItems(menuItems); }) .toList(); } - static List listToTree(List list) { - Map> nameIdentityMap = list.stream() - .filter(item -> item.getParentName() != null) + static List listToTree(Collection list) { + Map> parentNameIdentityMap = list.stream() + .filter(menuItemVo -> menuItemVo.getParentName() != null) .collect(Collectors.groupingBy(MenuItemVo::getParentName)); - list.forEach(node -> node.setChildren(nameIdentityMap.get(node.getMetadata().getName()))); - // clear map to release memory - nameIdentityMap.clear(); + + list.forEach(node -> { + // sort children + List children = + parentNameIdentityMap.getOrDefault(node.getMetadata().getName(), List.of()) + .stream() + .sorted(defaultTreeNodeComparator()) + .toList(); + node.setChildren(children); + }); + return list.stream() .filter(v -> v.getParentName() == null) .collect(Collectors.toList()); } List listAllMenuItem() { - Function priority = menuItem -> menuItem.getSpec().getPriority(); - Function name = menuItem -> menuItem.getMetadata().getName(); - - return client.list(MenuItem.class, null, - Comparator.comparing(priority).thenComparing(name).reversed() - ) + return client.list(MenuItem.class, null, null) .map(MenuItemVo::from) .collectList() .block(); } - static List populateParentName(List menuItemVos) { + static Comparator defaultTreeNodeComparator() { + Function priority = menuItem -> menuItem.getSpec().getPriority(); + Function createTime = menuItem -> menuItem.getMetadata() + .getCreationTimestamp(); + Function name = menuItem -> menuItem.getMetadata().getName(); + + return Comparator.comparing(priority) + .thenComparing(createTime, Comparators.nullsLow()) + .thenComparing(name); + } + + static Collection populateParentName(List menuItemVos) { Map nameIdentityMap = menuItemVos.stream() .collect(Collectors.toMap(menuItem -> menuItem.getMetadata().getName(), Function.identity())); - Map treeVoMap = new HashMap<>(); - // populate parentName - menuItemVos.forEach(menuItemVo -> { - final String parentName = menuItemVo.getMetadata().getName(); - treeVoMap.putIfAbsent(parentName, menuItemVo); - LinkedHashSet children = menuItemVo.getSpec().getChildren(); - if (CollectionUtils.isEmpty(children)) { - return; + nameIdentityMap.forEach((name, value) -> { + LinkedHashSet children = value.getSpec().getChildren(); + if (children != null) { + for (String child : children) { + MenuItemVo childNode = nameIdentityMap.get(child); + childNode.setParentName(name); + } } - children.forEach(childrenName -> { - MenuItemVo childrenVo = nameIdentityMap.get(childrenName); - childrenVo.setParentName(parentName); - treeVoMap.putIfAbsent(childrenVo.getMetadata().getName(), childrenVo); - }); }); - // clear map to release memory - nameIdentityMap.clear(); - Function priorityCmp = menuItem -> menuItem.getSpec().getPriority(); - return treeVoMap.values() - .stream() - .sorted(Comparator.comparing(priorityCmp)) - .toList(); + return nameIdentityMap.values(); } } diff --git a/src/main/java/run/halo/app/theme/finders/vo/MenuItemVo.java b/src/main/java/run/halo/app/theme/finders/vo/MenuItemVo.java index 3015d62ef..f30ccfbcd 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/MenuItemVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/MenuItemVo.java @@ -4,6 +4,7 @@ import java.util.List; import lombok.Builder; import lombok.Data; import lombok.ToString; +import org.apache.commons.lang3.StringUtils; import run.halo.app.core.extension.MenuItem; import run.halo.app.extension.MetadataOperator; @@ -16,7 +17,7 @@ import run.halo.app.extension.MetadataOperator; @Data @ToString @Builder -public class MenuItemVo { +public class MenuItemVo implements VisualizableTreeNode { MetadataOperator metadata; @@ -28,6 +29,16 @@ public class MenuItemVo { String parentName; + /** + * Gets menu item's display name. + */ + public String getDisplayName() { + if (status != null && StringUtils.isNotBlank(status.getDisplayName())) { + return status.getDisplayName(); + } + return spec.getDisplayName(); + } + /** * Convert {@link MenuItem} to {@link MenuItemVo}. * @@ -43,4 +54,9 @@ public class MenuItemVo { .children(List.of()) .build(); } + + @Override + public String nodeText() { + return getDisplayName(); + } } diff --git a/src/main/java/run/halo/app/theme/finders/vo/MenuVo.java b/src/main/java/run/halo/app/theme/finders/vo/MenuVo.java index 6b4c01fd0..bfeda813e 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/MenuVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/MenuVo.java @@ -1,5 +1,6 @@ package run.halo.app.theme.finders.vo; +import java.util.Iterator; import java.util.List; import lombok.Builder; import lombok.ToString; @@ -39,4 +40,20 @@ public class MenuVo { .menuItems(List.of()) .build(); } + + public void print(StringBuilder buffer) { + buffer.append(getSpec().getDisplayName()); + buffer.append('\n'); + if (menuItems == null) { + return; + } + for (Iterator it = menuItems.iterator(); it.hasNext(); ) { + MenuItemVo next = it.next(); + if (it.hasNext()) { + next.print(buffer, "├── ", "│ "); + } else { + next.print(buffer, "└── ", " "); + } + } + } } diff --git a/src/main/java/run/halo/app/theme/finders/vo/VisualizableTreeNode.java b/src/main/java/run/halo/app/theme/finders/vo/VisualizableTreeNode.java new file mode 100644 index 000000000..5f9b0ebf9 --- /dev/null +++ b/src/main/java/run/halo/app/theme/finders/vo/VisualizableTreeNode.java @@ -0,0 +1,37 @@ +package run.halo.app.theme.finders.vo; + +import java.util.Iterator; +import java.util.List; + +/** + * Show Tree Hierarchy. + * + * @author guqing + * @since 2.0.0 + */ +public interface VisualizableTreeNode> { + + /** + * Visualize tree node. + */ + default void print(StringBuilder buffer, String prefix, String childrenPrefix) { + buffer.append(prefix); + buffer.append(nodeText()); + buffer.append('\n'); + if (getChildren() == null) { + return; + } + for (Iterator it = getChildren().iterator(); it.hasNext(); ) { + T next = it.next(); + if (it.hasNext()) { + next.print(buffer, childrenPrefix + "├── ", childrenPrefix + "│ "); + } else { + next.print(buffer, childrenPrefix + "└── ", childrenPrefix + " "); + } + } + } + + String nodeText(); + + List getChildren(); +} diff --git a/src/test/java/run/halo/app/theme/finders/impl/MenuFinderImplTest.java b/src/test/java/run/halo/app/theme/finders/impl/MenuFinderImplTest.java index 71c87fcca..e738dc3f4 100644 --- a/src/test/java/run/halo/app/theme/finders/impl/MenuFinderImplTest.java +++ b/src/test/java/run/halo/app/theme/finders/impl/MenuFinderImplTest.java @@ -1,19 +1,18 @@ package run.halo.app.theme.finders.impl; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; -import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import org.skyscreamer.jsonassert.JSONAssert; import reactor.core.publisher.Flux; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; @@ -21,7 +20,6 @@ import run.halo.app.core.extension.Menu; import run.halo.app.core.extension.MenuItem; import run.halo.app.extension.Metadata; import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.infra.utils.JsonUtils; import run.halo.app.theme.finders.vo.MenuVo; /** @@ -44,7 +42,7 @@ class MenuFinderImplTest { } @Test - void listAsTree() throws JSONException { + void listAsTree() { Tuple2, List> tuple = testTree(); Mockito.when(client.list(eq(Menu.class), eq(null), eq(null))) .thenReturn(Flux.fromIterable(tuple.getT1())); @@ -52,141 +50,29 @@ class MenuFinderImplTest { .thenReturn(Flux.fromIterable(tuple.getT2())); List menuVos = menuFinder.listAsTree(); - JSONAssert.assertEquals(""" - [ - { - "metadata": { - "name": "D" - }, - "spec": { - "displayName": "D", - "menuItems": [ - "E" - ] - }, - "menuItems": [ - { - "metadata": { - "name": "E" - }, - "spec": { - "displayName": "E", - "priority": 0, - "children": [ - "A", - "C" - ] - }, - "status": {}, - "children": [ - { - "metadata": { - "name": "A" - }, - "spec": { - "displayName": "A", - "priority": 0, - "children": [ - "B" - ] - }, - "status": {}, - "children": [ - { - "metadata": { - "name": "B" - }, - "spec": { - "displayName": "B", - "priority": 0 - }, - "status": {}, - "parentName": "A" - } - ], - "parentName": "E" - }, - { - "metadata": { - "name": "C" - }, - "spec": { - "displayName": "C", - "priority": 0 - }, - "status": {}, - "parentName": "E" - } - ] - } - ] - }, - { - "metadata": { - "name": "X" - }, - "spec": { - "displayName": "X", - "menuItems": [ - "G" - ] - }, - "menuItems": [ - { - "metadata": { - "name": "G" - }, - "spec": { - "displayName": "G", - "priority": 0 - }, - "status": {} - } - ] - }, - { - "metadata": { - "name": "Y" - }, - "spec": { - "displayName": "Y", - "menuItems": [ - "F" - ] - }, - "menuItems": [ - { - "metadata": { - "name": "F" - }, - "spec": { - "displayName": "F", - "priority": 0, - "children": [ - "H" - ] - }, - "status": {}, - "children": [ - { - "metadata": { - "name": "H" - }, - "spec": { - "displayName": "H", - "priority": 0 - }, - "status": {}, - "parentName": "F" - } - ] - } - ] - } - ] - """, - JsonUtils.objectToJson(menuVos), - true); + assertThat(visualizeTree(menuVos)).isEqualTo(""" + D + └── E + ├── A + │ └── B + └── C + X + └── G + Y + └── F + └── H + """); + } + + /** + * Visualize a tree. + */ + String visualizeTree(List menuVos) { + StringBuilder stringBuilder = new StringBuilder(); + for (MenuVo menuVo : menuVos) { + menuVo.print(stringBuilder); + } + return stringBuilder.toString(); } Tuple2, List> testTree() {