This is an automated email from the ASF dual-hosted git repository.
jmclean pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 4ffe56ccd7 [#6481] improve(CLI): Refactor table output format (#6483)
4ffe56ccd7 is described below
commit 4ffe56ccd770e6614e1aab68311dd22f3e308479
Author: Lord of Abyss <[email protected]>
AuthorDate: Fri Feb 21 17:15:20 2025 +0800
[#6481] improve(CLI): Refactor table output format (#6483)
### What changes were proposed in this pull request?
Refactor table output format, make it easier to test and scale.
### Why are the changes needed?
Fix: #6481
### Does this PR introduce _any_ user-facing change?
No
### How was this patch tested?
table format test
```bash
gcli metalake list -i --output table
+-------------------+
| Metalake |
+-------------------+
| demo |
| cli_demo |
| demo_metalake |
| test_cli_metalake |
| tyy |
| demo3 |
+-------------------+
gcli metalake details -i --output table -m demo_metalake
+---------------+-------------+
| Metalake | Comment |
+---------------+-------------+
| demo_metalake | new comment |
+---------------+-------------+
gcli catalog list -i --output table -m demo_metalake
+-------------------+
| Catalog |
+-------------------+
| File |
| Hive_catalog |
| Iceberg_catalog |
| Mysql_catalog |
| Test_hive_catalog |
+-------------------+
gcli catalog details --name Hive_catalog -i --output table -m
demo_metalake
+--------------+------------+----------+-------------+
| Catalog | Type | Provider | Comment |
+--------------+------------+----------+-------------+
| Hive_catalog | RELATIONAL | hive | new comment |
+--------------+------------+----------+-------------+
```
plainformat test
```bash
gcli metalake list -i
# demo
# cli_demo
# demo_metalake
# test_cli_metalake
# demo3
gcli metalake details -i -m demo_metalake
# demo_metalake, new comment
gcli catalog list -i -m demo_metalake
# File
# Hive_catalog
# Iceberg_catalog
# Mysql_catalog
# Test_hive_catalog
gcli catalog details --name Hive_catalog -i -m demo_metalake
# Hive_catalog, RELATIONAL, hive, new comment
```
---
.../gravitino/cli/commands/CatalogDetails.java | 2 +-
.../org/apache/gravitino/cli/commands/Command.java | 23 +-
.../gravitino/cli/commands/ListCatalogs.java | 6 +-
.../gravitino/cli/commands/ListMetalakes.java | 6 +-
.../gravitino/cli/commands/MetalakeDetails.java | 2 +-
.../gravitino/cli/outputs/BaseOutputFormat.java | 95 +++
.../org/apache/gravitino/cli/outputs/Column.java | 245 ++++++++
.../org/apache/gravitino/cli/outputs/LineUtil.java | 103 ++++
.../gravitino/cli/outputs/OutputConstant.java | 62 ++
.../apache/gravitino/cli/outputs/OutputFormat.java | 29 +-
.../apache/gravitino/cli/outputs/PlainFormat.java | 124 ++--
.../apache/gravitino/cli/outputs/TableFormat.java | 642 ++++++++++++++++-----
.../cli/integration/test/TableFormatOutputIT.java | 10 +-
.../gravitino/cli/output/TestPlainFormat.java | 171 ++++++
.../gravitino/cli/output/TestTableFormat.java | 348 +++++++++++
15 files changed, 1652 insertions(+), 216 deletions(-)
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CatalogDetails.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CatalogDetails.java
index fac504a008..81365062c2 100644
---
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CatalogDetails.java
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CatalogDetails.java
@@ -52,7 +52,7 @@ public class CatalogDetails extends Command {
try {
GravitinoClient client = buildClient(metalake);
result = client.loadCatalog(catalog);
- output(result);
+ printResults(result);
} catch (NoSuchMetalakeException err) {
exitWithError(ErrorMessages.UNKNOWN_METALAKE);
} catch (NoSuchCatalogException err) {
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/Command.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/Command.java
index 6ecef9278b..f789e2336b 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/Command.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/Command.java
@@ -29,6 +29,7 @@ import org.apache.gravitino.cli.GravitinoConfig;
import org.apache.gravitino.cli.KerberosData;
import org.apache.gravitino.cli.Main;
import org.apache.gravitino.cli.OAuthData;
+import org.apache.gravitino.cli.outputs.BaseOutputFormat;
import org.apache.gravitino.cli.outputs.PlainFormat;
import org.apache.gravitino.cli.outputs.TableFormat;
import org.apache.gravitino.client.DefaultOAuth2TokenProvider;
@@ -89,16 +90,26 @@ public abstract class Command {
return;
}
- System.out.print(message);
+ printResults(message);
}
/**
- * Prints out an a results of a command.
+ * Outputs the entity result to the console.
+ *
+ * @param entity The entity to output.
+ * @param <T> The type of entity.
+ */
+ public <T> void printResults(T entity) {
+ output(entity);
+ }
+
+ /**
+ * Prints out the string result of a command.
*
* @param results The results to display.
*/
public void printResults(String results) {
- System.out.print(results);
+ BaseOutputFormat.output(results, System.out);
}
/**
@@ -226,14 +237,14 @@ public abstract class Command {
*/
protected <T> void output(T entity) {
if (outputFormat == null) {
- PlainFormat.output(entity);
+ PlainFormat.output(entity, context);
return;
}
if (outputFormat.equals(OUTPUT_FORMAT_TABLE)) {
- TableFormat.output(entity);
+ TableFormat.output(entity, context);
} else if (outputFormat.equals(OUTPUT_FORMAT_PLAIN)) {
- PlainFormat.output(entity);
+ PlainFormat.output(entity, context);
} else {
throw new IllegalArgumentException("Unsupported output format");
}
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListCatalogs.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListCatalogs.java
index ad8d171fec..6dc8b4cea3 100644
---
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListCatalogs.java
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListCatalogs.java
@@ -48,7 +48,11 @@ public class ListCatalogs extends Command {
try {
GravitinoClient client = buildClient(metalake);
catalogs = client.listCatalogsInfo();
- output(catalogs);
+ if (catalogs.length == 0) {
+ printInformation("No catalogs exist.");
+ return;
+ }
+ printResults(catalogs);
} catch (NoSuchMetalakeException err) {
exitWithError(ErrorMessages.UNKNOWN_METALAKE);
} catch (Exception exp) {
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListMetalakes.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListMetalakes.java
index a97e89bca2..0a030a3478 100644
---
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListMetalakes.java
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListMetalakes.java
@@ -42,7 +42,11 @@ public class ListMetalakes extends Command {
try {
GravitinoAdminClient client = buildAdminClient();
metalakes = client.listMetalakes();
- output(metalakes);
+ if (metalakes.length == 0) {
+ printInformation("No metalakes exist.");
+ return;
+ }
+ printResults(metalakes);
} catch (Exception exp) {
exitWithError(exp.getMessage());
}
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeDetails.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeDetails.java
index cc2bf6ce4b..b427052c0c 100644
---
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeDetails.java
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeDetails.java
@@ -46,7 +46,7 @@ public class MetalakeDetails extends Command {
try {
GravitinoClient client = buildClient(metalake);
Metalake metalakeEntity = client.loadMetalake(metalake);
- output(metalakeEntity);
+ printResults(metalakeEntity);
} catch (NoSuchMetalakeException err) {
exitWithError(ErrorMessages.UNKNOWN_METALAKE);
} catch (Exception exp) {
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/BaseOutputFormat.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/BaseOutputFormat.java
new file mode 100644
index 0000000000..a233fe8751
--- /dev/null
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/BaseOutputFormat.java
@@ -0,0 +1,95 @@
+/*
+ * 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.gravitino.cli.outputs;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import org.apache.gravitino.cli.CommandContext;
+
+/**
+ * Abstract base implementation of {@link OutputFormat} interface providing
common functionality for
+ * various output format implementations.
+ */
+public abstract class BaseOutputFormat<T> implements OutputFormat<T> {
+ protected CommandContext context;
+
+ /**
+ * Creates a new {@link BaseOutputFormat} with specified configuration.
+ *
+ * @param context the command context, must not be null;
+ */
+ public BaseOutputFormat(CommandContext context) {
+ this.context = context;
+ }
+
+ /**
+ * Outputs a message to the specified OutputStream. This method handles both
system streams
+ * ({@code System.out}, {@code System.err}) and regular output streams
differently: - For system
+ * streams: Preserves the stream open after writing - For other streams:
Automatically closes the
+ * stream after writing
+ *
+ * @param message the message to output, must not be null
+ * @param os the output stream to write to, must not be null If this is
{@code System.out} or
+ * {@code System.err}, the stream will not be closed
+ * @throws IllegalArgumentException if either message or os is null
+ * @throws UncheckedIOException if an I/O error occurs during writing
+ */
+ public static void output(String message, OutputStream os) {
+ if (message == null || os == null) {
+ throw new IllegalArgumentException(
+ "Message and OutputStream cannot be null, message: " + message + ",
os: " + os);
+ }
+ boolean isSystemStream = (os == System.out || os == System.err);
+
+ try {
+ PrintStream printStream =
+ new PrintStream(
+ isSystemStream ? os : new BufferedOutputStream(os),
+ true,
+ StandardCharsets.UTF_8.name());
+
+ try {
+ printStream.println(message);
+ printStream.flush();
+ } finally {
+ if (!isSystemStream) {
+ printStream.close();
+ }
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException("Failed to write message to output
stream", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc} This implementation checks the quiet flag and handles null
output gracefully. If
+ * quiet mode is enabled, no output is produced.
+ */
+ @Override
+ public void output(T entity) {
+ String outputMessage = getOutput(entity);
+ String output = outputMessage == null ? "" : outputMessage;
+ output(output, System.out);
+ }
+}
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Column.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Column.java
new file mode 100644
index 0000000000..25c92a9d7b
--- /dev/null
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Column.java
@@ -0,0 +1,245 @@
+/*
+ * 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.gravitino.cli.outputs;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import org.apache.gravitino.cli.CommandContext;
+
+/**
+ * Represents a column in a formatted table output. Manages column properties
including header,
+ * alignment, and content cells. Handles width calculations.
+ */
+public class Column {
+ public static final char ELLIPSIS = '…';
+ private final String header;
+ private final HorizontalAlign headerAlign;
+ private final HorizontalAlign dataAlign;
+ private final CommandContext context;
+
+ private int maxWidth;
+ private List<String> cellContents;
+
+ /**
+ * Creates a new {@code Column} instance with the specified header and
default alignment.
+ *
+ * @param context the command context.
+ * @param header the header of the column.
+ */
+ public Column(CommandContext context, String header) {
+ this(context, header, HorizontalAlign.CENTER, HorizontalAlign.LEFT);
+ }
+
+ /**
+ * Creates a new {@code Column} instance with the specified header and
alignment.
+ *
+ * @param context the command context.
+ * @param header the header of the column.
+ * @param headerAlign the alignment of the header.
+ * @param dataAlign the alignment of the data in the column.
+ */
+ public Column(
+ CommandContext context,
+ String header,
+ HorizontalAlign headerAlign,
+ HorizontalAlign dataAlign) {
+ this.context = context;
+ this.header = LineUtil.capitalize(header);
+ this.headerAlign = headerAlign;
+ this.dataAlign = dataAlign;
+
+ this.cellContents = Lists.newArrayList();
+ this.maxWidth = LineUtil.getDisplayWidth(header);
+ }
+
+ /**
+ * Specifies the horizontal text alignment within table elements such as
cells and headers. This
+ * enum provides options for standard left-to-right text positioning.
+ */
+ public enum HorizontalAlign {
+ LEFT,
+ CENTER,
+ RIGHT
+ }
+
+ /**
+ * Returns the header of the column.
+ *
+ * @return the header of the column.
+ */
+ public String getHeader() {
+ return header;
+ }
+
+ /**
+ * Returns the alignment of the header.
+ *
+ * @return the alignment of the header.
+ */
+ public HorizontalAlign getHeaderAlign() {
+ return headerAlign;
+ }
+
+ /**
+ * Returns the alignment of the data in the column.
+ *
+ * @return the alignment of the data in the column.
+ */
+ public HorizontalAlign getDataAlign() {
+ return dataAlign;
+ }
+
+ /**
+ * Returns the maximum width of the column.
+ *
+ * @return the maximum width of the column.
+ */
+ public int getMaxWidth() {
+ return maxWidth;
+ }
+
+ /**
+ * Returns the command context.
+ *
+ * @return the {@link CommandContext} instance.
+ */
+ public CommandContext getContext() {
+ return context;
+ }
+
+ /**
+ * Returns a copy of this column.
+ *
+ * @return a copy of this column.
+ */
+ public Column copy() {
+ return new Column(context, header, headerAlign, dataAlign);
+ }
+
+ /**
+ * Adds a cell to the column and updates the maximum width of the column.
+ *
+ * @param cell the cell to add to the column.
+ * @return this column instance, for chaining.
+ */
+ public Column addCell(String cell) {
+ if (cell == null) {
+ cell = "null";
+ }
+
+ maxWidth = Math.max(maxWidth, LineUtil.getDisplayWidth(cell));
+ cellContents.add(cell);
+ return this;
+ }
+
+ /**
+ * Adds a cell to the column and updates the maximum width of the column.
+ *
+ * @param cell the cell to add to the column.
+ * @return this column instance, for chaining.
+ */
+ public Column addCell(Object cell) {
+ return addCell(cell == null ? "null" : cell.toString());
+ }
+
+ /**
+ * Adds a cell to the column and updates the maximum width of the column.
+ *
+ * @param cell the cell to add to the column.
+ * @return this column instance, for chaining.
+ */
+ public Column addCell(char cell) {
+ return addCell(String.valueOf(cell));
+ }
+
+ /**
+ * Adds a cell to the column and updates the maximum width of the column.
+ *
+ * @param cell the cell to add to the column.
+ * @return this column instance, for chaining.
+ */
+ public Column addCell(int cell) {
+ return addCell(String.valueOf(cell));
+ }
+
+ /**
+ * Adds a cell to the column and updates the maximum width of the column.
+ *
+ * @param cell the cell to add to the column.
+ * @return this column instance, for chaining.
+ */
+ public Column addCell(double cell) {
+ return addCell(String.valueOf(cell));
+ }
+
+ /**
+ * Adds a cell to the column and updates the maximum width of the column.
+ *
+ * @param cell the cell to add to the column.
+ * @return this column instance, for chaining.
+ */
+ public Column addCell(boolean cell) {
+ return addCell(String.valueOf(cell));
+ }
+
+ /**
+ * Returns a limited version of this column, with a maximum of {@code limit}
cells.
+ *
+ * @param limit the maximum number of cells to include in the limited column.
+ * @return a limited version of this column, with a maximum of {@code limit}
cells.
+ */
+ public Column getLimitedColumn(int limit) {
+ if (cellContents.size() <= limit) {
+ return this;
+ }
+
+ Column newColumn = copy();
+ newColumn.cellContents = cellContents.subList(0, Math.min(limit,
cellContents.size()));
+ newColumn.reCalculateMaxWidth();
+ newColumn.addCell(ELLIPSIS);
+
+ return newColumn;
+ }
+
+ /**
+ * Returns the cell at the specified index.
+ *
+ * @param index the index of the cell to return.
+ * @return the cell at the specified index.
+ */
+ public String getCell(int index) {
+ return cellContents.get(index);
+ }
+
+ /**
+ * Returns the number of cells in the column.
+ *
+ * @return the number of cells in the column.
+ */
+ public int getCellCount() {
+ return cellContents.size();
+ }
+
+ private void reCalculateMaxWidth() {
+ for (String cell : cellContents) {
+ maxWidth = Math.max(maxWidth, LineUtil.getDisplayWidth(cell));
+ }
+ }
+}
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/LineUtil.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/LineUtil.java
new file mode 100644
index 0000000000..7aadfe5e52
--- /dev/null
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/LineUtil.java
@@ -0,0 +1,103 @@
+/*
+ * 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.gravitino.cli.outputs;
+
+import com.google.common.base.Preconditions;
+import java.util.regex.Pattern;
+
+public class LineUtil {
+ // This expression is primarily used to match characters that have a display
width of
+ // 2, such as characters from Korean, Chinese
+ private static final Pattern FULL_WIDTH_PATTERN =
+ Pattern.compile(
+
"[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6]");
+
+ /**
+ * Get the display width of a string.
+ *
+ * @param str the string to measure.
+ * @return the display width of the string.
+ */
+ public static int getDisplayWidth(String str) {
+ int width = 0;
+ for (int i = 0; i < str.length(); i++) {
+ width += getCharWidth(str.charAt(i));
+ }
+
+ return width;
+ }
+
+ private static int getCharWidth(char ch) {
+ String s = String.valueOf(ch);
+ if (FULL_WIDTH_PATTERN.matcher(s).find()) {
+ return 2;
+ }
+
+ return 1;
+ }
+
+ /**
+ * Get the space string of the specified length.
+ *
+ * @param n the length of the space string to get.
+ * @return the space string of the specified length.
+ */
+ public static String getSpaces(int n) {
+ Preconditions.checkArgument(n >= 0, "n must be non-negative");
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < n; i++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Capitalize the first letter of a string.
+ *
+ * @param str the string to capitalize.
+ * @return the capitalized string.
+ */
+ public static String capitalize(String str) {
+ int strLen = str.length();
+ if (strLen == 0) {
+ return str;
+ } else {
+ int firstCodepoint = str.codePointAt(0);
+ int newCodePoint = Character.toTitleCase(firstCodepoint);
+ if (firstCodepoint == newCodePoint) {
+ return str;
+ } else {
+ int[] newCodePoints = new int[strLen];
+ int outOffset = 0;
+ newCodePoints[outOffset++] = newCodePoint;
+
+ int codePoint;
+ for (int inOffset = Character.charCount(firstCodepoint);
+ inOffset < strLen;
+ inOffset += Character.charCount(codePoint)) {
+ codePoint = str.codePointAt(inOffset);
+ newCodePoints[outOffset++] = codePoint;
+ }
+
+ return new String(newCodePoints, 0, outOffset);
+ }
+ }
+ }
+}
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputConstant.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputConstant.java
new file mode 100644
index 0000000000..756984485e
--- /dev/null
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputConstant.java
@@ -0,0 +1,62 @@
+/*
+ * 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.gravitino.cli.outputs;
+
+import com.google.common.collect.ImmutableList;
+
+public class OutputConstant {
+ public static final ImmutableList<Character> BASIC_ASCII =
+ ImmutableList.of(
+ '+', '-', '+', '+', '|', '|', '|', '+', '-', '+', '+', '|', '|',
'|', '+', '-', '+', '+',
+ '+', '-', '+', '+', '|', '|', '|', '+', '-', '+', '+');
+
+ // ===== Table Upper Border Indices =====
+ public static final int TABLE_UPPER_BORDER_LEFT_IDX = 0;
+ public static final int TABLE_UPPER_BORDER_MIDDLE_IDX = 1;
+ public static final int TABLE_UPPER_BORDER_COLUMN_SEPARATOR_IDX = 2;
+ public static final int TABLE_UPPER_BORDER_RIGHT_IDX = 3;
+
+ // ===== Data Line Indices =====
+ public static final int DATA_LINE_LEFT_IDX = 4;
+ public static final int DATA_LINE_COLUMN_SEPARATOR_IDX = 5;
+ public static final int DATA_LINE_RIGHT_IDX = 6;
+
+ // ===== Data Row Border Indices =====
+ public static final int DATA_ROW_BORDER_LEFT_IDX = 14;
+ public static final int DATA_ROW_BORDER_MIDDLE_IDX = 15;
+ public static final int DATA_ROW_BORDER_COLUMN_SEPARATOR_IDX = 16;
+ public static final int DATA_ROW_BORDER_RIGHT_IDX = 17;
+
+ // ===== Table Bottom Border Indices =====
+ public static final int TABLE_BOTTOM_BORDER_LEFT_IDX = 25;
+ public static final int TABLE_BOTTOM_BORDER_MIDDLE_IDX = 26;
+ public static final int TABLE_BOTTOM_BORDER_COLUMN_SEPARATOR_IDX = 27;
+ public static final int TABLE_BOTTOM_BORDER_RIGHT_IDX = 28;
+
+ // ===== Header Bottom Border Indices =====
+ public static final int HEADER_BOTTOM_BORDER_LEFT_IDX = 18;
+ public static final int HEADER_BOTTOM_BORDER_MIDDLE_IDX = 19;
+ public static final int HEADER_BOTTOM_BORDER_COLUMN_SEPARATOR_IDX = 20;
+ public static final int HEADER_BOTTOM_BORDER_RIGHT_IDX = 21;
+
+ private OutputConstant() {
+ // private constructor to prevent instantiation
+ }
+}
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputFormat.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputFormat.java
index 8e6ab31162..fe0d7b7c9e 100644
---
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputFormat.java
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputFormat.java
@@ -18,7 +18,32 @@
*/
package org.apache.gravitino.cli.outputs;
-/** Output format interface for the CLI results. */
+import com.google.common.base.Joiner;
+
+/**
+ * Defines formatting behavior for command-line interface output.
Implementations of this interface
+ * handle the conversion of entities to their string representation in
specific output formats.
+ */
public interface OutputFormat<T> {
- void output(T object);
+ /** Joiner for creating comma-separated output strings, ignoring null values
*/
+ Joiner COMMA_JOINER = Joiner.on(", ").skipNulls();
+ /** Joiner for creating line-separated output strings, ignoring null values
*/
+ Joiner NEWLINE_JOINER = Joiner.on(System.lineSeparator()).skipNulls();
+
+ /**
+ * Displays the entity in the specified output format. This method handles
the actual output
+ * operation
+ *
+ * @param entity The entity to be formatted and output
+ */
+ void output(T entity);
+
+ /**
+ * Returns entity's string representation. This method only handles the
formatting without
+ * performing any I/O operations.
+ *
+ * @param entity The entity to be formatted
+ * @return The formatted string representation of the entity
+ */
+ String getOutput(T entity);
}
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java
index 66e616c4f7..f61578ffc6 100644
---
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java
@@ -23,68 +23,114 @@ import java.util.List;
import java.util.stream.Collectors;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.Metalake;
+import org.apache.gravitino.cli.CommandContext;
/** Plain format to print a pretty string to standard out. */
-public class PlainFormat {
- public static void output(Object object) {
- if (object instanceof Metalake) {
- new MetalakePlainFormat().output((Metalake) object);
- } else if (object instanceof Metalake[]) {
- new MetalakesPlainFormat().output((Metalake[]) object);
- } else if (object instanceof Catalog) {
- new CatalogPlainFormat().output((Catalog) object);
- } else if (object instanceof Catalog[]) {
- new CatalogsPlainFormat().output((Catalog[]) object);
+public abstract class PlainFormat<T> extends BaseOutputFormat<T> {
+
+ /**
+ * Routes the object to its appropriate formatter and outputs the formatted
result. Creates a new
+ * formatter instance for the given object type and delegates the formatting.
+ *
+ * @param entity The object to format
+ * @param context The command context
+ * @throws IllegalArgumentException if the object type is not supported
+ */
+ public static void output(Object entity, CommandContext context) {
+ if (entity instanceof Metalake) {
+ new MetalakePlainFormat(context).output((Metalake) entity);
+ } else if (entity instanceof Metalake[]) {
+ new MetalakeListPlainFormat(context).output((Metalake[]) entity);
+ } else if (entity instanceof Catalog) {
+ new CatalogPlainFormat(context).output((Catalog) entity);
+ } else if (entity instanceof Catalog[]) {
+ new CatalogListPlainFormat(context).output((Catalog[]) entity);
} else {
throw new IllegalArgumentException("Unsupported object type");
}
}
- static final class MetalakePlainFormat implements OutputFormat<Metalake> {
+ /**
+ * Creates a new {@link PlainFormat} with the specified output properties.
+ *
+ * @param context The command context.
+ */
+ public PlainFormat(CommandContext context) {
+ super(context);
+ }
+
+ /**
+ * Formats a single {@link Metalake} instance as a comma-separated string.
Output format: name,
+ * comment
+ */
+ static final class MetalakePlainFormat extends PlainFormat<Metalake> {
+
+ public MetalakePlainFormat(CommandContext context) {
+ super(context);
+ }
+
+ /** {@inheritDoc} */
@Override
- public void output(Metalake metalake) {
- System.out.println(metalake.name() + "," + metalake.comment());
+ public String getOutput(Metalake metalake) {
+ return COMMA_JOINER.join(metalake.name(), metalake.comment());
}
}
- static final class MetalakesPlainFormat implements OutputFormat<Metalake[]> {
+ /**
+ * Formats an array of Metalakes, outputting one name per line. Returns null
if the array is empty
+ * or null.
+ */
+ static final class MetalakeListPlainFormat extends PlainFormat<Metalake[]> {
+
+ public MetalakeListPlainFormat(CommandContext context) {
+ super(context);
+ }
+
+ /** {@inheritDoc} */
@Override
- public void output(Metalake[] metalakes) {
- if (metalakes.length == 0) {
- System.out.println("No metalakes exist.");
- } else {
- List<String> metalakeNames =
-
Arrays.stream(metalakes).map(Metalake::name).collect(Collectors.toList());
- String all = String.join(System.lineSeparator(), metalakeNames);
- System.out.println(all);
- }
+ public String getOutput(Metalake[] metalakes) {
+ List<String> metalakeNames =
+
Arrays.stream(metalakes).map(Metalake::name).collect(Collectors.toList());
+ return NEWLINE_JOINER.join(metalakeNames);
}
}
- static final class CatalogPlainFormat implements OutputFormat<Catalog> {
+ /**
+ * Formats a single {@link Catalog} instance as a comma-separated string.
Output format: name,
+ * type, provider, comment
+ */
+ static final class CatalogPlainFormat extends PlainFormat<Catalog> {
+ public CatalogPlainFormat(CommandContext context) {
+ super(context);
+ }
+
+ /** {@inheritDoc} */
@Override
- public void output(Catalog catalog) {
- System.out.println(
- catalog.name()
- + ","
- + catalog.type()
- + ","
- + catalog.provider()
- + ","
- + catalog.comment());
+ public String getOutput(Catalog catalog) {
+ return COMMA_JOINER.join(
+ catalog.name(), catalog.type(), catalog.provider(),
catalog.comment());
}
}
- static final class CatalogsPlainFormat implements OutputFormat<Catalog[]> {
+ /**
+ * Formats an array of Catalogs, outputting one name per line. Returns null
if the array is empty
+ * or null.
+ */
+ static final class CatalogListPlainFormat extends PlainFormat<Catalog[]> {
+ public CatalogListPlainFormat(CommandContext context) {
+ super(context);
+ }
+
+ /** {@inheritDoc} */
@Override
- public void output(Catalog[] catalogs) {
- if (catalogs.length == 0) {
- System.out.println("No catalogs exist.");
+ public String getOutput(Catalog[] catalogs) {
+ if (catalogs == null || catalogs.length == 0) {
+ output("No catalogs exist.", System.out);
+ return null;
} else {
List<String> catalogNames =
Arrays.stream(catalogs).map(Catalog::name).collect(Collectors.toList());
- String all = String.join(System.lineSeparator(), catalogNames);
- System.out.println(all);
+ return NEWLINE_JOINER.join(catalogNames);
}
}
}
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/TableFormat.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/TableFormat.java
index a3c9975652..6d08f73edf 100644
---
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/TableFormat.java
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/TableFormat.java
@@ -18,204 +18,526 @@
*/
package org.apache.gravitino.cli.outputs;
-import java.util.ArrayList;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.DATA_LINE_COLUMN_SEPARATOR_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.DATA_LINE_LEFT_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.DATA_LINE_RIGHT_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.DATA_ROW_BORDER_COLUMN_SEPARATOR_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.DATA_ROW_BORDER_LEFT_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.DATA_ROW_BORDER_MIDDLE_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.DATA_ROW_BORDER_RIGHT_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.HEADER_BOTTOM_BORDER_COLUMN_SEPARATOR_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.HEADER_BOTTOM_BORDER_LEFT_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.HEADER_BOTTOM_BORDER_MIDDLE_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.HEADER_BOTTOM_BORDER_RIGHT_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.TABLE_BOTTOM_BORDER_COLUMN_SEPARATOR_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.TABLE_BOTTOM_BORDER_LEFT_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.TABLE_BOTTOM_BORDER_MIDDLE_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.TABLE_BOTTOM_BORDER_RIGHT_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.TABLE_UPPER_BORDER_COLUMN_SEPARATOR_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.TABLE_UPPER_BORDER_LEFT_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.TABLE_UPPER_BORDER_MIDDLE_IDX;
+import static
org.apache.gravitino.cli.outputs.OutputConstant.TABLE_UPPER_BORDER_RIGHT_IDX;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
-import java.util.regex.Pattern;
+import java.util.Objects;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.Metalake;
+import org.apache.gravitino.cli.CommandContext;
+
+/**
+ * Abstract base class for formatting entity information into ASCII-art
tables. Provides
+ * comprehensive table rendering with features including: - Header and footer
rows - Column
+ * alignments and padding - Border styles and row separators - Content
overflow handling - Row
+ * numbers - Data limiting and sorting
+ */
+public abstract class TableFormat<T> extends BaseOutputFormat<T> {
+ public static final int PADDING = 1;
-/** Table format to print a pretty table to standard out. */
-public class TableFormat {
- public static void output(Object object) {
- if (object instanceof Metalake) {
- new MetalakeTableFormat().output((Metalake) object);
- } else if (object instanceof Metalake[]) {
- new MetalakesTableFormat().output((Metalake[]) object);
- } else if (object instanceof Catalog) {
- new CatalogTableFormat().output((Catalog) object);
- } else if (object instanceof Catalog[]) {
- new CatalogsTableFormat().output((Catalog[]) object);
+ /**
+ * Routes the entity object to its appropriate table formatter. Creates a
new formatter instance
+ * based on the object's type.
+ *
+ * @param entity The object to format.
+ * @param context the command context.
+ * @throws IllegalArgumentException if the object type is not supported
+ */
+ public static void output(Object entity, CommandContext context) {
+ if (entity instanceof Metalake) {
+ new MetalakeTableFormat(context).output((Metalake) entity);
+ } else if (entity instanceof Metalake[]) {
+ new MetalakeListTableFormat(context).output((Metalake[]) entity);
+ } else if (entity instanceof Catalog) {
+ new CatalogTableFormat(context).output((Catalog) entity);
+ } else if (entity instanceof Catalog[]) {
+ new CatalogListTableFormat(context).output((Catalog[]) entity);
} else {
throw new IllegalArgumentException("Unsupported object type");
}
}
- static final class MetalakeTableFormat implements OutputFormat<Metalake> {
- @Override
- public void output(Metalake metalake) {
- List<String> headers = Arrays.asList("metalake", "comment");
- List<List<String>> rows = new ArrayList<>();
- rows.add(Arrays.asList(metalake.name(), metalake.comment()));
- TableFormatImpl tableFormat = new TableFormatImpl();
- tableFormat.print(headers, rows);
- }
+ /**
+ * Creates a new {@link TableFormat} with the specified properties.
+ *
+ * @param context the command context.
+ */
+ public TableFormat(CommandContext context) {
+ super(context);
+ // TODO: add other options for TableFormat
}
- static final class MetalakesTableFormat implements OutputFormat<Metalake[]> {
- @Override
- public void output(Metalake[] metalakes) {
- if (metalakes.length == 0) {
- System.out.println("No metalakes exist.");
- } else {
- List<String> headers = Collections.singletonList("metalake");
- List<List<String>> rows = new ArrayList<>();
- for (int i = 0; i < metalakes.length; i++) {
- rows.add(Arrays.asList(metalakes[i].name()));
- }
- TableFormatImpl tableFormat = new TableFormatImpl();
- tableFormat.print(headers, rows);
- }
+ /**
+ * Get the formatted output string for the given columns.
+ *
+ * @param columns the columns to print.
+ * @return the table formatted output string.
+ */
+ public String getTableFormat(Column... columns) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ String[] headers =
+ Arrays.stream(columns)
+ .map(Column::getHeader)
+ .filter(Objects::nonNull)
+ .toArray(String[]::new);
+
+ List<Character> borders = OutputConstant.BASIC_ASCII;
+
+ if (headers.length != columns.length) {
+ throw new IllegalArgumentException("Headers must be provided for all
columns");
}
+
+ try (OutputStreamWriter osw = new OutputStreamWriter(baos,
StandardCharsets.UTF_8)) {
+ writeUpperBorder(osw, borders, System.lineSeparator(), columns);
+ writeHeader(osw, borders, System.lineSeparator(), columns);
+ writeHeaderBorder(osw, borders, System.lineSeparator(), columns);
+ writeData(osw, borders, columns, System.lineSeparator());
+ writeBottomBorder(osw, borders, System.lineSeparator(), columns);
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ return new String(baos.toByteArray(), StandardCharsets.UTF_8);
}
- static final class CatalogTableFormat implements OutputFormat<Catalog> {
- @Override
- public void output(Catalog catalog) {
- List<String> headers = Arrays.asList("catalog", "type", "provider",
"comment");
- List<List<String>> rows = new ArrayList<>();
- rows.add(
- Arrays.asList(
- catalog.name(),
- catalog.type().toString(),
- catalog.provider(),
- catalog.comment() + ""));
- TableFormatImpl tableFormat = new TableFormatImpl();
- tableFormat.print(headers, rows);
+ /**
+ * Writes the top border of the table using specified border characters.
+ *
+ * @param writer the writer for output
+ * @param borders the collection of border characters for rendering
+ * @param lineSeparator the system-specific line separator
+ * @param columns the array of columns defining the table structure
+ * @throws IOException if an error occurs while writing to the output
+ */
+ private static void writeUpperBorder(
+ OutputStreamWriter writer, List<Character> borders, String
lineSeparator, Column[] columns)
+ throws IOException {
+ writeHorizontalLine(
+ writer,
+ borders.get(TABLE_UPPER_BORDER_LEFT_IDX),
+ borders.get(TABLE_UPPER_BORDER_MIDDLE_IDX),
+ borders.get(TABLE_UPPER_BORDER_COLUMN_SEPARATOR_IDX),
+ borders.get(TABLE_UPPER_BORDER_RIGHT_IDX),
+ lineSeparator,
+ columns);
+ }
+
+ /**
+ * Writes the bottom border that separates the header from the table content.
+ *
+ * @param writer the writer for output
+ * @param borders the collection of border characters for rendering
+ * @param lineSeparator the system-specific line separator
+ * @param columns the array of columns defining the table structure
+ * @throws IOException if an error occurs while writing to the output
+ */
+ private static void writeHeaderBorder(
+ OutputStreamWriter writer, List<Character> borders, String
lineSeparator, Column[] columns)
+ throws IOException {
+ writeHorizontalLine(
+ writer,
+ borders.get(HEADER_BOTTOM_BORDER_LEFT_IDX),
+ borders.get(HEADER_BOTTOM_BORDER_MIDDLE_IDX),
+ borders.get(HEADER_BOTTOM_BORDER_COLUMN_SEPARATOR_IDX),
+ borders.get(HEADER_BOTTOM_BORDER_RIGHT_IDX),
+ lineSeparator,
+ columns);
+ }
+
+ /**
+ * Writes the separator line between data rows.
+ *
+ * @param writer the writer for output
+ * @param borders the collection of border characters for rendering
+ * @param lineSeparator the system-specific line separator
+ * @param columns the array of columns defining the table structure
+ * @throws IOException if an error occurs while writing to the output
+ */
+ private static void writeRowSeparator(
+ OutputStreamWriter writer, List<Character> borders, String
lineSeparator, Column[] columns)
+ throws IOException {
+ writeHorizontalLine(
+ writer,
+ borders.get(DATA_ROW_BORDER_LEFT_IDX),
+ borders.get(DATA_ROW_BORDER_MIDDLE_IDX),
+ borders.get(DATA_ROW_BORDER_COLUMN_SEPARATOR_IDX),
+ borders.get(DATA_ROW_BORDER_RIGHT_IDX),
+ lineSeparator,
+ columns);
+ }
+
+ /**
+ * Writes the bottom border that closes the table.
+ *
+ * @param writer the writer for output
+ * @param borders the collection of border characters for rendering
+ * @param lineSeparator the system-specific line separator
+ * @param columns the array of columns defining the table structure
+ * @throws IOException if an error occurs while writing to the output
+ */
+ private static void writeBottomBorder(
+ OutputStreamWriter writer, List<Character> borders, String
lineSeparator, Column[] columns)
+ throws IOException {
+ writeHorizontalLine(
+ writer,
+ borders.get(TABLE_BOTTOM_BORDER_LEFT_IDX),
+ borders.get(TABLE_BOTTOM_BORDER_MIDDLE_IDX),
+ borders.get(TABLE_BOTTOM_BORDER_COLUMN_SEPARATOR_IDX),
+ borders.get(TABLE_BOTTOM_BORDER_RIGHT_IDX),
+ lineSeparator,
+ columns);
+ }
+
+ /**
+ * Writes the data rows of the table.
+ *
+ * <p>For each row of data:
+ *
+ * <ul>
+ * <li>Writes the data line with appropriate borders and alignment
+ * <li>If not the last row and row boundaries are enabled in the style,
writes a separator line
+ * between rows
+ * </ul>
+ *
+ * @param writer the writer for output
+ * @param borders the collection of border characters for rendering
+ * @param columns the array of columns containing the data to write
+ * @param lineSeparator the system-specific line separator
+ * @throws IOException if an error occurs while writing to the output
+ */
+ private void writeData(
+ OutputStreamWriter writer, List<Character> borders, Column[] columns,
String lineSeparator)
+ throws IOException {
+ int dataSize = columns[0].getCellCount();
+ Column.HorizontalAlign[] dataAligns =
+
Arrays.stream(columns).map(Column::getDataAlign).toArray(Column.HorizontalAlign[]::new);
+
+ for (int i = 0; i < dataSize; i++) {
+ String[] data = getData(columns, i);
+ writeRow(
+ writer,
+ borders.get(DATA_LINE_LEFT_IDX),
+ borders.get(DATA_LINE_COLUMN_SEPARATOR_IDX),
+ borders.get(DATA_LINE_RIGHT_IDX),
+ data,
+ columns,
+ dataAligns,
+ lineSeparator);
}
}
- static final class CatalogsTableFormat implements OutputFormat<Catalog[]> {
- @Override
- public void output(Catalog[] catalogs) {
- if (catalogs.length == 0) {
- System.out.println("No catalogs exist.");
- } else {
- List<String> headers = Collections.singletonList("catalog");
- List<List<String>> rows = new ArrayList<>();
- for (int i = 0; i < catalogs.length; i++) {
- rows.add(Arrays.asList(catalogs[i].name()));
- }
- TableFormatImpl tableFormat = new TableFormatImpl();
- tableFormat.print(headers, rows);
+ /**
+ * Writes a horizontal line in the table using specified border characters.
The line consists of
+ * repeated middle characters for each column width, separated by column
separators and bounded by
+ * left/right borders.
+ *
+ * @param osw The output stream writer for writing the line.
+ * @param left The character used for the left border.
+ * @param middle The character to repeat for creating the line.
+ * @param columnSeparator The character used between columns.
+ * @param right The character used for the right border.
+ * @param lineSeparator The line separator to append.
+ * @param columns Array of columns containing width information.
+ * @throws IOException If an error occurs while writing to the output stream.
+ */
+ private static void writeHorizontalLine(
+ OutputStreamWriter osw,
+ Character left,
+ Character middle,
+ Character columnSeparator,
+ Character right,
+ String lineSeparator,
+ Column[] columns)
+ throws IOException {
+
+ Integer[] colWidths =
+ Arrays.stream(columns).map(s -> s.getMaxWidth() + 2 *
PADDING).toArray(Integer[]::new);
+
+ if (left != null) {
+ osw.write(left);
+ }
+
+ for (int col = 0; col < colWidths.length; col++) {
+ writeRepeated(osw, middle, colWidths[col]);
+ if (columnSeparator != null && col != colWidths.length - 1) {
+ osw.write(columnSeparator);
}
}
+
+ if (right != null) {
+ osw.write(right);
+ }
+
+ if (lineSeparator != null) {
+ osw.write(System.lineSeparator());
+ }
+ }
+
+ /**
+ * Renders the header row of a formatted table, applying specified
alignments and borders. This
+ * method processes the column definitions to extract headers and their
alignment, then delegates
+ * the actual writing to writeDataLine.
+ *
+ * @param osw The output writer for writing the formatted header
+ * @param borders A list containing border characters in the following
order: [4]: left border
+ * character [5]: middle border character [6]: right border character
+ * @param lineSeparator Platform-specific line separator (e.g., \n on Unix,
\r\n on Windows)
+ * @param columns Array of Column objects defining the structure of each
table column, including
+ * header text and alignment preferences
+ * @throws IOException If any error occurs during writing to the output
stream
+ */
+ private static void writeHeader(
+ OutputStreamWriter osw, List<Character> borders, String lineSeparator,
Column[] columns)
+ throws IOException {
+ Column.HorizontalAlign[] dataAligns =
+
Arrays.stream(columns).map(Column::getHeaderAlign).toArray(Column.HorizontalAlign[]::new);
+
+ String[] headers =
+ Arrays.stream(columns)
+ .map(Column::getHeader)
+ .filter(Objects::nonNull)
+ .toArray(String[]::new);
+
+ writeRow(
+ osw,
+ borders.get(4),
+ borders.get(5),
+ borders.get(6),
+ headers,
+ columns,
+ dataAligns,
+ lineSeparator);
}
- static final class TableFormatImpl {
- private int[] maxElementLengths;
- // This expression is primarily used to match characters that have a
display width of
- // 2, such as characters from Korean, Chinese
- private static final Pattern FULL_WIDTH_PATTERN =
- Pattern.compile(
-
"[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6]");
- private int[][] elementOutputWidths;
- private static final String horizontalDelimiter = "-";
- private static final String verticalDelimiter = "|";
- private static final String crossDelimiter = "+";
- private static final String indent = " ";
+ /**
+ * Write the data to the output stream.
+ *
+ * @param osw the output stream writer.
+ * @param left the left border character.
+ * @param columnSeparator the column separator character.
+ * @param right the right border character.
+ * @param data the data to write.
+ * @param columns the columns to write.
+ * @param lineSeparator the line separator.
+ */
+ private static void writeRow(
+ OutputStreamWriter osw,
+ Character left,
+ Character columnSeparator,
+ Character right,
+ String[] data,
+ Column[] columns,
+ Column.HorizontalAlign[] dataAligns,
+ String lineSeparator)
+ throws IOException {
+
+ int maxWidth;
+ Column.HorizontalAlign dataAlign;
- public void debug() {
- System.out.println();
- Arrays.stream(maxElementLengths).forEach(e -> System.out.print(e + " "));
+ if (left != null) {
+ osw.write(left);
}
- public void print(List<String> headers, List<List<String>> rows) {
- if (rows.size() > 0 && headers.size() != rows.get(0).size()) {
- throw new IllegalArgumentException("Number of columns is not equal.");
- }
- maxElementLengths = new int[headers.size()];
- elementOutputWidths = new int[rows.size()][headers.size()];
- updateMaxLengthsFromList(headers);
- updateMaxLengthsFromNestedList(rows);
- printLine();
- System.out.println();
- for (int i = 0; i < headers.size(); ++i) {
- System.out.printf(
- verticalDelimiter + indent + "%-" + maxElementLengths[i] + "s" +
indent,
- headers.get(i));
+ for (int i = 0; i < data.length; i++) {
+ maxWidth = columns[i].getMaxWidth();
+ dataAlign = dataAligns[i];
+ writeJustified(osw, data[i], dataAlign, maxWidth, PADDING);
+ if (i < data.length - 1) {
+ osw.write(columnSeparator);
}
- System.out.println(verticalDelimiter);
- printLine();
- System.out.println();
-
- // print rows
- for (int i = 0; i < rows.size(); ++i) {
- List<String> columns = rows.get(i);
- for (int j = 0; j < columns.size(); ++j) {
- String column = columns.get(j);
- // Handle cases where the width and number of characters are
inconsistent
- if (elementOutputWidths[i][j] != column.length()) {
- if (elementOutputWidths[i][j] > maxElementLengths[j]) {
- System.out.printf(
- verticalDelimiter + indent + "%-" + column.length() + "s" +
indent, column);
- } else {
- int paddingLength =
- maxElementLengths[j] - (elementOutputWidths[i][j] -
column.length());
- System.out.printf(
- verticalDelimiter + indent + "%-" + paddingLength + "s" +
indent, column);
- }
- } else {
- System.out.printf(
- verticalDelimiter + indent + "%-" + maxElementLengths[j] + "s"
+ indent, column);
- }
- }
- System.out.println(verticalDelimiter);
- }
- printLine();
- // add one more line
- System.out.println("");
}
- private void updateMaxLengthsFromList(List<String> elements) {
- String s;
- for (int i = 0; i < elements.size(); ++i) {
- s = elements.get(i);
- if (getOutputWidth(s) > maxElementLengths[i]) maxElementLengths[i] =
getOutputWidth(s);
- }
+ if (right != null) {
+ osw.write(right);
}
- private void updateMaxLengthsFromNestedList(List<List<String>> elements) {
- int rowIdx = 0;
- for (List<String> row : elements) {
- String s;
- for (int i = 0; i < row.size(); ++i) {
- s = row.get(i);
- int consoleWidth = getOutputWidth(s);
- elementOutputWidths[rowIdx][i] = consoleWidth;
- if (consoleWidth > maxElementLengths[i]) maxElementLengths[i] =
consoleWidth;
- }
- rowIdx++;
- }
+ osw.write(lineSeparator);
+ }
+
+ /**
+ * Retrieves data from all columns for a specific row index. Creates an
array of cell values by
+ * extracting the data at the given row index from each column.
+ *
+ * @param columns Array of columns to extract data from.
+ * @param rowIndex Zero-based index of the row to retrieve.
+ * @return Array of cell values for the specified row.
+ * @throws IndexOutOfBoundsException if rowIndex is invalid for any column.
+ */
+ private static String[] getData(Column[] columns, int rowIndex) {
+ return Arrays.stream(columns).map(c ->
c.getCell(rowIndex)).toArray(String[]::new);
+ }
+
+ /**
+ * Justifies the given string according to the specified alignment and
maximum length then writes
+ * it to the output stream.
+ *
+ * @param osw the output stream writer.
+ * @param str the string to justify.
+ * @param align the horizontal alignment.
+ * @param maxLength the maximum length.
+ * @param minPadding the minimum padding.
+ * @throws IOException if an I/O error occurs.
+ */
+ private static void writeJustified(
+ OutputStreamWriter osw,
+ String str,
+ Column.HorizontalAlign align,
+ int maxLength,
+ int minPadding)
+ throws IOException {
+
+ osw.write(LineUtil.getSpaces(minPadding));
+ if (str.length() < maxLength) {
+ int leftPadding =
+ align == Column.HorizontalAlign.LEFT
+ ? 0
+ : align == Column.HorizontalAlign.CENTER
+ ? (maxLength - LineUtil.getDisplayWidth(str)) / 2
+ : maxLength - LineUtil.getDisplayWidth(str);
+
+ writeRepeated(osw, ' ', leftPadding);
+ osw.write(str);
+ writeRepeated(osw, ' ', maxLength - LineUtil.getDisplayWidth(str) -
leftPadding);
+ } else {
+ osw.write(str);
}
+ osw.write(LineUtil.getSpaces(minPadding));
+ }
- private int getOutputWidth(String s) {
- int width = 0;
- for (int i = 0; i < s.length(); i++) {
- width += getCharWidth(s.charAt(i));
- }
+ /**
+ * Writes a character repeatedly to the output stream a specified number of
times. Used for
+ * creating horizontal lines and padding in the table.
+ *
+ * @param osw Output stream to write to.
+ * @param c Character to repeat.
+ * @param num Number of times to repeat the character (must be non-negative).
+ * @throws IOException If an I/O error occurs during writing.
+ * @throws IllegalArgumentException if num is negative.
+ */
+ private static void writeRepeated(OutputStreamWriter osw, char c, int num)
throws IOException {
+ for (int i = 0; i < num; i++) {
+ osw.append(c);
+ }
+ }
+
+ /**
+ * Formats a metalake into a table string representation. Creates a
two-column table with headers
+ * "METALAKE" and "COMMENT", containing the metalake's name and comment
respectively.
+ */
+ static final class MetalakeTableFormat extends TableFormat<Metalake> {
- return width;
+ /**
+ * Creates a new {@link TableFormat} with the specified properties.
+ *
+ * @param context the command context.
+ */
+ public MetalakeTableFormat(CommandContext context) {
+ super(context);
}
- private static int getCharWidth(char ch) {
- String s = String.valueOf(ch);
- if (FULL_WIDTH_PATTERN.matcher(s).find()) {
- return 2;
- }
+ /** {@inheritDoc} */
+ @Override
+ public String getOutput(Metalake metalake) {
+ Column columnName = new Column(context, "metalake");
+ Column columnComment = new Column(context, "comment");
+
+ columnName.addCell(metalake.name());
+ columnComment.addCell(metalake.comment());
- return 1;
+ return getTableFormat(columnName, columnComment);
}
+ }
- private void printLine() {
- System.out.print(crossDelimiter);
- for (int i = 0; i < maxElementLengths.length; ++i) {
- for (int j = 0; j < maxElementLengths[i] + indent.length() * 2; ++j) {
- System.out.print(horizontalDelimiter);
- }
- System.out.print(crossDelimiter);
- }
+ /**
+ * Formats an array of Metalakes into a single-column table display. Lists
all metalake names in a
+ * vertical format.
+ */
+ static final class MetalakeListTableFormat extends TableFormat<Metalake[]> {
+
+ public MetalakeListTableFormat(CommandContext context) {
+ super(context);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getOutput(Metalake[] metalakes) {
+
+ Column columnName = new Column(context, "metalake");
+ Arrays.stream(metalakes).forEach(metalake ->
columnName.addCell(metalake.name()));
+
+ return getTableFormat(columnName);
+ }
+ }
+
+ /**
+ * Formats a single Catalog instance into a four-column table display.
Displays catalog details
+ * including name, type, provider, and comment information.
+ */
+ static final class CatalogTableFormat extends TableFormat<Catalog> {
+
+ public CatalogTableFormat(CommandContext context) {
+ super(context);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getOutput(Catalog catalog) {
+ Column columnName = new Column(context, "catalog");
+ Column columnType = new Column(context, "type");
+ Column columnProvider = new Column(context, "provider");
+ Column columnComment = new Column(context, "comment");
+
+ columnName.addCell(catalog.name());
+ columnType.addCell(catalog.type().name());
+ columnProvider.addCell(catalog.provider());
+ columnComment.addCell(catalog.comment());
+
+ return getTableFormat(columnName, columnType, columnProvider,
columnComment);
+ }
+ }
+
+ /**
+ * Formats an array of Catalogs into a single-column table display. Lists
all catalog names in a
+ * vertical format.
+ */
+ static final class CatalogListTableFormat extends TableFormat<Catalog[]> {
+
+ public CatalogListTableFormat(CommandContext context) {
+ super(context);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getOutput(Catalog[] catalogs) {
+ Column columnName = new Column(context, "catalog");
+ Arrays.stream(catalogs).forEach(metalake ->
columnName.addCell(metalake.name()));
+
+ return getTableFormat(columnName);
}
}
}
diff --git
a/clients/cli/src/test/java/org/apache/gravitino/cli/integration/test/TableFormatOutputIT.java
b/clients/cli/src/test/java/org/apache/gravitino/cli/integration/test/TableFormatOutputIT.java
index f23d0284fb..bca9e960ff 100644
---
a/clients/cli/src/test/java/org/apache/gravitino/cli/integration/test/TableFormatOutputIT.java
+++
b/clients/cli/src/test/java/org/apache/gravitino/cli/integration/test/TableFormatOutputIT.java
@@ -106,7 +106,7 @@ public class TableFormatOutputIT extends BaseIT {
String output = new String(outputStream.toByteArray(),
StandardCharsets.UTF_8).trim();
assertEquals(
"+-------------+\n"
- + "| metalake |\n"
+ + "| Metalake |\n"
+ "+-------------+\n"
+ "| my_metalake |\n"
+ "+-------------+",
@@ -138,7 +138,7 @@ public class TableFormatOutputIT extends BaseIT {
String output = new String(outputStream.toByteArray(),
StandardCharsets.UTF_8).trim();
assertEquals(
"+-------------+-------------+\n"
- + "| metalake | comment |\n"
+ + "| Metalake | Comment |\n"
+ "+-------------+-------------+\n"
+ "| my_metalake | my metalake |\n"
+ "+-------------+-------------+",
@@ -170,7 +170,7 @@ public class TableFormatOutputIT extends BaseIT {
String output = new String(outputStream.toByteArray(),
StandardCharsets.UTF_8).trim();
assertEquals(
"+-----------+\n"
- + "| catalog |\n"
+ + "| Catalog |\n"
+ "+-----------+\n"
+ "| postgres |\n"
+ "| postgres2 |\n"
@@ -205,7 +205,7 @@ public class TableFormatOutputIT extends BaseIT {
String output = new String(outputStream.toByteArray(),
StandardCharsets.UTF_8).trim();
assertEquals(
"+----------+------------+-----------------+---------+\n"
- + "| catalog | type | provider | comment |\n"
+ + "| Catalog | Type | Provider | Comment |\n"
+ "+----------+------------+-----------------+---------+\n"
+ "| postgres | RELATIONAL | jdbc-postgresql | null |\n"
+ "+----------+------------+-----------------+---------+",
@@ -237,7 +237,7 @@ public class TableFormatOutputIT extends BaseIT {
String output = new String(outputStream.toByteArray(),
StandardCharsets.UTF_8).trim();
assertEquals(
"+-----------+------------+-----------------+-------------------+\n"
- + "| catalog | type | provider | comment
|\n"
+ + "| Catalog | Type | Provider | Comment
|\n"
+
"+-----------+------------+-----------------+-------------------+\n"
+ "| postgres2 | RELATIONAL | jdbc-postgresql | catalog, 用于测试 |\n"
+
"+-----------+------------+-----------------+-------------------+",
diff --git
a/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestPlainFormat.java
b/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestPlainFormat.java
new file mode 100644
index 0000000000..300a12e9d5
--- /dev/null
+++
b/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestPlainFormat.java
@@ -0,0 +1,171 @@
+/*
+ * 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.gravitino.cli.output;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Metalake;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.cli.CommandContext;
+import org.apache.gravitino.cli.outputs.PlainFormat;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestPlainFormat {
+
+ private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ private final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
+ private final PrintStream originalOut = System.out;
+ private final PrintStream originalErr = System.err;
+
+ @BeforeEach
+ void setUp() {
+ System.setOut(new PrintStream(outContent));
+ System.setErr(new PrintStream(errContent));
+ }
+
+ @AfterEach
+ public void restoreStreams() {
+ System.setOut(originalOut);
+ System.setErr(originalErr);
+ }
+
+ @Test
+ void testMetalakeDetailsWithPlainFormat() {
+ CommandContext mockContext = getMockContext();
+ Metalake mockMetalake = getMockMetalake();
+
+ PlainFormat.output(mockMetalake, mockContext);
+ String output = new String(outContent.toByteArray(),
StandardCharsets.UTF_8).trim();
+ Assertions.assertEquals("demo_metalake, This is a demo metalake", output);
+ }
+
+ @Test
+ void testListMetalakeWithPlainFormat() {
+ CommandContext mockContext = getMockContext();
+ Metalake mockMetalake1 = getMockMetalake("metalake1", "This is a
metalake");
+ Metalake mockMetalake2 = getMockMetalake("metalake2", "This is another
metalake");
+
+ PlainFormat.output(new Metalake[] {mockMetalake1, mockMetalake2},
mockContext);
+ String output = new String(outContent.toByteArray(),
StandardCharsets.UTF_8).trim();
+ Assertions.assertEquals("metalake1\n" + "metalake2", output);
+ }
+
+ @Test
+ void testCatalogDetailsWithPlainFormat() {
+ CommandContext mockContext = getMockContext();
+ Catalog mockCatalog = getMockCatalog();
+
+ PlainFormat.output(mockCatalog, mockContext);
+ String output = new String(outContent.toByteArray(),
StandardCharsets.UTF_8).trim();
+ Assertions.assertEquals(
+ "demo_catalog, RELATIONAL, demo_provider, This is a demo catalog",
output);
+ }
+
+ @Test
+ void testListCatalogWithPlainFormat() {
+ CommandContext mockContext = getMockContext();
+ Catalog mockCatalog1 =
+ getMockCatalog("catalog1", Catalog.Type.FILESET, "provider1", "This is
a catalog");
+ Catalog mockCatalog2 =
+ getMockCatalog("catalog2", Catalog.Type.RELATIONAL, "provider2", "This
is another catalog");
+
+ PlainFormat.output(new Catalog[] {mockCatalog1, mockCatalog2},
mockContext);
+ String output = new String(outContent.toByteArray(),
StandardCharsets.UTF_8).trim();
+ Assertions.assertEquals("catalog1\n" + "catalog2", output);
+ }
+
+ @Test
+ void testOutputWithUnsupportType() {
+ CommandContext mockContext = getMockContext();
+ Object mockObject = new Object();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ PlainFormat.output(mockObject, mockContext);
+ });
+ }
+
+ private CommandContext getMockContext() {
+ CommandContext mockContext = mock(CommandContext.class);
+
+ return mockContext;
+ }
+
+ private Metalake getMockMetalake() {
+ return getMockMetalake("demo_metalake", "This is a demo metalake");
+ }
+
+ private Metalake getMockMetalake(String name, String comment) {
+ Metalake mockMetalake = mock(Metalake.class);
+ when(mockMetalake.name()).thenReturn(name);
+ when(mockMetalake.comment()).thenReturn(comment);
+
+ return mockMetalake;
+ }
+
+ private Catalog getMockCatalog() {
+ return getMockCatalog(
+ "demo_catalog", Catalog.Type.RELATIONAL, "demo_provider", "This is a
demo catalog");
+ }
+
+ private Catalog getMockCatalog(String name, Catalog.Type type, String
provider, String comment) {
+ Catalog mockCatalog = mock(Catalog.class);
+ when(mockCatalog.name()).thenReturn(name);
+ when(mockCatalog.type()).thenReturn(type);
+ when(mockCatalog.provider()).thenReturn(provider);
+ when(mockCatalog.comment()).thenReturn(comment);
+
+ return mockCatalog;
+ }
+
+ private Schema getMockSchema() {
+ return getMockSchema("demo_schema", "This is a demo schema");
+ }
+
+ private Schema getMockSchema(String name, String comment) {
+ Schema mockSchema = mock(Schema.class);
+ when(mockSchema.name()).thenReturn(name);
+ when(mockSchema.comment()).thenReturn(comment);
+
+ return mockSchema;
+ }
+
+ private Audit getMockAudit() {
+ Audit mockAudit = mock(Audit.class);
+ when(mockAudit.creator()).thenReturn("demo_user");
+
when(mockAudit.createTime()).thenReturn(Instant.ofEpochMilli(1611111111111L));
+ when(mockAudit.lastModifier()).thenReturn("demo_user");
+
when(mockAudit.lastModifiedTime()).thenReturn(Instant.ofEpochMilli(1611111111111L));
+
+ return mockAudit;
+ }
+}
diff --git
a/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestTableFormat.java
b/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestTableFormat.java
new file mode 100644
index 0000000000..64d5ea4987
--- /dev/null
+++
b/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestTableFormat.java
@@ -0,0 +1,348 @@
+/*
+ * 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.gravitino.cli.output;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Metalake;
+import org.apache.gravitino.cli.CommandContext;
+import org.apache.gravitino.cli.outputs.Column;
+import org.apache.gravitino.cli.outputs.TableFormat;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestTableFormat {
+ private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ private final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
+ private final PrintStream originalOut = System.out;
+ private final PrintStream originalErr = System.err;
+
+ @BeforeEach
+ void setUp() {
+ System.setOut(new PrintStream(outContent));
+ System.setErr(new PrintStream(errContent));
+ }
+
+ @AfterEach
+ public void restoreStreams() {
+ System.setOut(originalOut);
+ System.setErr(originalErr);
+ }
+
+ @Test
+ void testCreateDefaultTableFormat() {
+ CommandContext mockContext = getMockContext();
+
+ Column columnA = new Column(mockContext, "metalake");
+ Column columnB = new Column(mockContext, "comment");
+
+ columnA.addCell("cell1").addCell("cell2").addCell("cell3");
+ columnB.addCell("cell4").addCell("cell5").addCell("cell6");
+
+ TableFormat<String> tableFormat =
+ new TableFormat<String>(mockContext) {
+ @Override
+ public String getOutput(String entity) {
+ return null;
+ }
+ };
+
+ String outputString = tableFormat.getTableFormat(columnA, columnB).trim();
+ Assertions.assertEquals(
+ "+----------+---------+\n"
+ + "| Metalake | Comment |\n"
+ + "+----------+---------+\n"
+ + "| cell1 | cell4 |\n"
+ + "| cell2 | cell5 |\n"
+ + "| cell3 | cell6 |\n"
+ + "+----------+---------+",
+ outputString);
+ }
+
+ @Test
+ void testTitleWithLeftAlign() {
+ CommandContext mockContext = getMockContext();
+
+ Column columnA =
+ new Column(
+ mockContext, "metalake", Column.HorizontalAlign.LEFT,
Column.HorizontalAlign.CENTER);
+ Column columnB =
+ new Column(
+ mockContext, "comment", Column.HorizontalAlign.LEFT,
Column.HorizontalAlign.CENTER);
+
+ columnA.addCell("cell1").addCell("cell2").addCell("cell3").addCell("very
long cell");
+ columnB.addCell("cell4").addCell("cell5").addCell("cell6").addCell("very
long cell");
+
+ TableFormat<String> tableFormat =
+ new TableFormat<String>(mockContext) {
+ @Override
+ public String getOutput(String entity) {
+ return null;
+ }
+ };
+
+ String outputString = tableFormat.getTableFormat(columnA, columnB).trim();
+ Assertions.assertEquals(
+ "+----------------+----------------+\n"
+ + "| Metalake | Comment |\n"
+ + "+----------------+----------------+\n"
+ + "| cell1 | cell4 |\n"
+ + "| cell2 | cell5 |\n"
+ + "| cell3 | cell6 |\n"
+ + "| very long cell | very long cell |\n"
+ + "+----------------+----------------+",
+ outputString);
+ }
+
+ @Test
+ void testTitleWithRightAlign() {
+ CommandContext mockContext = getMockContext();
+
+ Column columnA =
+ new Column(
+ mockContext, "metalake", Column.HorizontalAlign.RIGHT,
Column.HorizontalAlign.CENTER);
+ Column columnB =
+ new Column(
+ mockContext, "comment", Column.HorizontalAlign.RIGHT,
Column.HorizontalAlign.CENTER);
+
+ columnA.addCell("cell1").addCell("cell2").addCell("cell3").addCell("very
long cell");
+ columnB.addCell("cell4").addCell("cell5").addCell("cell6").addCell("very
long cell");
+
+ TableFormat<String> tableFormat =
+ new TableFormat<String>(mockContext) {
+ @Override
+ public String getOutput(String entity) {
+ return null;
+ }
+ };
+
+ String outputString = tableFormat.getTableFormat(columnA, columnB).trim();
+ Assertions.assertEquals(
+ "+----------------+----------------+\n"
+ + "| Metalake | Comment |\n"
+ + "+----------------+----------------+\n"
+ + "| cell1 | cell4 |\n"
+ + "| cell2 | cell5 |\n"
+ + "| cell3 | cell6 |\n"
+ + "| very long cell | very long cell |\n"
+ + "+----------------+----------------+",
+ outputString);
+ }
+
+ @Test
+ void testDataWithCenterAlign() {
+ CommandContext mockContext = getMockContext();
+
+ Column columnA =
+ new Column(
+ mockContext, "metalake", Column.HorizontalAlign.CENTER,
Column.HorizontalAlign.CENTER);
+ Column columnB =
+ new Column(
+ mockContext, "comment", Column.HorizontalAlign.CENTER,
Column.HorizontalAlign.CENTER);
+
+ columnA.addCell("cell1").addCell("cell2").addCell("cell3").addCell("very
long cell");
+ columnB.addCell("cell4").addCell("cell5").addCell("cell6").addCell("very
long cell");
+
+ TableFormat<String> tableFormat =
+ new TableFormat<String>(mockContext) {
+ @Override
+ public String getOutput(String entity) {
+ return null;
+ }
+ };
+
+ String outputString = tableFormat.getTableFormat(columnA, columnB).trim();
+ Assertions.assertEquals(
+ "+----------------+----------------+\n"
+ + "| Metalake | Comment |\n"
+ + "+----------------+----------------+\n"
+ + "| cell1 | cell4 |\n"
+ + "| cell2 | cell5 |\n"
+ + "| cell3 | cell6 |\n"
+ + "| very long cell | very long cell |\n"
+ + "+----------------+----------------+",
+ outputString);
+ }
+
+ @Test
+ void testDataWithRightAlign() {
+ CommandContext mockContext = getMockContext();
+
+ Column columnA =
+ new Column(
+ mockContext, "metalake", Column.HorizontalAlign.CENTER,
Column.HorizontalAlign.RIGHT);
+ Column columnB =
+ new Column(
+ mockContext, "comment", Column.HorizontalAlign.CENTER,
Column.HorizontalAlign.RIGHT);
+
+ columnA.addCell("cell1").addCell("cell2").addCell("cell3").addCell("very
long cell");
+ columnB.addCell("cell4").addCell("cell5").addCell("cell6").addCell("very
long cell");
+
+ TableFormat<String> tableFormat =
+ new TableFormat<String>(mockContext) {
+ @Override
+ public String getOutput(String entity) {
+ return null;
+ }
+ };
+
+ String outputString = tableFormat.getTableFormat(columnA, columnB).trim();
+ Assertions.assertEquals(
+ "+----------------+----------------+\n"
+ + "| Metalake | Comment |\n"
+ + "+----------------+----------------+\n"
+ + "| cell1 | cell4 |\n"
+ + "| cell2 | cell5 |\n"
+ + "| cell3 | cell6 |\n"
+ + "| very long cell | very long cell |\n"
+ + "+----------------+----------------+",
+ outputString);
+ }
+
+ @Test
+ void testMetalakeDetailsWithTableFormat() {
+ CommandContext mockContext = getMockContext();
+
+ Metalake mockMetalake = getMockMetalake();
+ TableFormat.output(mockMetalake, mockContext);
+
+ String output = new String(outContent.toByteArray(),
StandardCharsets.UTF_8).trim();
+ Assertions.assertEquals(
+ "+---------------+-------------------------+\n"
+ + "| Metalake | Comment |\n"
+ + "+---------------+-------------------------+\n"
+ + "| demo_metalake | This is a demo metalake |\n"
+ + "+---------------+-------------------------+",
+ output);
+ }
+
+ @Test
+ void testListMetalakeWithTableFormat() {
+ CommandContext mockContext = getMockContext();
+ Metalake mockMetalake1 = getMockMetalake("metalake1", "This is a
metalake");
+ Metalake mockMetalake2 = getMockMetalake("metalake2", "This is another
metalake");
+
+ TableFormat.output(new Metalake[] {mockMetalake1, mockMetalake2},
mockContext);
+ String output = new String(outContent.toByteArray(),
StandardCharsets.UTF_8).trim();
+ Assertions.assertEquals(
+ "+-----------+\n"
+ + "| Metalake |\n"
+ + "+-----------+\n"
+ + "| metalake1 |\n"
+ + "| metalake2 |\n"
+ + "+-----------+",
+ output);
+ }
+
+ @Test
+ void testCatalogDetailsWithTableFormat() {
+ CommandContext mockContext = getMockContext();
+ Catalog mockCatalog = getMockCatalog();
+
+ TableFormat.output(mockCatalog, mockContext);
+ String output = new String(outContent.toByteArray(),
StandardCharsets.UTF_8).trim();
+ Assertions.assertEquals(
+
"+--------------+------------+---------------+------------------------+\n"
+ + "| Catalog | Type | Provider | Comment
|\n"
+ +
"+--------------+------------+---------------+------------------------+\n"
+ + "| demo_catalog | RELATIONAL | demo_provider | This is a demo
catalog |\n"
+ +
"+--------------+------------+---------------+------------------------+",
+ output);
+ }
+
+ @Test
+ void testListCatalogWithTableFormat() {
+ CommandContext mockContext = getMockContext();
+ Catalog mockCatalog1 =
+ getMockCatalog("catalog1", Catalog.Type.FILESET, "provider1", "This is
a catalog");
+ Catalog mockCatalog2 =
+ getMockCatalog("catalog2", Catalog.Type.RELATIONAL, "provider2", "This
is another catalog");
+
+ TableFormat.output(new Catalog[] {mockCatalog1, mockCatalog2},
mockContext);
+ String output = new String(outContent.toByteArray(),
StandardCharsets.UTF_8).trim();
+ Assertions.assertEquals(
+ "+----------+\n"
+ + "| Catalog |\n"
+ + "+----------+\n"
+ + "| catalog1 |\n"
+ + "| catalog2 |\n"
+ + "+----------+",
+ output);
+ }
+
+ @Test
+ void testOutputWithUnsupportType() {
+ CommandContext mockContext = getMockContext();
+ Object mockObject = new Object();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ TableFormat.output(mockObject, mockContext);
+ });
+ }
+
+ private void addRepeatedCells(Column column, int count) {
+ for (int i = 0; i < count; i++) {
+ column.addCell(column.getHeader() + "-" + (i + 1));
+ }
+ }
+
+ private CommandContext getMockContext() {
+ CommandContext mockContext = mock(CommandContext.class);
+
+ return mockContext;
+ }
+
+ private Metalake getMockMetalake() {
+ return getMockMetalake("demo_metalake", "This is a demo metalake");
+ }
+
+ private Metalake getMockMetalake(String name, String comment) {
+ Metalake mockMetalake = mock(Metalake.class);
+ when(mockMetalake.name()).thenReturn(name);
+ when(mockMetalake.comment()).thenReturn(comment);
+
+ return mockMetalake;
+ }
+
+ private Catalog getMockCatalog() {
+ return getMockCatalog(
+ "demo_catalog", Catalog.Type.RELATIONAL, "demo_provider", "This is a
demo catalog");
+ }
+
+ private Catalog getMockCatalog(String name, Catalog.Type type, String
provider, String comment) {
+ Catalog mockCatalog = mock(Catalog.class);
+ when(mockCatalog.name()).thenReturn(name);
+ when(mockCatalog.type()).thenReturn(type);
+ when(mockCatalog.provider()).thenReturn(provider);
+ when(mockCatalog.comment()).thenReturn(comment);
+
+ return mockCatalog;
+ }
+}