You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@uniffle.apache.org by zu...@apache.org on 2023/06/29 09:19:32 UTC

[incubator-uniffle] branch master updated: [#978][Improvement] Provides a tool class to format CLI output content (#979)

This is an automated email from the ASF dual-hosted git repository.

zuston pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-uniffle.git


The following commit(s) were added to refs/heads/master by this push:
     new f622aca8 [#978][Improvement] Provides a tool class to format CLI output content (#979)
f622aca8 is described below

commit f622aca8a453000874d6885654ff3029d1e38fc8
Author: yl09099 <33...@users.noreply.github.com>
AuthorDate: Thu Jun 29 17:19:26 2023 +0800

    [#978][Improvement] Provides a tool class to format CLI output content (#979)
    
    ### What changes were proposed in this pull request?
    
    Provides a tool class to format CLI output content.
    
    ### Why are the changes needed?
    
    This tool is to make CLI output layout much better, especially for the apps/servers display
    
    ### Does this PR introduce _any_ user-facing change?
    
    The CLI command display is more beautiful.Similar to:
    
    ![image](https://github.com/apache/incubator-uniffle/assets/33595968/2829da56-fdf6-4934-95dc-e59ecef2e128)
    
    ### How was this patch tested?
    
    Added UT.
---
 .../org/apache/uniffle/cli/CLIContentUtils.java    | 278 +++++++++++++++++++++
 .../apache/uniffle/cli/CLIContentUtilsTest.java    | 111 ++++++++
 cli/src/test/resources/CLIContentResult            |  32 +++
 3 files changed, 421 insertions(+)

diff --git a/cli/src/main/java/org/apache/uniffle/cli/CLIContentUtils.java b/cli/src/main/java/org/apache/uniffle/cli/CLIContentUtils.java
new file mode 100644
index 00000000..bf8cfcb5
--- /dev/null
+++ b/cli/src/main/java/org/apache/uniffle/cli/CLIContentUtils.java
@@ -0,0 +1,278 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.uniffle.cli;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The main core class that generates the ASCII TABLE.
+ */
+public final class CLIContentUtils {
+  /** Table title. */
+  private String title;
+  /** Last processed row type. */
+  private TableRowType lastTableRowType;
+  /** StringBuilder object used to concatenate strings. */
+  private StringBuilder join;
+  /** An ordered Map that holds each row of data. */
+  private List<TableRow> tableRows;
+  /** Maps the maximum length of each column. */
+  private Map<Integer, Integer> maxColMap;
+
+  /**
+   * Contains the title constructor.
+   * @param title titleName
+   */
+  public CLIContentUtils(String title) {
+    this.init();
+    this.title = title;
+  }
+
+  /**
+   * Initialize the data.
+   */
+  private void init() {
+    this.join = new StringBuilder();
+    this.tableRows = new ArrayList<>();
+    this.maxColMap = new HashMap<>();
+  }
+
+  /**
+   * Adds elements from the collection to the header data in the table.
+   * @param headers Header data
+   * @return FormattingCLIUtils object
+   */
+  public CLIContentUtils addHeaders(List<?> headers) {
+    return this.appendRows(TableRowType.HEADER, headers.toArray());
+  }
+
+  /**
+   * Adds a row of normal data to the table.
+   * @param objects Common row data
+   * @return FormattingCLIUtils object
+   */
+  public CLIContentUtils addLine(Object... objects) {
+    return this.appendRows(TableRowType.LINE, objects);
+  }
+
+  /**
+   * Adds the middle row of data to the table.
+   * @param tableRowType TableRowType
+   * @param objects Table row data
+   * @return FormattingCLIUtils object
+   */
+  private CLIContentUtils appendRows(TableRowType tableRowType, Object... objects) {
+    if (objects != null && objects.length > 0) {
+      int len = objects.length;
+      if (this.maxColMap.size() > len) {
+        throw new IllegalArgumentException("The number of columns that inserted a row "
+            + "of data into the table is different from the number of previous columns, check!");
+      }
+      List<String> lines = new ArrayList<>();
+      for (int i = 0; i < len; i++) {
+        Object o = objects[i];
+        String value = o == null ? "null" : o.toString();
+        lines.add(value);
+        Integer maxColSize = this.maxColMap.get(i);
+        if (maxColSize == null) {
+          this.maxColMap.put(i, value.length());
+          continue;
+        }
+        if (value.length() > maxColSize) {
+          this.maxColMap.put(i, value.length());
+        }
+      }
+      this.tableRows.add(new TableRow(tableRowType, lines));
+    }
+    return this;
+  }
+
+  /**
+   * Builds the string for the row of the table title.
+   */
+  private void buildTitle() {
+    if (this.title != null) {
+      int maxTitleSize = 0;
+      for (Integer maxColSize : this.maxColMap.values()) {
+        maxTitleSize += maxColSize;
+      }
+      maxTitleSize += 3 * (this.maxColMap.size() - 1);
+      if (this.title.length() > maxTitleSize) {
+        this.title = this.title.substring(0, maxTitleSize);
+      }
+      this.join.append("+");
+      for (int i = 0; i < maxTitleSize + 2; i++) {
+        this.join.append("-");
+      }
+      this.join.append("+\n")
+          .append("|")
+          .append(StrUtils.center(this.title, maxTitleSize + 2, ' '))
+          .append("|\n");
+      this.lastTableRowType = TableRowType.TITLE;
+    }
+  }
+
+  /**
+   * Build the table, first build the title, and then walk through each row of data to build.
+   */
+  private void buildTable() {
+    this.buildTitle();
+    for (int i = 0, len = this.tableRows.size(); i < len; i++) {
+      List<String> data = this.tableRows.get(i).data;
+      switch (this.tableRows.get(i).tableRowType) {
+        case HEADER:
+          if (this.lastTableRowType != TableRowType.HEADER) {
+            this.buildRowBorder(data);
+          }
+          this.buildRowLine(data);
+          this.buildRowBorder(data);
+          break;
+        case LINE:
+          this.buildRowLine(data);
+          if (i == len - 1) {
+            this.buildRowBorder(data);
+          }
+          break;
+        default:
+          break;
+      }
+    }
+  }
+
+  /**
+   * Method to build a border row.
+   * @param data dataLine
+   */
+  private void buildRowBorder(List<String> data) {
+    this.join.append("+");
+    for (int i = 0, len = data.size(); i < len; i++) {
+      for (int j = 0; j < this.maxColMap.get(i) + 2; j++) {
+        this.join.append("-");
+      }
+      this.join.append("+");
+    }
+    this.join.append("\n");
+  }
+
+  /**
+   * A way to build rows of data.
+   * @param data dataLine
+   */
+  private void buildRowLine(List<String> data) {
+    this.join.append("|");
+    for (int i = 0, len = data.size(); i < len; i++) {
+      this.join.append(StrUtils.center(data.get(i), this.maxColMap.get(i) + 2, ' '))
+          .append("|");
+    }
+    this.join.append("\n");
+  }
+
+  /**
+   * Rendering is born as a result.
+   * @return ASCII string of Table
+   */
+  public String render() {
+    this.buildTable();
+    return this.join.toString();
+  }
+
+  /**
+   * The type of each table row and the entity class of the data.
+   */
+  private static class TableRow {
+    private TableRowType tableRowType;
+    private List<String> data;
+
+    TableRow(TableRowType tableRowType, List<String> data) {
+      this.tableRowType = tableRowType;
+      this.data = data;
+    }
+  }
+
+  /**
+   * An enumeration class that distinguishes between table headers and normal table data.
+   */
+  private enum TableRowType {
+    TITLE, HEADER, LINE
+  }
+
+  /**
+   * String utility class.
+   */
+  private static final class StrUtils {
+    /**
+     * Puts a string in the middle of a given size.
+     * @param str Character string
+     * @param size Total size
+     * @param padChar Fill character
+     * @return String result
+     */
+    private static String center(String str, int size, char padChar) {
+      if (str != null && size > 0) {
+        int strLen = str.length();
+        int pads = size - strLen;
+        if (pads > 0) {
+          str = leftPad(str, strLen + pads / 2, padChar);
+          str = rightPad(str, size, padChar);
+        }
+      }
+      return str;
+    }
+
+    /**
+     * Left-fill the given string and size.
+     * @param str String
+     * @param size totalSize
+     * @param padChar Fill character
+     * @return String result
+     */
+    private static String leftPad(final String str, int size, char padChar) {
+      int pads = size - str.length();
+      return pads <= 0 ? str : repeat(padChar, pads).concat(str);
+    }
+
+    /**
+     * Right-fill the given string and size.
+     * @param str String
+     * @param size totalSize
+     * @param padChar Fill character
+     * @return String result
+     */
+    private static String rightPad(final String str, int size, char padChar) {
+      int pads = size - str.length();
+      return pads <= 0 ? str : str.concat(repeat(padChar, pads));
+    }
+
+    /**
+     * Re-fill characters as strings.
+     * @param ch String
+     * @param repeat Number of repeats
+     * @return String
+     */
+    private static String repeat(char ch, int repeat) {
+      char[] buf = new char[repeat];
+      for (int i = repeat - 1; i >= 0; i--) {
+        buf[i] = ch;
+      }
+      return new String(buf);
+    }
+  }
+}
diff --git a/cli/src/test/java/org/apache/uniffle/cli/CLIContentUtilsTest.java b/cli/src/test/java/org/apache/uniffle/cli/CLIContentUtilsTest.java
new file mode 100644
index 00000000..12275154
--- /dev/null
+++ b/cli/src/test/java/org/apache/uniffle/cli/CLIContentUtilsTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.uniffle.cli;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.text.DecimalFormat;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class CLIContentUtilsTest {
+
+  @Test
+  public void testTableFormat() throws IOException, URISyntaxException {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    PrintWriter writer = new PrintWriter(baos);
+    String titleString = " 5 ShuffleServers were found";
+    List<String> headerStrings = Arrays.asList("HostName", "IP", "Port", "UsedMem",
+        "PreAllocatedMem", "AvaliableMem", "TotalMem", "Status");
+    CLIContentUtils formattingCLIUtils = new CLIContentUtils(titleString)
+        .addHeaders(headerStrings);
+    DecimalFormat df = new DecimalFormat("#.00");
+    formattingCLIUtils.addLine("uniffledata-hostname01", "10.93.23.11",
+        "9909", df.format(0.59 * 100) + "%",
+        df.format(0.74 * 100) + "%",
+        df.format(0.34 * 100) + "%",
+        df.format(105) + "G",
+        "ACTIVE");
+    formattingCLIUtils.addLine("uniffledata-hostname02", "10.93.23.12",
+        "9909", df.format(0.54 * 100) + "%",
+        df.format(0.78 * 100) + "%",
+        df.format(0.55 * 100) + "%",
+        df.format(105) + "G",
+        "ACTIVE");
+    formattingCLIUtils.addLine("uniffledata-hostname03", "10.93.23.13",
+        "9909", df.format(0.55 * 100) + "%",
+        df.format(0.56 * 100) + "%",
+        df.format(0.79 * 100) + "%",
+        df.format(105) + "G",
+        "ACTIVE");
+    formattingCLIUtils.addLine("uniffledata-hostname04", "10.93.23.14",
+        "9909", df.format(0.34 * 100) + "%",
+        df.format(0.84 * 100) + "%",
+        df.format(0.64 * 100) + "%",
+        df.format(105) + "G",
+        "ACTIVE");
+    formattingCLIUtils.addLine("uniffledata-hostname05", "10.93.23.15",
+        "9909", df.format(0.34 * 100) + "%",
+        df.format(0.89 * 100) + "%",
+        df.format(0.16 * 100) + "%",
+        df.format(105) + "G",
+        "ACTIVE");
+    formattingCLIUtils.addLine("uniffledata-hostname06", "10.93.23.16",
+        "9909", df.format(0.34 * 100) + "%",
+        df.format(0.45 * 100) + "%",
+        df.format(0.67 * 100) + "%",
+        df.format(105) + "G",
+        "ACTIVE");
+    formattingCLIUtils.addLine("uniffledata-hostname07", "10.93.23.17",
+        "9909", df.format(0.34 * 100) + "%",
+        df.format(0.15 * 100) + "%",
+        df.format(0.98 * 100) + "%",
+        df.format(105) + "G",
+        "ACTIVE");
+    formattingCLIUtils.addLine("uniffledata-hostname08", "10.93.23.18",
+        "9909", df.format(0.34 * 100) + "%",
+        df.format(0.77 * 100) + "%",
+        df.format(0.67 * 100) + "%",
+        df.format(105) + "G",
+        "ACTIVE");
+    formattingCLIUtils.addLine("rssdata-hostname09", "10.93.23.19",
+        "9909", df.format(0.14 * 100) + "%",
+        df.format(0.44 * 100) + "%",
+        df.format(0.68 * 100) + "%",
+        df.format(100) + "G",
+        "LOST");
+    StringBuilder resultStrBuilder = new StringBuilder();
+    List<String> lines = Files.readAllLines(Paths
+        .get(this.getClass().getResource("/CLIContentResult").toURI()));
+    for (String line : lines) {
+      if (line != null && line.length() != 0 && !line.startsWith("#")) {
+        resultStrBuilder.append(line + "\n");
+      }
+    }
+    String expectStr = resultStrBuilder.toString();
+    assertEquals(expectStr, formattingCLIUtils.render());
+  }
+}
diff --git a/cli/src/test/resources/CLIContentResult b/cli/src/test/resources/CLIContentResult
new file mode 100644
index 00000000..bf2d44d2
--- /dev/null
+++ b/cli/src/test/resources/CLIContentResult
@@ -0,0 +1,32 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
++------------------------------------------------------------------------------------------------------------+
+|                                         5 ShuffleServers were found                                        |
++------------------------+-------------+------+---------+-----------------+--------------+----------+--------+
+|        HostName        |     IP      | Port | UsedMem | PreAllocatedMem | AvaliableMem | TotalMem | Status |
++------------------------+-------------+------+---------+-----------------+--------------+----------+--------+
+| uniffledata-hostname01 | 10.93.23.11 | 9909 | 59.00%  |     74.00%      |    34.00%    | 105.00G  | ACTIVE |
+| uniffledata-hostname02 | 10.93.23.12 | 9909 | 54.00%  |     78.00%      |    55.00%    | 105.00G  | ACTIVE |
+| uniffledata-hostname03 | 10.93.23.13 | 9909 | 55.00%  |     56.00%      |    79.00%    | 105.00G  | ACTIVE |
+| uniffledata-hostname04 | 10.93.23.14 | 9909 | 34.00%  |     84.00%      |    64.00%    | 105.00G  | ACTIVE |
+| uniffledata-hostname05 | 10.93.23.15 | 9909 | 34.00%  |     89.00%      |    16.00%    | 105.00G  | ACTIVE |
+| uniffledata-hostname06 | 10.93.23.16 | 9909 | 34.00%  |     45.00%      |    67.00%    | 105.00G  | ACTIVE |
+| uniffledata-hostname07 | 10.93.23.17 | 9909 | 34.00%  |     15.00%      |    98.00%    | 105.00G  | ACTIVE |
+| uniffledata-hostname08 | 10.93.23.18 | 9909 | 34.00%  |     77.00%      |    67.00%    | 105.00G  | ACTIVE |
+|   rssdata-hostname09   | 10.93.23.19 | 9909 | 14.00%  |     44.00%      |    68.00%    | 100.00G  |  LOST  |
++------------------------+-------------+------+---------+-----------------+--------------+----------+--------+