Skip to content

Commit

Permalink
[#978][Improvement] Provides a tool class to format CLI output content (
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
yl09099 committed Jun 29, 2023
1 parent b9e99ea commit f622aca
Show file tree
Hide file tree
Showing 3 changed files with 421 additions and 0 deletions.
278 changes: 278 additions & 0 deletions cli/src/main/java/org/apache/uniffle/cli/CLIContentUtils.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
111 changes: 111 additions & 0 deletions cli/src/test/java/org/apache/uniffle/cli/CLIContentUtilsTest.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading

0 comments on commit f622aca

Please sign in to comment.