From 32b0e3a36b236636f605e0634776cffe439a465f Mon Sep 17 00:00:00 2001 From: pancx Date: Sat, 18 Jan 2025 15:05:07 +0800 Subject: [PATCH 1/4] tmp --- clients/cli/build.gradle.kts | 1 + .../gravitino/cli/CatalogCommandHandler.java | 11 +- .../gravitino/cli/GravitinoCommandLine.java | 2 + .../gravitino/cli/GravitinoOptions.java | 10 +- .../gravitino/cli/MetalakeCommandHandler.java | 14 +- .../gravitino/cli/TestableCommandLine.java | 17 +- .../cli/commands/CatalogDetails.java | 12 +- .../gravitino/cli/commands/Command.java | 48 +- .../gravitino/cli/commands/ListCatalogs.java | 8 +- .../gravitino/cli/commands/ListMetalakes.java | 7 +- .../cli/commands/MetalakeDetails.java | 8 +- .../cli/outputs/BaseOutputFormat.java | 103 +++ .../apache/gravitino/cli/outputs/Column.java | 163 ++++ .../cli/outputs/HorizontalAlign.java | 35 + .../gravitino/cli/outputs/OutputConstant.java | 68 ++ .../gravitino/cli/outputs/OutputFormat.java | 29 +- .../gravitino/cli/outputs/OutputProperty.java | 211 +++++ .../cli/outputs/OverflowBehaviour.java | 25 + .../gravitino/cli/outputs/PlainFormat.java | 215 ++++- .../apache/gravitino/cli/outputs/Style.java | 75 ++ .../gravitino/cli/outputs/TableFormat.java | 812 ++++++++++++++---- .../apache/gravitino/cli/utils/LineUtil.java | 118 +++ .../gravitino/cli/TestCatalogCommands.java | 15 +- .../gravitino/cli/TestMetalakeCommands.java | 7 +- .../integration/test/TableFormatOutputIT.java | 61 +- .../gravitino/cli/output/TestColumn.java | 35 + .../gravitino/cli/output/TestPlainFormat.java | 212 +++++ .../gravitino/cli/output/TestTableFormat.java | 444 ++++++++++ 28 files changed, 2493 insertions(+), 273 deletions(-) create mode 100644 clients/cli/src/main/java/org/apache/gravitino/cli/outputs/BaseOutputFormat.java create mode 100644 clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Column.java create mode 100644 clients/cli/src/main/java/org/apache/gravitino/cli/outputs/HorizontalAlign.java create mode 100644 clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputConstant.java create mode 100644 clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputProperty.java create mode 100644 clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OverflowBehaviour.java create mode 100644 clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Style.java create mode 100644 clients/cli/src/main/java/org/apache/gravitino/cli/utils/LineUtil.java create mode 100644 clients/cli/src/test/java/org/apache/gravitino/cli/output/TestColumn.java create mode 100644 clients/cli/src/test/java/org/apache/gravitino/cli/output/TestPlainFormat.java create mode 100644 clients/cli/src/test/java/org/apache/gravitino/cli/output/TestTableFormat.java diff --git a/clients/cli/build.gradle.kts b/clients/cli/build.gradle.kts index 9358808f9f5..7129fcde4dd 100644 --- a/clients/cli/build.gradle.kts +++ b/clients/cli/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(libs.guava) implementation(libs.slf4j.api) implementation(libs.slf4j.simple) + implementation(libs.commons.lang3) implementation(project(":api")) implementation(project(":clients:client-java")) implementation(project(":common")) diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/CatalogCommandHandler.java b/clients/cli/src/main/java/org/apache/gravitino/cli/CatalogCommandHandler.java index 8e238406854..37e6b50461a 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/CatalogCommandHandler.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/CatalogCommandHandler.java @@ -24,6 +24,7 @@ import java.util.Map; import org.apache.commons.cli.CommandLine; import org.apache.gravitino.cli.commands.Command; +import org.apache.gravitino.cli.outputs.OutputProperty; /** * Handles the command execution for Catalogs based on command type and the command line options. @@ -128,8 +129,11 @@ private void handleDetailsCommand() { if (line.hasOption(GravitinoOptions.AUDIT)) { gravitinoCommandLine.newCatalogAudit(url, ignore, metalake, catalog).validate().handle(); } else { + // TODO: move this to GravitinoCommandLine class + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setOutputFormat(outputFormat); gravitinoCommandLine - .newCatalogDetails(url, ignore, outputFormat, metalake, catalog) + .newCatalogDetails(url, ignore, property, metalake, catalog) .validate() .handle(); } @@ -219,6 +223,9 @@ private void handleUpdateCommand() { /** Handles the "LIST" command. */ private void handleListCommand() { - gravitinoCommandLine.newListCatalogs(url, ignore, outputFormat, metalake).validate().handle(); + // TODO: move this to GravitinoCommandLine class + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setOutputFormat(outputFormat); + gravitinoCommandLine.newListCatalogs(url, ignore, property, metalake).validate().handle(); } } diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java index d7e257a8a81..6020bb589a6 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java @@ -27,6 +27,7 @@ import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; +import org.apache.gravitino.cli.outputs.OutputProperty; /* Gravitino Command line */ public class GravitinoCommandLine extends TestableCommandLine { @@ -107,6 +108,7 @@ public static void displayHelp(Options options) { /** Executes the appropriate command based on the command type. */ private void executeCommand() { + OutputProperty outputProperty = OutputProperty.fromLine(line); if (CommandActions.HELP.equals(command)) { handleHelpCommand(); } else if (line.hasOption(GravitinoOptions.OWNER)) { diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java index 47f9914233d..696f58dbf94 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java @@ -24,13 +24,17 @@ /* Gravitino Command line options */ public class GravitinoOptions { + public static final String ALIAS = "alias"; + public static final String ALL = "all"; public static final String AUDIT = "audit"; public static final String AUTO = "auto"; public static final String COLUMNFILE = "columnfile"; public static final String COMMENT = "comment"; public static final String DATATYPE = "datatype"; public static final String DEFAULT = "default"; + public static final String DISABLE = "disable"; public static final String DISTRIBUTION = "distribution"; + public static final String ENABLE = "enable"; public static final String FILESET = "fileset"; public static final String FORCE = "force"; public static final String GROUP = "group"; @@ -49,6 +53,7 @@ public class GravitinoOptions { public static final String PROPERTIES = "properties"; public static final String PROPERTY = "property"; public static final String PROVIDER = "provider"; + public static final String QUIET = "quiet"; public static final String RENAME = "rename"; public static final String ROLE = "role"; public static final String SERVER = "server"; @@ -59,10 +64,6 @@ public class GravitinoOptions { public static final String USER = "user"; public static final String VALUE = "value"; public static final String VERSION = "version"; - public static final String ALL = "all"; - public static final String ENABLE = "enable"; - public static final String DISABLE = "disable"; - public static final String ALIAS = "alias"; public static final String URI = "uri"; /** @@ -91,6 +92,7 @@ public Options options() { options.addOption(createSimpleOption(null, SORTORDER, "display sortorder information")); options.addOption(createSimpleOption(null, ENABLE, "enable entities")); options.addOption(createSimpleOption(null, DISABLE, "disable entities")); + options.addOption(createSimpleOption("q", QUIET, "disable command output")); // Create/update options options.addOption(createArgOption(RENAME, "new entity name")); diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/MetalakeCommandHandler.java b/clients/cli/src/main/java/org/apache/gravitino/cli/MetalakeCommandHandler.java index 993116f19f5..25c02919364 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/MetalakeCommandHandler.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/MetalakeCommandHandler.java @@ -21,6 +21,7 @@ import org.apache.commons.cli.CommandLine; import org.apache.gravitino.cli.commands.Command; +import org.apache.gravitino.cli.outputs.OutputProperty; /** * Handles the command execution for Metalakes based on command type and the command line options. @@ -113,7 +114,10 @@ private boolean executeCommand() { /** Handles the "LIST" command. */ private void handleListCommand() { String outputFormat = line.getOptionValue(GravitinoOptions.OUTPUT); - gravitinoCommandLine.newListMetalakes(url, ignore, outputFormat).validate().handle(); + // TODO: move this to GravitinoCommandLine class + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setOutputFormat(outputFormat); + gravitinoCommandLine.newListMetalakes(url, ignore, property).validate().handle(); } /** Handles the "DETAILS" command. */ @@ -121,11 +125,11 @@ private void handleDetailsCommand() { if (line.hasOption(GravitinoOptions.AUDIT)) { gravitinoCommandLine.newMetalakeAudit(url, ignore, metalake).validate().handle(); } else { + // TODO: move this to GravitinoCommandLine class String outputFormat = line.getOptionValue(GravitinoOptions.OUTPUT); - gravitinoCommandLine - .newMetalakeDetails(url, ignore, outputFormat, metalake) - .validate() - .handle(); + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setOutputFormat(outputFormat); + gravitinoCommandLine.newMetalakeDetails(url, ignore, property, metalake).validate().handle(); } } diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java index b229ef16aa3..4751ca509a7 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java @@ -141,6 +141,7 @@ import org.apache.gravitino.cli.commands.UpdateTopicComment; import org.apache.gravitino.cli.commands.UserAudit; import org.apache.gravitino.cli.commands.UserDetails; +import org.apache.gravitino.cli.outputs.OutputProperty; /* * Methods used for testing @@ -162,12 +163,12 @@ protected MetalakeAudit newMetalakeAudit(String url, boolean ignore, String meta } protected MetalakeDetails newMetalakeDetails( - String url, boolean ignore, String outputFormat, String metalake) { - return new MetalakeDetails(url, ignore, outputFormat, metalake); + String url, boolean ignore, OutputProperty property, String metalake) { + return new MetalakeDetails(url, ignore, property, metalake); } - protected ListMetalakes newListMetalakes(String url, boolean ignore, String outputFormat) { - return new ListMetalakes(url, ignore, outputFormat); + protected ListMetalakes newListMetalakes(String url, boolean ignore, OutputProperty property) { + return new ListMetalakes(url, ignore, property); } protected CreateMetalake newCreateMetalake( @@ -211,13 +212,13 @@ protected CatalogAudit newCatalogAudit( } protected CatalogDetails newCatalogDetails( - String url, boolean ignore, String outputFormat, String metalake, String catalog) { - return new CatalogDetails(url, ignore, outputFormat, metalake, catalog); + String url, boolean ignore, OutputProperty property, String metalake, String catalog) { + return new CatalogDetails(url, ignore, property, metalake, catalog); } protected ListCatalogs newListCatalogs( - String url, boolean ignore, String outputFormat, String metalake) { - return new ListCatalogs(url, ignore, outputFormat, metalake); + String url, boolean ignore, OutputProperty property, String metalake) { + return new ListCatalogs(url, ignore, property, metalake); } protected CreateCatalog newCreateCatalog( 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 a204f560d09..68e712ad592 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 @@ -21,6 +21,7 @@ import org.apache.gravitino.Catalog; import org.apache.gravitino.cli.ErrorMessages; +import org.apache.gravitino.cli.outputs.OutputProperty; import org.apache.gravitino.client.GravitinoClient; import org.apache.gravitino.exceptions.NoSuchCatalogException; import org.apache.gravitino.exceptions.NoSuchMetalakeException; @@ -35,14 +36,17 @@ public class CatalogDetails extends Command { * * @param url The URL of the Gravitino server. * @param ignoreVersions If true don't check the client/server versions match. - * @param outputFormat The output format. + * @param property The output format. * @param metalake The name of the metalake. * @param catalog The name of the catalog. */ public CatalogDetails( - String url, boolean ignoreVersions, String outputFormat, String metalake, String catalog) { - - super(url, ignoreVersions, outputFormat); + String url, + boolean ignoreVersions, + OutputProperty property, + String metalake, + String catalog) { + super(url, ignoreVersions, property); this.metalake = metalake; this.catalog = catalog; } 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 ea6abdd6393..0a501ceddbc 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 @@ -19,15 +19,20 @@ package org.apache.gravitino.cli.commands; +import static org.apache.gravitino.cli.outputs.OutputProperty.OUTPUT_FORMAT_PLAIN; +import static org.apache.gravitino.cli.outputs.OutputProperty.OUTPUT_FORMAT_TABLE; import static org.apache.gravitino.client.GravitinoClientBase.Builder; import com.google.common.base.Joiner; import java.io.File; +import java.io.OutputStream; + import org.apache.gravitino.cli.ErrorMessages; 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.OutputProperty; import org.apache.gravitino.cli.outputs.PlainFormat; import org.apache.gravitino.cli.outputs.TableFormat; import org.apache.gravitino.client.DefaultOAuth2TokenProvider; @@ -39,8 +44,7 @@ /* The base for all commands. */ public abstract class Command { - public static final String OUTPUT_FORMAT_TABLE = "table"; - public static final String OUTPUT_FORMAT_PLAIN = "plain"; + public static final Joiner COMMA_JOINER = Joiner.on(", ").skipNulls(); protected static String authentication = null; @@ -51,7 +55,7 @@ public abstract class Command { private static final String KERBEROS_AUTH = "kerberos"; private final String url; private final boolean ignoreVersions; - private final String outputFormat; + private final OutputProperty outputProperty; /** * Command constructor. @@ -60,9 +64,7 @@ public abstract class Command { * @param ignoreVersions If true don't check the client/server versions match. */ public Command(String url, boolean ignoreVersions) { - this.url = url; - this.ignoreVersions = ignoreVersions; - this.outputFormat = OUTPUT_FORMAT_PLAIN; + this(url, ignoreVersions, OutputProperty.defaultOutputProperty()); } /** @@ -70,12 +72,13 @@ public Command(String url, boolean ignoreVersions) { * * @param url The URL of the Gravitino server. * @param ignoreVersions If true don't check the client/server versions match. - * @param outputFormat output format used in some commands + * @param outputProperty The output property to use for the command. */ - public Command(String url, boolean ignoreVersions, String outputFormat) { + public Command(String url, boolean ignoreVersions, OutputProperty outputProperty) { this.url = url; this.ignoreVersions = ignoreVersions; - this.outputFormat = outputFormat; + this.outputProperty = + outputProperty == null ? OutputProperty.defaultOutputProperty() : outputProperty; } /** @@ -212,15 +215,28 @@ protected Builder constructClient(Builder * @param The type of entity. */ protected void output(T entity) { - if (outputFormat == null) { - PlainFormat.output(entity); - return; + if (OUTPUT_FORMAT_PLAIN.equals(outputProperty.getOutputFormat())) { + PlainFormat.output(entity, outputProperty); + } else if (OUTPUT_FORMAT_TABLE.equals(outputProperty.getOutputFormat())) { + TableFormat.output(entity, outputProperty); + } else { + throw new IllegalArgumentException("Unsupported output format"); } + } + + protected void outputInfo(String message) { + output(message, System.out); + } + + protected void outputError(String message) { + output(message, System.err); + } - if (outputFormat.equals(OUTPUT_FORMAT_TABLE)) { - TableFormat.output(entity); - } else if (outputFormat.equals(OUTPUT_FORMAT_PLAIN)) { - PlainFormat.output(entity); + protected void output(String message, OutputStream os) { + if (OUTPUT_FORMAT_PLAIN.equals(outputProperty.getOutputFormat())) { + PlainFormat.output(message, os); + } else if (OUTPUT_FORMAT_TABLE.equals(outputProperty.getOutputFormat())) { + TableFormat.output(message, os); } 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 e6aaf811ec9..c92db25e0d6 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 @@ -21,6 +21,7 @@ import org.apache.gravitino.Catalog; import org.apache.gravitino.cli.ErrorMessages; +import org.apache.gravitino.cli.outputs.OutputProperty; import org.apache.gravitino.client.GravitinoClient; import org.apache.gravitino.exceptions.NoSuchMetalakeException; @@ -34,11 +35,12 @@ public class ListCatalogs extends Command { * * @param url The URL of the Gravitino server. * @param ignoreVersions If true don't check the client/server versions match. - * @param outputFormat The output format. + * @param property The output property to use. * @param metalake The name of the metalake. */ - public ListCatalogs(String url, boolean ignoreVersions, String outputFormat, String metalake) { - super(url, ignoreVersions, outputFormat); + public ListCatalogs( + String url, boolean ignoreVersions, OutputProperty property, String metalake) { + super(url, ignoreVersions, property); this.metalake = metalake; } 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 ee5ac81d646..59859c6c9de 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 @@ -20,6 +20,7 @@ package org.apache.gravitino.cli.commands; import org.apache.gravitino.Metalake; +import org.apache.gravitino.cli.outputs.OutputProperty; import org.apache.gravitino.client.GravitinoAdminClient; /** Lists all metalakes. */ @@ -30,10 +31,10 @@ public class ListMetalakes extends Command { * * @param url The URL of the Gravitino server. * @param ignoreVersions If true don't check the client/server versions match. - * @param outputFormat The output format. + * @param property The output format. */ - public ListMetalakes(String url, boolean ignoreVersions, String outputFormat) { - super(url, ignoreVersions, outputFormat); + public ListMetalakes(String url, boolean ignoreVersions, OutputProperty property) { + super(url, ignoreVersions, property); } /** Lists all metalakes. */ 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 ea503710d42..53537af5a72 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 @@ -21,6 +21,7 @@ import org.apache.gravitino.Metalake; import org.apache.gravitino.cli.ErrorMessages; +import org.apache.gravitino.cli.outputs.OutputProperty; import org.apache.gravitino.client.GravitinoClient; import org.apache.gravitino.exceptions.NoSuchMetalakeException; @@ -33,11 +34,12 @@ public class MetalakeDetails extends Command { * * @param url The URL of the Gravitino server. * @param ignoreVersions If true don't check the client/server versions match. - * @param outputFormat The output format. + * @param property The output property to use. * @param metalake The name of the metalake. */ - public MetalakeDetails(String url, boolean ignoreVersions, String outputFormat, String metalake) { - super(url, ignoreVersions, outputFormat); + public MetalakeDetails( + String url, boolean ignoreVersions, OutputProperty property, String metalake) { + super(url, ignoreVersions, property); this.metalake = metalake; } 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 00000000000..d982f28e02d --- /dev/null +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/BaseOutputFormat.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 java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; + +/** + * Abstract base implementation of {@link OutputFormat} interface providing common functionality for + * various output format implementations. This class handles basic output operations and provides + * configurable behavior for quiet mode, output limiting, and sorting. + * + * @author pancx + */ +public abstract class BaseOutputFormat implements OutputFormat { + protected boolean quiet; + protected int limit; + protected boolean sort; + + /** + * Creates a new {@link BaseOutputFormat} with specified configuration. + * + * @param quiet If true, suppresses all output except for errors. + * @param limit Maximum number of items to output (use a negative for unlimited). + * @param sort If true, output will be sorted according to implementation. + */ + public BaseOutputFormat(boolean quiet, int limit, boolean sort) { + this.quiet = quiet; + this.limit = limit; + this.sort = sort; + } + + /** + * 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"); + } + 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) { + if (quiet) return; + 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 00000000000..a508dc3268b --- /dev/null +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Column.java @@ -0,0 +1,163 @@ +/* + * 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 java.util.Locale; +import org.apache.gravitino.cli.utils.LineUtil; + +/** + * Represents a column in a formatted table output. Manages column properties including header, + * footer, alignment, visibility, and content cells. Handles width calculations and content overflow + * behavior. + */ +public class Column { + private final String header; + private final String footer; + private final HorizontalAlign headerAlign; + private final HorizontalAlign dataAlign; + private final HorizontalAlign footerAlign; + private OverflowBehaviour overflowBehaviour; + private int maxWidth; + private boolean visible; + private final OutputProperty property; + private List cellContents; + + /** + * Creates a {@link Column} instance with specified header, footer and output properties. + * Initializes the column with default values and empty cell list. + * + * @param header Column header text (will be converted to uppercase). + * @param footer Column footer text. + * @param property a {@link OutputProperty} instance containing Output formatting properties. + */ + public Column(String header, String footer, OutputProperty property) { + this.property = property; + this.header = header == null ? "" : header.toUpperCase(Locale.ENGLISH); + this.footer = footer; + this.headerAlign = property.getHeaderAlign(); + this.dataAlign = property.getDataAlign(); + this.footerAlign = property.getFooterAlign(); + this.overflowBehaviour = property.getOverflowBehaviour(); + this.visible = true; + this.maxWidth = LineUtil.getDisplayWidth(header); + this.cellContents = Lists.newArrayList(); + } + + /** + * Creates a copy of this column with the same properties but empty cells. + * + * @return New {@link Column} instance with copied properties. + */ + public Column copy() { + Column newColumn = new Column(header, footer, property); + newColumn.setOverflowBehaviour(overflowBehaviour); + newColumn.setVisible(visible); + + return newColumn; + } + + /** + * Adds a new cell to the column and updates the maximum width if necessary. Null values are + * converted to "null" string. + * + * @param cell Cell content to add. + * @return This column instance for method chaining. + */ + public Column addCell(String cell) { + if (cell == null) { + cell = "null"; + } + + maxWidth = Math.max(maxWidth, LineUtil.getDisplayWidth(cell)); + cellContents.add(cell); + return this; + } + + /** + * Creates a new {@link Column} with limited number of cells and an ellipsis indicator. + * + * @param limit Maximum number of cells to include + * @return New Column instance with limited 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(String.valueOf(OutputConstant.ELLIPSIS)); + + return newColumn; + } + + private void reCalculateMaxWidth() { + for (String cell : cellContents) { + maxWidth = Math.max(maxWidth, LineUtil.getDisplayWidth(cell)); + } + } + + public String getCell(int index) { + return cellContents.get(index); + } + + public int getCellCount() { + return cellContents.size(); + } + + public String getHeader() { + return header; + } + + public String getFooter() { + return footer; + } + + public HorizontalAlign getHeaderAlign() { + return headerAlign; + } + + public HorizontalAlign getDataAlign() { + return dataAlign; + } + + public HorizontalAlign getFooterAlign() { + return footerAlign; + } + + public int getMaxWidth() { + return maxWidth; + } + + public void setMaxWidth(int maxWidth) { + this.maxWidth = maxWidth; + } + + public void setOverflowBehaviour(OverflowBehaviour overflowBehaviour) { + this.overflowBehaviour = overflowBehaviour; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } +} diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/HorizontalAlign.java b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/HorizontalAlign.java new file mode 100644 index 00000000000..f51f3cdb991 --- /dev/null +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/HorizontalAlign.java @@ -0,0 +1,35 @@ +/* + * 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; + +/** + * Specifies the horizontal text alignment within table elements such as cells, headers, and + * footers. This enum provides options for standard left-to-right text positioning. + */ +public enum HorizontalAlign { + /** Text is aligned to the left side */ + LEFT, + + /** Text is centered horizontally */ + CENTER, + + /** Text is aligned to the right side */ + RIGHT +} 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 00000000000..fd5e63311fc --- /dev/null +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputConstant.java @@ -0,0 +1,68 @@ +/* + * 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; + +/** + * Defines constants for rendering ASCII-art tables, including border characters and their indices. + * Provides two sets of border characters: + */ +public class OutputConstant { + public static final char ELLIPSIS = '…'; + + public static final ImmutableList FANCY_ASCII = + ImmutableList.of( + '╔', '═', '╤', '╗', '║', '│', '║', '╠', '═', '╪', '╣', '║', '│', '║', '╟', '─', '┼', '╢', + '╠', '═', '╪', '╣', '║', '│', '║', '╚', '═', '╧', '╝'); + public static final ImmutableList 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; +} 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 8e6ab311628..fe0d7b7c9e0 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 { - 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/OutputProperty.java b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputProperty.java new file mode 100644 index 00000000000..d4a003d486f --- /dev/null +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputProperty.java @@ -0,0 +1,211 @@ +/* + * 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 org.apache.commons.cli.CommandLine; +import org.apache.gravitino.cli.GravitinoOptions; + +/** + * Configuration properties for controlling output formatting and behavior. This class encapsulates + * all settings that affect how data is formatted and displayed, including alignment, styling, and + * overflow handling. + */ +public class OutputProperty { + public static final String OUTPUT_FORMAT_TABLE = "table"; + public static final String OUTPUT_FORMAT_PLAIN = "plain"; + + /** Default configuration with common settings */ + private static final OutputProperty DEFAULT_OUTPUT_PROPERTY = + new OutputProperty( + false, + false, + -1, + Style.BASIC2, + HorizontalAlign.CENTER, + HorizontalAlign.LEFT, + HorizontalAlign.CENTER, + OUTPUT_FORMAT_PLAIN, + OverflowBehaviour.CLIP_RIGHT, + true); + + private boolean sort; + private boolean quiet; + private int limit; + private Style style; + private HorizontalAlign headerAlign; + private HorizontalAlign dataAlign; + private final HorizontalAlign footerAlign; + private String outputFormat; + private final OverflowBehaviour overflowBehaviour; + private boolean rowNumbersEnabled; + + /** + * Creates a new {@link OutputProperty} with specified configurations. + * + * @param sort Whether to sort the output. + * @param quiet Whether to suppress output. + * @param limit Maximum number of rows (-1 for unlimited). + * @param style Border style for tables. + * @param headerAlign Header text alignment. + * @param dataAlign Data cell alignment. + * @param footerAlign Footer text alignment. + * @param outputFormat Output format type. + * @param overflowBehaviour Overflow handling strategy. + * @param rowNumbersEnabled Whether to show row numbers. + */ + public OutputProperty( + boolean sort, + boolean quiet, + int limit, + Style style, + HorizontalAlign headerAlign, + HorizontalAlign dataAlign, + HorizontalAlign footerAlign, + String outputFormat, + OverflowBehaviour overflowBehaviour, + boolean rowNumbersEnabled) { + this.sort = sort; + this.quiet = quiet; + this.limit = limit; + this.style = style; + this.headerAlign = headerAlign; + this.dataAlign = dataAlign; + this.footerAlign = footerAlign; + this.outputFormat = outputFormat; + this.overflowBehaviour = overflowBehaviour; + this.rowNumbersEnabled = rowNumbersEnabled; + } + + /** + * Returns a new instance with default output properties. + * + * @return Default configuration instance. + */ + public static OutputProperty defaultOutputProperty() { + return DEFAULT_OUTPUT_PROPERTY.copy(); + } + + /** + * Creates a new {@link OutputProperty} instance from command line arguments. + * + * @param line Command line. + * @return Configured {@code OutputProperty} instance. + */ + public static OutputProperty fromLine(CommandLine line) { + OutputProperty outputProperty = defaultOutputProperty(); + if (line.hasOption(GravitinoOptions.QUIET)) { + outputProperty.setQuiet(true); + } + + // TODO: implement other options. + return outputProperty; + } + + public boolean isSort() { + return sort; + } + + public boolean isQuiet() { + return quiet; + } + + public int getLimit() { + return limit; + } + + public Style getStyle() { + return style; + } + + public HorizontalAlign getHeaderAlign() { + return headerAlign; + } + + public HorizontalAlign getDataAlign() { + return dataAlign; + } + + public HorizontalAlign getFooterAlign() { + return footerAlign; + } + + public String getOutputFormat() { + return outputFormat; + } + + public OverflowBehaviour getOverflowBehaviour() { + return overflowBehaviour; + } + + public boolean isRowNumbersEnabled() { + return rowNumbersEnabled; + } + + public void setOutputFormat(String outputFormat) { + this.outputFormat = outputFormat; + } + + public void setSort(boolean sort) { + this.sort = sort; + } + + public void setQuiet(boolean quiet) { + this.quiet = quiet; + } + + public void setLimit(int limit) { + this.limit = limit; + } + + public void setStyle(Style style) { + this.style = style; + } + + public void setRowNumbersEnabled(boolean rowNumbersEnabled) { + this.rowNumbersEnabled = rowNumbersEnabled; + } + + public void setHeaderAlign(HorizontalAlign headerAlign) { + this.headerAlign = headerAlign; + } + + public void setDataAlign(HorizontalAlign dataAlign) { + this.dataAlign = dataAlign; + } + + /** + * Creates a new instance with current property values. + * + * @return New {@code OutputProperty} instance + */ + public OutputProperty copy() { + return new OutputProperty( + sort, + quiet, + limit, + style, + headerAlign, + dataAlign, + footerAlign, + outputFormat, + overflowBehaviour, + rowNumbersEnabled); + } +} diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OverflowBehaviour.java b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OverflowBehaviour.java new file mode 100644 index 00000000000..5390f8bf8e4 --- /dev/null +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OverflowBehaviour.java @@ -0,0 +1,25 @@ +/* + * 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; + +/** Defines the behavior of the CLI when the output is too wide to fit the terminal. */ +public enum OverflowBehaviour { + CLIP_RIGHT +} 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 66e616c4f78..fd3873f3db4 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,219 @@ import java.util.stream.Collectors; import org.apache.gravitino.Catalog; import org.apache.gravitino.Metalake; +import org.apache.gravitino.Schema; +import org.apache.gravitino.rel.Table; -/** Plain format to print a pretty string to standard out. */ -public class PlainFormat { - public static void output(Object object) { +/** + * Formats entity into plain text representation for command-line output. Supports formatting of + * single objects and arrays of Metalake, Catalog, Schema, and Table objects. Each supported type + * has its own specialized formatter as an inner class. + */ +public abstract class PlainFormat extends BaseOutputFormat { + + /** + * 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 object The object to format + * @param property Configuration properties for output formatting + * @throws IllegalArgumentException if the object type is not supported + */ + public static void output(Object object, OutputProperty property) { if (object instanceof Metalake) { - new MetalakePlainFormat().output((Metalake) object); + new MetalakePlainFormat(property).output((Metalake) object); } else if (object instanceof Metalake[]) { - new MetalakesPlainFormat().output((Metalake[]) object); + new MetalakesPlainFormat(property).output((Metalake[]) object); } else if (object instanceof Catalog) { - new CatalogPlainFormat().output((Catalog) object); + new CatalogPlainFormat(property).output((Catalog) object); } else if (object instanceof Catalog[]) { - new CatalogsPlainFormat().output((Catalog[]) object); + new CatalogsPlainFormat(property).output((Catalog[]) object); + } else if (object instanceof Schema) { + new SchemaPlainFormat(property).output((Schema) object); + } else if (object instanceof Schema[]) { + new SchemasPlainFormat(property).output((Schema[]) object); + } else if (object instanceof Table) { + new TablePlainFormat(property).output((Table) object); + } else if (object instanceof Table[]) { + new TablesPlainFormat(property).output((Table[]) object); } else { throw new IllegalArgumentException("Unsupported object type"); } } - static final class MetalakePlainFormat implements OutputFormat { + /** + * Creates a new {@link PlainFormat} with the specified output properties. + * + * @param property Configuration for controlling output behavior + */ + public PlainFormat(OutputProperty property) { + super(property.isQuiet(), property.getLimit(), property.isSort()); + } + + /** + * Formats a single {@link Metalake} instance as a comma-separated string. Output format: name, + * comment + */ + static final class MetalakePlainFormat extends PlainFormat { + + public MetalakePlainFormat(OutputProperty property) { + super(property); + } + + /** {@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 { + /** + * Formats an array of Metalakes, outputting one name per line. Returns null if the array is empty + * or null. + */ + static final class MetalakesPlainFormat extends PlainFormat { + + public MetalakesPlainFormat(OutputProperty property) { + super(property); + } + + /** {@inheritDoc} */ @Override - public void output(Metalake[] metalakes) { - if (metalakes.length == 0) { - System.out.println("No metalakes exist."); + public String getOutput(Metalake[] metalakes) { + if (metalakes == null || metalakes.length == 0) { + return null; } else { List metalakeNames = Arrays.stream(metalakes).map(Metalake::name).collect(Collectors.toList()); - String all = String.join(System.lineSeparator(), metalakeNames); - System.out.println(all); + return NEWLINE_JOINER.join(metalakeNames); } } } - static final class CatalogPlainFormat implements OutputFormat { + /** + * Formats a single {@link Catalog} instance as a comma-separated string. Output format: name, + * type, provider, comment + */ + static final class CatalogPlainFormat extends PlainFormat { + public CatalogPlainFormat(OutputProperty property) { + super(property); + } + + /** {@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 { + /** + * Formats an array of Catalogs, outputting one name per line. Returns null if the array is empty + * or null. + */ + static final class CatalogsPlainFormat extends PlainFormat { + public CatalogsPlainFormat(OutputProperty property) { + super(property); + } + + /** {@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 exists.", System.err); + return null; } else { List 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); + } + } + } + + /** + * Formats a single {@link Schema} instance as a comma-separated string. Output format: name, + * comment + */ + static final class SchemaPlainFormat extends PlainFormat { + public SchemaPlainFormat(OutputProperty property) { + super(property); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Schema schema) { + return COMMA_JOINER.join(schema.name(), schema.comment()); + } + } + + /** + * Formats an array of Schemas, outputting one name per line. Returns null if the array is empty + * or null. + */ + static final class SchemasPlainFormat extends PlainFormat { + public SchemasPlainFormat(OutputProperty property) { + super(property); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Schema[] schemas) { + if (schemas == null || schemas.length == 0) { + return null; + } else { + List schemaNames = + Arrays.stream(schemas).map(Schema::name).collect(Collectors.toList()); + return NEWLINE_JOINER.join(schemaNames); + } + } + } + + /** + * Formats a single Table instance with detailed column information. Output format: table_name + * column1_name, column1_type, column1_comment column2_name, column2_type, column2_comment ... + * table_comment + */ + static final class TablePlainFormat extends PlainFormat { + public TablePlainFormat(OutputProperty property) { + super(property); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Table table) { + StringBuilder output = new StringBuilder(table.name() + System.lineSeparator()); + List columnOutput = + Arrays.stream(table.columns()) + .map( + column -> + COMMA_JOINER.join( + column.name(), column.dataType().simpleString(), column.comment())) + .collect(Collectors.toList()); + output.append(NEWLINE_JOINER.join(columnOutput)); + output.append(System.lineSeparator()); + output.append(table.comment()); + return output.toString(); + } + } + + /** + * Formats an array of Tables, outputting one name per line. Returns null if the array is empty or + * null. + */ + static final class TablesPlainFormat extends PlainFormat { + public TablesPlainFormat(OutputProperty property) { + super(property); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Table[] tables) { + if (tables == null || tables.length == 0) { + return null; + } else { + List tableNames = + Arrays.stream(tables).map(Table::name).collect(Collectors.toList()); + return NEWLINE_JOINER.join(tableNames); } } } diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Style.java b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Style.java new file mode 100644 index 00000000000..16989f0a866 --- /dev/null +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Style.java @@ -0,0 +1,75 @@ +/* + * 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 static org.apache.gravitino.cli.outputs.OutputConstant.BASIC_ASCII; +import static org.apache.gravitino.cli.outputs.OutputConstant.FANCY_ASCII; + +import com.google.common.collect.ImmutableList; +import java.util.List; + +/** + * Defines different styles for formatting and displaying data. Each style contains a specific set + * of characters for rendering and configuration for whether to show boundaries between data rows. + */ +public enum Style { + FANCY(FANCY_ASCII, false), + FANCY2(FANCY_ASCII, true), + BASIC(BASIC_ASCII, true), + BASIC2(BASIC_ASCII, false); + + /** + * A simple style using basic ASCII characters for display. Shows boundaries between data rows for + * better visual separation. + */ + private final ImmutableList characters; + + private final boolean showRowBoundaries; + + /** + * Constructs a {@link Style} instance. + * + * @param characters the list of characters to use for the style + * @param showRowBoundaries {@code true} to show boundaries between data rows, {@code false} + * otherwise + */ + Style(ImmutableList characters, boolean showRowBoundaries) { + this.characters = characters; + this.showRowBoundaries = showRowBoundaries; + } + + /** + * Returns the list of characters used for this style. + * + * @return the list of characters used for rendering + */ + public List getCharacters() { + return characters; + } + + /** + * Indicates whether this style shows boundaries between data rows. + * + * @return {@code true} if boundaries between data rows are shown, {@code false} otherwise + */ + public boolean isRowBoundariesEnabled() { + return showRowBoundaries; + } +} 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 a3c99756524..4ec7f649e56 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,203 +18,705 @@ */ 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 com.google.common.base.Preconditions; +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.commons.lang3.StringUtils; import org.apache.gravitino.Catalog; import org.apache.gravitino.Metalake; +import org.apache.gravitino.Schema; +import org.apache.gravitino.cli.utils.LineUtil; + +/** + * 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 extends BaseOutputFormat { -/** 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); + public static final int PADDING = 1; + private final Style style; + private final boolean rowNumbersEnabled; + protected final OutputProperty property; + + /** + * 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 property Output configuration properties. + * @throws IllegalArgumentException if the object type is not supported + */ + public static void output(Object entity, OutputProperty property) { + if (entity instanceof Metalake) { + new MetalakeTableFormat(property).output((Metalake) entity); + } else if (entity instanceof Metalake[]) { + new MetalakesTableFormat(property).output((Metalake[]) entity); + } else if (entity instanceof Catalog) { + new CatalogTableFormat(property).output((Catalog) entity); + } else if (entity instanceof Catalog[]) { + new CatalogsTableFormat(property).output((Catalog[]) entity); + } else if (entity instanceof Schema) { + new SchemaTableFormat(property).output((Schema) entity); + } else if (entity instanceof Schema[]) { + new SchemasTableFormat(property).output((Schema[]) entity); } else { throw new IllegalArgumentException("Unsupported object type"); } } - static final class MetalakeTableFormat implements OutputFormat { - @Override - public void output(Metalake metalake) { - List headers = Arrays.asList("metalake", "comment"); - List> 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 property Configuration for table formatting and output behavior + */ + public TableFormat(OutputProperty property) { + super(property.isQuiet(), property.getLimit(), property.isSort()); + this.property = property; + this.style = property.getStyle(); + this.rowNumbersEnabled = property.isRowNumbersEnabled(); } - static final class MetalakesTableFormat implements OutputFormat { - @Override - public void output(Metalake[] metalakes) { - if (metalakes.length == 0) { - System.out.println("No metalakes exist."); - } else { - List headers = Collections.singletonList("metalake"); - List> 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 formatTable(Column... columns) { + checkColumns(columns); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + String[] headers = + Arrays.stream(columns) + .map(Column::getHeader) + .filter(Objects::nonNull) + .toArray(String[]::new); + + String[] footers = + Arrays.stream(columns) + .map(Column::getFooter) + .filter(StringUtils::isNotBlank) + .toArray(String[]::new); + + List borders = style.getCharacters(); + checkHeaders(headers, columns); + + if (headers.length != columns.length) { + throw new IllegalArgumentException("Headers must be provided for all columns"); + } + + if (limit != -1) { + columns = getLimitedColumns(columns); + } + + if (isRowNumbersEnabled()) { + Column numberColumn = new Column("", "", property); + int cellCount = columns[0].getCellCount(); + for (int i = 0; i < cellCount; i++) { + numberColumn.addCell(String.valueOf(i + 1)); } + columns = LineUtil.addFirst(numberColumn, 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, style, columns, System.lineSeparator()); + + if (footers.length > 0) { + writeRowSeparator(osw, style, System.lineSeparator(), columns); + writeFooter(osw, borders, columns); + } + + writeBottomBorder(osw, borders, System.lineSeparator(), columns); + + } catch (IOException e) { + throw new RuntimeException(e); + } + + return baos.toString(); } - static final class CatalogTableFormat implements OutputFormat { - @Override - public void output(Catalog catalog) { - List headers = Arrays.asList("catalog", "type", "provider", "comment"); - List> rows = new ArrayList<>(); - rows.add( - Arrays.asList( - catalog.name(), - catalog.type().toString(), - catalog.provider(), - catalog.comment() + "")); - TableFormatImpl tableFormat = new TableFormatImpl(); - tableFormat.print(headers, rows); + private void checkColumns(Column... columns) { + // TODO: automatic filling of missing data with empty strings + Preconditions.checkArgument(columns.length > 0, "At least one column must be provided"); + int cellCount = columns[0].getCellCount(); + for (Column column : columns) { + Preconditions.checkArgument( + column.getCellCount() == cellCount, "All columns must have the same cell count"); } } - static final class CatalogsTableFormat implements OutputFormat { - @Override - public void output(Catalog[] catalogs) { - if (catalogs.length == 0) { - System.out.println("No catalogs exist."); - } else { - List headers = Collections.singletonList("catalog"); - List> 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); - } + private void checkHeaders(String[] headers, Column[] columns) { + // TODO: automatic filling of missing data with empty strings + Preconditions.checkArgument( + headers.length == columns.length, "Headers must be provided for all columns"); + for (String header : headers) { + Preconditions.checkArgument(header != null, "Headers must not be null"); } } - 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 = " "; + /** + * Limits the number of rows in the table columns based on a predefined limit. If the current cell + * count is below the limit, returns the original columns unchanged. Otherwise, creates new + * columns with data truncated to the limit. + * + * @param columns The array of columns to potentially limit + * @return A new array of columns with limited rows, or the original array if no limiting is + * needed + * @throws IllegalArgumentException If the columns array is null or empty + */ + private Column[] getLimitedColumns(Column[] columns) { + if (columns[0].getCellCount() < limit) { + return columns; + } - public void debug() { - System.out.println(); - Arrays.stream(maxElementLengths).forEach(e -> System.out.print(e + " ")); + Column[] limitedColumns = new Column[columns.length]; + for (int i = 0; i < columns.length; i++) { + limitedColumns[i] = columns[i].getLimitedColumn(limit); } - public void print(List headers, List> 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)); - } - System.out.println(verticalDelimiter); - printLine(); - System.out.println(); - - // print rows - for (int i = 0; i < rows.size(); ++i) { - List 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); + return limitedColumns; + } + + /** + * Sort the columns based on the first column. + * + * @param columns the array of columns to sort. + * @return the sorted array of columns. + */ + private Column[] getSortedColumns(Column[] columns) { + // TODO: implement sorting + return columns; + } + + /** + * 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 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 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 style the table style containing border characters + * @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, Style style, String lineSeparator, Column[] columns) + throws IOException { + List borders = style.getCharacters(); + 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 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. + * + *

For each row of data: + * + *

    + *
  • Writes the data line with appropriate borders and alignment + *
  • If not the last row and row boundaries are enabled in the style, writes a separator line + * between rows + *
+ * + * @param writer the writer for output + * @param style the table style containing border characters and row boundary settings + * @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, Style style, Column[] columns, String lineSeparator) + throws IOException { + List borders = style.getCharacters(); + int dataSize = columns[0].getCellCount(); + HorizontalAlign[] dataAligns = + Arrays.stream(columns).map(Column::getDataAlign).toArray(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); + + if (i < dataSize - 1 && style.isRowBoundariesEnabled()) { + writeRowSeparator(writer, style, lineSeparator, columns); } - printLine(); - // add one more line - System.out.println(""); + } + } + + /** + * 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); } - private void updateMaxLengthsFromList(List elements) { - String s; - for (int i = 0; i < elements.size(); ++i) { - s = elements.get(i); - if (getOutputWidth(s) > maxElementLengths[i]) maxElementLengths[i] = getOutputWidth(s); + for (int col = 0; col < colWidths.length; col++) { + writeRepeated(osw, middle, colWidths[col]); + if (columnSeparator != null && col != colWidths.length - 1) { + osw.write(columnSeparator); } } - private void updateMaxLengthsFromNestedList(List> elements) { - int rowIdx = 0; - for (List 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++; + 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 borders, String lineSeparator, Column[] columns) + throws IOException { + HorizontalAlign[] dataAligns = + Arrays.stream(columns).map(Column::getHeaderAlign).toArray(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); + } + + /** + * Writes the footer row of the table with proper alignment and borders. Processes all column + * footers, filters out null values, and applies footer-specific alignments for each column. + * + * @param osw The output stream writer to write the formatted footer. + * @param borders List of border characters where: borders.get(4) - left border character. + * borders.get(5) - column separator character. borders.get(6) - right border character. + * @param columns Array of columns containing footer content and formatting properties. + * @throws IOException If an error occurs while writing to the output stream. + */ + private static void writeFooter(OutputStreamWriter osw, List borders, Column[] columns) + throws IOException { + String[] footers = + Arrays.stream(columns) + .map(Column::getFooter) + .filter(Objects::nonNull) + .toArray(String[]::new); + + HorizontalAlign[] dataAligns = + Arrays.stream(columns).map(Column::getFooterAlign).toArray(HorizontalAlign[]::new); + writeRow( + osw, + borders.get(4), + borders.get(5), + borders.get(6), + footers, + columns, + dataAligns, + System.lineSeparator()); + } + + /** + * 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, + HorizontalAlign[] dataAligns, + String lineSeparator) + throws IOException { + + int maxWidth; + HorizontalAlign dataAlign; + + if (left != null) { + osw.write(left); + } + + 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); } } - private int getOutputWidth(String s) { - int width = 0; - for (int i = 0; i < s.length(); i++) { - width += getCharWidth(s.charAt(i)); + if (right != null) { + osw.write(right); + } + + 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, HorizontalAlign align, int maxLength, int minPadding) + throws IOException { + + osw.write(LineUtil.getSpaces(minPadding)); + if (str.length() < maxLength) { + int leftPadding = + align == HorizontalAlign.LEFT + ? 0 + : align == 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)); + } + + public boolean isRowNumbersEnabled() { + return rowNumbersEnabled; + } + + /** + * 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 { + + public MetalakeTableFormat(OutputProperty property) { + super(property); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Metalake metalake) { + Column columnA = new Column("METALAKE", null, property); + Column columnB = new Column("COMMENT", null, property); + + columnA.addCell(metalake.name()); + columnB.addCell(metalake.comment()); + + return formatTable(columnA, columnB); + } + } + + /** + * Formats an array of Metalakes into a single-column table display. Lists all metalake names in a + * vertical format. + */ + static final class MetalakesTableFormat extends TableFormat { + + public MetalakesTableFormat(OutputProperty property) { + super(property); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Metalake[] metalakes) { + if (metalakes.length == 0) { + output("No metalakes exist.", System.err); + return null; + } else { + Column columnA = new Column("METALAKE", null, property); + Arrays.stream(metalakes).forEach(metalake -> columnA.addCell(metalake.name())); + + return formatTable(columnA); } + } + } + + /** + * 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 { + + public CatalogTableFormat(OutputProperty property) { + super(property); + } - return width; + /** {@inheritDoc} */ + @Override + public String getOutput(Catalog catalog) { + Column columnA = new Column("CATALOG", null, property); + Column columnB = new Column("TYPE", null, property); + Column columnC = new Column("provider", null, property); + Column columnD = new Column("comment", null, property); + + columnA.addCell(catalog.name()); + columnB.addCell(catalog.type().name()); + columnC.addCell(catalog.provider()); + columnD.addCell(catalog.comment()); + + return formatTable(columnA, columnB, columnC, columnD); + } + } + + /** + * Formats an array of Catalogs into a single-column table display. Lists all catalog names in a + * vertical format. + */ + static final class CatalogsTableFormat extends TableFormat { + + public CatalogsTableFormat(OutputProperty property) { + super(property); } - 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(Catalog[] catalogs) { + if (catalogs.length == 0) { + output("No metalakes exist.", System.err); + return null; + } else { + Column columnA = new Column("METALAKE", null, property); + Arrays.stream(catalogs).forEach(metalake -> columnA.addCell(metalake.name())); + + return formatTable(columnA); } + } + } + + /** + * Formats a single {@link Schema} instance into a two-column table display. Displays catalog + * details including name and comment information. + */ + static final class SchemaTableFormat extends TableFormat { + public SchemaTableFormat(OutputProperty property) { + super(property); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Schema schema) { + Column columnA = new Column("SCHEMA", null, property); + Column columnB = new Column("COMMENT", null, property); + + columnA.addCell(schema.name()); + columnB.addCell(schema.comment()); + + return formatTable(columnA, columnB); + } + } - return 1; + /** + * Formats an array of Schemas into a single-column table display. Lists all schema names in a + * vertical format. + */ + static final class SchemasTableFormat extends TableFormat { + public SchemasTableFormat(OutputProperty property) { + super(property); } - 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); + /** {@inheritDoc} */ + @Override + public String getOutput(Schema[] schemas) { + if (schemas.length == 0) { + output("No schemas exist.", System.err); + return null; + } else { + Column column = new Column("SCHEMA", null, property); + Arrays.stream(schemas).forEach(schema -> column.addCell(schema.name())); + + return formatTable(column); } } } diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/utils/LineUtil.java b/clients/cli/src/main/java/org/apache/gravitino/cli/utils/LineUtil.java new file mode 100644 index 00000000000..d69bb3754a8 --- /dev/null +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/utils/LineUtil.java @@ -0,0 +1,118 @@ +/* + * 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.utils; + +import java.util.Arrays; +import java.util.Objects; +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]"); + + /** + * Calculates the display width of a string, considering full-width characters. Full-width + * characters are counted as width 2, others as width 1. + * + * @param str Input string to measure + * @return Display width of the string, 0 if input is null + */ + public static int getDisplayWidth(String str) { + if (str == null) return 0; + int width = 0; + for (int i = 0; i < str.length(); i++) { + width += getCharWidth(str.charAt(i)); + } + + return width; + } + + /** + * Determines the display width of a single character. + * + * @param ch Character to measure + * @return 2 for full-width characters, 1 for others + */ + private static int getCharWidth(char ch) { + String s = String.valueOf(ch); + if (FULL_WIDTH_PATTERN.matcher(s).find()) { + return 2; + } + + return 1; + } + + /** + * Creates a string containing a specified number of spaces. + * + * @param n Number of spaces to generate + * @return String containing n spaces + * @throws IllegalArgumentException if n is negative + */ + public static String getSpaces(int n) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + sb.append(' '); + } + return sb.toString(); + } + + /** + * Adds an element to the beginning of an array. + * + * @param element Element to add at the start + * @param array Original array + * @param Type of array elements + * @return New array with element added at the beginning + * @throws NullPointerException if array is null + */ + public static T[] addFirst(T element, T[] array) { + Objects.requireNonNull(array, "Array must not be null"); + @SuppressWarnings("unchecked") + T[] newArray = Arrays.copyOf(array, array.length + 1); + System.arraycopy(array, 0, newArray, 1, array.length); + newArray[0] = element; + return newArray; + } + + /** + * Creates a new array filled with the specified string value. + * + * @param str String to fill the array with + * @param length Length of the array to create + * @return New array filled with the specified string + * @throws IllegalArgumentException if str is null or length is not positive + */ + public static String[] buildArrayWithFill(String str, int length) { + if (str == null) { + throw new IllegalArgumentException("str must not be null"); + } + if (length <= 0) { + throw new IllegalArgumentException("length must be greater than 0"); + } + + String[] result = new String[length]; + Arrays.fill(result, str); + return result; + } +} diff --git a/clients/cli/src/test/java/org/apache/gravitino/cli/TestCatalogCommands.java b/clients/cli/src/test/java/org/apache/gravitino/cli/TestCatalogCommands.java index afa19b94c5a..432c6958998 100644 --- a/clients/cli/src/test/java/org/apache/gravitino/cli/TestCatalogCommands.java +++ b/clients/cli/src/test/java/org/apache/gravitino/cli/TestCatalogCommands.java @@ -22,6 +22,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -39,7 +41,6 @@ import org.apache.gravitino.cli.commands.CatalogDetails; import org.apache.gravitino.cli.commands.CatalogDisable; import org.apache.gravitino.cli.commands.CatalogEnable; -import org.apache.gravitino.cli.commands.Command; import org.apache.gravitino.cli.commands.CreateCatalog; import org.apache.gravitino.cli.commands.DeleteCatalog; import org.apache.gravitino.cli.commands.ListCatalogProperties; @@ -48,6 +49,7 @@ import org.apache.gravitino.cli.commands.SetCatalogProperty; import org.apache.gravitino.cli.commands.UpdateCatalogComment; import org.apache.gravitino.cli.commands.UpdateCatalogName; +import org.apache.gravitino.cli.outputs.OutputProperty; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -91,7 +93,8 @@ void testListCatalogsCommand() { mockCommandLine, mockOptions, CommandEntities.CATALOG, CommandActions.LIST)); doReturn(mockList) .when(commandLine) - .newListCatalogs(GravitinoCommandLine.DEFAULT_URL, false, null, "metalake_demo"); + .newListCatalogs( + eq(GravitinoCommandLine.DEFAULT_URL), eq(false), any(), eq("metalake_demo")); doReturn(mockList).when(mockList).validate(); commandLine.handleCommandLine(); verify(mockList).handle(); @@ -112,7 +115,11 @@ void testCatalogDetailsCommand() { doReturn(mockDetails) .when(commandLine) .newCatalogDetails( - GravitinoCommandLine.DEFAULT_URL, false, null, "metalake_demo", "catalog"); + eq(GravitinoCommandLine.DEFAULT_URL), + eq(false), + any(), + eq("metalake_demo"), + eq("catalog")); doReturn(mockDetails).when(mockDetails).validate(); commandLine.handleCommandLine(); verify(mockDetails).handle(); @@ -431,7 +438,7 @@ void testCatalogDetailsCommandWithoutCatalog() { .newCatalogDetails( GravitinoCommandLine.DEFAULT_URL, false, - Command.OUTPUT_FORMAT_TABLE, + OutputProperty.defaultOutputProperty(), "metalake_demo", "catalog"); String output = new String(errContent.toByteArray(), StandardCharsets.UTF_8).trim(); diff --git a/clients/cli/src/test/java/org/apache/gravitino/cli/TestMetalakeCommands.java b/clients/cli/src/test/java/org/apache/gravitino/cli/TestMetalakeCommands.java index dae2fe63400..e16569e2753 100644 --- a/clients/cli/src/test/java/org/apache/gravitino/cli/TestMetalakeCommands.java +++ b/clients/cli/src/test/java/org/apache/gravitino/cli/TestMetalakeCommands.java @@ -22,6 +22,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -89,7 +91,7 @@ void testListMetalakesCommand() { mockCommandLine, mockOptions, CommandEntities.METALAKE, CommandActions.LIST)); doReturn(mockList) .when(commandLine) - .newListMetalakes(GravitinoCommandLine.DEFAULT_URL, false, null); + .newListMetalakes(eq(GravitinoCommandLine.DEFAULT_URL), eq(false), any()); doReturn(mockList).when(mockList).validate(); commandLine.handleCommandLine(); verify(mockList).handle(); @@ -107,7 +109,8 @@ void testMetalakeDetailsCommand() { mockCommandLine, mockOptions, CommandEntities.METALAKE, CommandActions.DETAILS)); doReturn(mockDetails) .when(commandLine) - .newMetalakeDetails(GravitinoCommandLine.DEFAULT_URL, false, null, "metalake_demo"); + .newMetalakeDetails( + eq(GravitinoCommandLine.DEFAULT_URL), eq(false), any(), eq("metalake_demo")); doReturn(mockDetails).when(mockDetails).validate(); commandLine.handleCommandLine(); verify(mockDetails).handle(); 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 f23d0284fb2..389971cbd76 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 @@ -18,6 +18,7 @@ */ package org.apache.gravitino.cli.integration.test; +import static org.apache.gravitino.cli.outputs.OutputProperty.OUTPUT_FORMAT_TABLE; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.ByteArrayOutputStream; @@ -25,7 +26,6 @@ import java.nio.charset.StandardCharsets; import org.apache.gravitino.cli.GravitinoOptions; import org.apache.gravitino.cli.Main; -import org.apache.gravitino.cli.commands.Command; import org.apache.gravitino.integration.test.util.BaseIT; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -94,7 +94,7 @@ public void testMetalakeListCommand() { "metalake", "list", commandArg(GravitinoOptions.OUTPUT), - Command.OUTPUT_FORMAT_TABLE, + OUTPUT_FORMAT_TABLE, commandArg(GravitinoOptions.URL), gravitinoUrl }; @@ -105,16 +105,17 @@ public void testMetalakeListCommand() { // Get the output and verify it String output = new String(outputStream.toByteArray(), StandardCharsets.UTF_8).trim(); assertEquals( - "+-------------+\n" - + "| metalake |\n" - + "+-------------+\n" - + "| my_metalake |\n" - + "+-------------+", + "+---+-------------+\n" + + "| | METALAKE |\n" + + "+---+-------------+\n" + + "| 1 | my_metalake |\n" + + "+---+-------------+", output); } @Test public void testMetalakeDetailsCommand() { + Main.useExit = false; // Create a byte array output stream to capture the output of the command ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PrintStream originalOut = System.out; @@ -126,7 +127,7 @@ public void testMetalakeDetailsCommand() { commandArg(GravitinoOptions.METALAKE), "my_metalake", commandArg(GravitinoOptions.OUTPUT), - Command.OUTPUT_FORMAT_TABLE, + OUTPUT_FORMAT_TABLE, commandArg(GravitinoOptions.URL), gravitinoUrl }; @@ -137,11 +138,11 @@ public void testMetalakeDetailsCommand() { // Get the output and verify it String output = new String(outputStream.toByteArray(), StandardCharsets.UTF_8).trim(); assertEquals( - "+-------------+-------------+\n" - + "| metalake | comment |\n" - + "+-------------+-------------+\n" - + "| my_metalake | my metalake |\n" - + "+-------------+-------------+", + "+---+-------------+-------------+\n" + + "| | METALAKE | COMMENT |\n" + + "+---+-------------+-------------+\n" + + "| 1 | my_metalake | my metalake |\n" + + "+---+-------------+-------------+", output); } @@ -158,7 +159,7 @@ public void testCatalogListCommand() { commandArg(GravitinoOptions.METALAKE), "my_metalake", commandArg(GravitinoOptions.OUTPUT), - Command.OUTPUT_FORMAT_TABLE, + OUTPUT_FORMAT_TABLE, commandArg(GravitinoOptions.URL), gravitinoUrl }; @@ -169,12 +170,12 @@ public void testCatalogListCommand() { // Get the output and verify it String output = new String(outputStream.toByteArray(), StandardCharsets.UTF_8).trim(); assertEquals( - "+-----------+\n" - + "| catalog |\n" - + "+-----------+\n" - + "| postgres |\n" - + "| postgres2 |\n" - + "+-----------+", + "+---+-----------+\n" + + "| | METALAKE |\n" + + "+---+-----------+\n" + + "| 1 | postgres |\n" + + "| 2 | postgres2 |\n" + + "+---+-----------+", output); } @@ -204,11 +205,11 @@ public void testCatalogDetailsCommand() { // Get the output and verify it String output = new String(outputStream.toByteArray(), StandardCharsets.UTF_8).trim(); assertEquals( - "+----------+------------+-----------------+---------+\n" - + "| catalog | type | provider | comment |\n" - + "+----------+------------+-----------------+---------+\n" - + "| postgres | RELATIONAL | jdbc-postgresql | null |\n" - + "+----------+------------+-----------------+---------+", + "+---+----------+------------+-----------------+---------+\n" + + "| | CATALOG | TYPE | PROVIDER | COMMENT |\n" + + "+---+----------+------------+-----------------+---------+\n" + + "| 1 | postgres | RELATIONAL | jdbc-postgresql | null |\n" + + "+---+----------+------------+-----------------+---------+", output); } @@ -236,11 +237,11 @@ public void testCatalogDetailsCommandFullCornerCharacter() { // Get the output and verify it String output = new String(outputStream.toByteArray(), StandardCharsets.UTF_8).trim(); assertEquals( - "+-----------+------------+-----------------+-------------------+\n" - + "| catalog | type | provider | comment |\n" - + "+-----------+------------+-----------------+-------------------+\n" - + "| postgres2 | RELATIONAL | jdbc-postgresql | catalog, 用于测试 |\n" - + "+-----------+------------+-----------------+-------------------+", + "+---+-----------+------------+-----------------+-------------------+\n" + + "| | CATALOG | TYPE | PROVIDER | COMMENT |\n" + + "+---+-----------+------------+-----------------+-------------------+\n" + + "| 1 | postgres2 | RELATIONAL | jdbc-postgresql | catalog, 用于测试 |\n" + + "+---+-----------+------------+-----------------+-------------------+", output); } diff --git a/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestColumn.java b/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestColumn.java new file mode 100644 index 00000000000..cd47c3efdb4 --- /dev/null +++ b/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestColumn.java @@ -0,0 +1,35 @@ +/* + * 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 org.apache.gravitino.cli.outputs.Column; +import org.apache.gravitino.cli.outputs.OutputProperty; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TestColumn { + + @Test + void testCreateColumn() { + Column column = new Column("METALAKE", "Footer", OutputProperty.defaultOutputProperty()); + column.addCell("cell1").addCell("cell2").addCell("cell3"); + Assertions.assertEquals(8, column.getMaxWidth()); + } +} 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 00000000000..006a8c2b91e --- /dev/null +++ b/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestPlainFormat.java @@ -0,0 +1,212 @@ +/* + * 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.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.apache.gravitino.Catalog; +import org.apache.gravitino.Metalake; +import org.apache.gravitino.Schema; +import org.apache.gravitino.cli.outputs.OutputProperty; +import org.apache.gravitino.cli.outputs.PlainFormat; +import org.apache.gravitino.file.Fileset; +import org.apache.gravitino.rel.Column; +import org.apache.gravitino.rel.Table; +import org.apache.gravitino.rel.types.Type; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.com.google.common.base.Joiner; + +public class TestPlainFormat { + public static final Joiner COMMA_JOINER = Joiner.on(", ").skipNulls(); + com.google.common.base.Joiner NEWLINE_JOINER = + com.google.common.base.Joiner.on(System.lineSeparator()).skipNulls(); + 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 testUnsupportType() { + Fileset mockFileset = mock(Fileset.class); + OutputProperty property = OutputProperty.defaultOutputProperty(); + Assertions.assertThrows( + IllegalArgumentException.class, () -> PlainFormat.output(mockFileset, property)); + } + + @Test + void testMetalakePlainFormat() { + Metalake mockMetalake = mock(Metalake.class); + when(mockMetalake.name()).thenReturn("demo_metalake"); + when(mockMetalake.comment()).thenReturn("metalake comment"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + PlainFormat.output(mockMetalake, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + COMMA_JOINER.join(Arrays.asList("demo_metalake", "metalake comment")), output); + } + + @Test + void testMetalakesPlainFormat() { + Metalake mockMetalake1 = mock(Metalake.class); + Metalake mockMetalake2 = mock(Metalake.class); + + when(mockMetalake1.name()).thenReturn("demo_metalake1"); + when(mockMetalake2.name()).thenReturn("demo_metalake2"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + PlainFormat.output(new Metalake[] {mockMetalake1, mockMetalake2}, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + NEWLINE_JOINER.join(Arrays.asList("demo_metalake1", "demo_metalake2")), output); + } + + @Test + void testCatalogPlainFormat() { + Catalog mockCatalog = mock(Catalog.class); + when(mockCatalog.name()).thenReturn("demo_catalog"); + when(mockCatalog.type()).thenReturn(Catalog.Type.RELATIONAL); + when(mockCatalog.provider()).thenReturn("demo_provider"); + when(mockCatalog.comment()).thenReturn("catalog comment"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + PlainFormat.output(mockCatalog, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + COMMA_JOINER.join( + Arrays.asList("demo_catalog", "RELATIONAL", "demo_provider", "catalog comment")), + output); + } + + @Test + void testCatalogsPlainFormat() { + Catalog mockCatalog1 = mock(Catalog.class); + Catalog mockCatalog2 = mock(Catalog.class); + + when(mockCatalog1.name()).thenReturn("demo_catalog1"); + when(mockCatalog2.name()).thenReturn("demo_catalog2"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + PlainFormat.output(new Catalog[] {mockCatalog1, mockCatalog2}, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + NEWLINE_JOINER.join(Arrays.asList("demo_catalog1", "demo_catalog2")), output); + } + + @Test + void testSchemaPlainFormat() { + Schema mmockSchema = mock(Schema.class); + when(mmockSchema.name()).thenReturn("demo_schema"); + when(mmockSchema.comment()).thenReturn("schema comment"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + PlainFormat.output(mmockSchema, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + COMMA_JOINER.join(Arrays.asList("demo_schema", "schema comment")), output); + } + + @Test + void testSchemasPlainFormat() { + Schema mockSchema1 = mock(Schema.class); + Schema mockSchema2 = mock(Schema.class); + + when(mockSchema1.name()).thenReturn("demo_schema1"); + when(mockSchema2.name()).thenReturn("demo_schema2"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + PlainFormat.output(new Schema[] {mockSchema1, mockSchema2}, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + NEWLINE_JOINER.join(Arrays.asList("demo_schema1", "demo_schema2")), output); + } + + @Test + void testTablePlainFormat() { + Table mockTable = mock(Table.class); + Column mockColumn1 = mock(Column.class); + Column mockColumn2 = mock(Column.class); + Type mockType1 = mock(Type.class); + Type mockType2 = mock(Type.class); + when(mockType1.simpleString()).thenReturn("int"); + when(mockType2.simpleString()).thenReturn("string"); + when(mockColumn1.name()).thenReturn("demo_column1"); + when(mockColumn2.name()).thenReturn("demo_column2"); + when(mockColumn1.dataType()).thenReturn(mockType1); + when(mockColumn2.dataType()).thenReturn(mockType2); + when(mockColumn1.comment()).thenReturn("column1 comment"); + when(mockColumn2.comment()).thenReturn("column2 comment"); + + when(mockTable.name()).thenReturn("demo_table"); + when(mockTable.columns()).thenReturn(new Column[] {mockColumn1, mockColumn2}); + when(mockTable.comment()).thenReturn("table comment"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + PlainFormat.output(mockTable, property); + + StringBuilder sb = new StringBuilder(); + sb.append("demo_table\n"); + + sb.append( + NEWLINE_JOINER.join( + Arrays.asList( + COMMA_JOINER.join(Arrays.asList("demo_column1", "int", "column1 comment")), + COMMA_JOINER.join(Arrays.asList("demo_column2", "string", "column2 comment"))))); + sb.append("\n"); + sb.append("table comment"); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals(sb.toString(), output); + } + + @Test + void testTablesPlainFormat() { + Table mockSchema1 = mock(Table.class); + Table mockSchema2 = mock(Table.class); + + when(mockSchema1.name()).thenReturn("demo_table1"); + when(mockSchema2.name()).thenReturn("demo_table2"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + PlainFormat.output(new Table[] {mockSchema1, mockSchema2}, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + NEWLINE_JOINER.join(Arrays.asList("demo_table1", "demo_table2")), output); + } +} 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 00000000000..d08183d8978 --- /dev/null +++ b/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestTableFormat.java @@ -0,0 +1,444 @@ +/* + * 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 org.apache.gravitino.cli.outputs.Column; +import org.apache.gravitino.cli.outputs.HorizontalAlign; +import org.apache.gravitino.cli.outputs.OutputProperty; +import org.apache.gravitino.cli.outputs.Style; +import org.apache.gravitino.cli.outputs.TableFormat; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TestTableFormat { + @Test + void testTableOutputWithFooter() { + + Column columnA = new Column("METALAKE", "FooterA", OutputProperty.defaultOutputProperty()); + Column columnB = new Column("comment", "FooterB", OutputProperty.defaultOutputProperty()); + + columnA.addCell("cell1").addCell("cell2").addCell("cell3"); + columnB.addCell("cell4").addCell("cell5").addCell("cell6"); + + TableFormat tableFormat = + new TableFormat(OutputProperty.defaultOutputProperty()) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(columnA, columnB).trim(); + Assertions.assertEquals( + "+---+----------+---------+\n" + + "| | METALAKE | COMMENT |\n" + + "+---+----------+---------+\n" + + "| 1 | cell1 | cell4 |\n" + + "| 2 | cell2 | cell5 |\n" + + "| 3 | cell3 | cell6 |\n" + + "+---+----------+---------+\n" + + "| | FooterA | FooterB |\n" + + "+---+----------+---------+", + outputString); + } + + @Test + void testTableOutputWithoutFooter() { + Column columnA = new Column("METALAKE", null, OutputProperty.defaultOutputProperty()); + Column columnB = new Column("comment", null, OutputProperty.defaultOutputProperty()); + + columnA.addCell("cell1").addCell("cell2").addCell("cell3"); + columnB.addCell("cell4").addCell("cell5").addCell("cell6"); + + TableFormat tableFormat = + new TableFormat(OutputProperty.defaultOutputProperty()) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(columnA, columnB).trim(); + Assertions.assertEquals( + "+---+----------+---------+\n" + + "| | METALAKE | COMMENT |\n" + + "+---+----------+---------+\n" + + "| 1 | cell1 | cell4 |\n" + + "| 2 | cell2 | cell5 |\n" + + "| 3 | cell3 | cell6 |\n" + + "+---+----------+---------+", + outputString); + } + + @Test + void testTableOutputWithoutRowNumber() { + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setRowNumbersEnabled(false); + Column columnA = new Column("METALAKE", null, property); + Column columnB = new Column("comment", null, property); + + columnA.addCell("cell1").addCell("cell2").addCell("cell3"); + columnB.addCell("cell4").addCell("cell5").addCell("cell6"); + + TableFormat tableFormat = + new TableFormat(property) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(columnA, columnB).trim(); + Assertions.assertEquals( + "+----------+---------+\n" + + "| METALAKE | COMMENT |\n" + + "+----------+---------+\n" + + "| cell1 | cell4 |\n" + + "| cell2 | cell5 |\n" + + "| cell3 | cell6 |\n" + + "+----------+---------+", + outputString); + } + + @Test + void testTableOutputWithFancyStyle() { + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setStyle(Style.FANCY); + + Column columnA = new Column("METALAKE", null, property); + Column columnB = new Column("comment", null, property); + + columnA.addCell("cell1").addCell("cell2").addCell("cell3"); + columnB.addCell("cell4").addCell("cell5").addCell("cell6"); + + TableFormat tableFormat = + new TableFormat(property) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(columnA, columnB).trim(); + Assertions.assertEquals( + "╔═══╤══════════╤═════════╗\n" + + "║ │ METALAKE │ COMMENT ║\n" + + "╠═══╪══════════╪═════════╣\n" + + "║ 1 │ cell1 │ cell4 ║\n" + + "║ 2 │ cell2 │ cell5 ║\n" + + "║ 3 │ cell3 │ cell6 ║\n" + + "╚═══╧══════════╧═════════╝", + outputString); + } + + @Test + void testTableOutputWithFancy2Style() { + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setStyle(Style.FANCY2); + property.setRowNumbersEnabled(false); + + Column columnA = new Column("METALAKE", null, property); + Column columnB = new Column("comment", null, property); + + columnA.addCell("cell1").addCell("cell2").addCell("cell3"); + columnB.addCell("cell4").addCell("cell5").addCell("cell6"); + + TableFormat tableFormat = + new TableFormat(property) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(columnA, columnB).trim(); + Assertions.assertEquals( + "╔══════════╤═════════╗\n" + + "║ METALAKE │ COMMENT ║\n" + + "╠══════════╪═════════╣\n" + + "║ cell1 │ cell4 ║\n" + + "╟──────────┼─────────╢\n" + + "║ cell2 │ cell5 ║\n" + + "╟──────────┼─────────╢\n" + + "║ cell3 │ cell6 ║\n" + + "╚══════════╧═════════╝", + outputString); + } + + @Test + void testTableOutputWithBasic2Style() { + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setStyle(Style.BASIC2); + property.setRowNumbersEnabled(false); + + Column columnA = new Column("METALAKE", null, property); + Column columnB = new Column("comment", null, property); + + columnA.addCell("cell1").addCell("cell2").addCell("cell3"); + columnB.addCell("cell4").addCell("cell5").addCell("cell6"); + + TableFormat tableFormat = + new TableFormat(property) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(columnA, columnB).trim(); + Assertions.assertEquals( + "+----------+---------+\n" + + "| METALAKE | COMMENT |\n" + + "+----------+---------+\n" + + "| cell1 | cell4 |\n" + + "| cell2 | cell5 |\n" + + "| cell3 | cell6 |\n" + + "+----------+---------+", + outputString); + } + + @Test + void testTableOutputWithLimit() { + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setStyle(Style.BASIC2); + property.setLimit(5); + + Column columnA = new Column("METALAKE", null, property); + Column columnB = new Column("comment", null, property); + + addRepeatedCells(columnA, 10); + addRepeatedCells(columnB, 10); + + TableFormat tableFormat = + new TableFormat(property) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(columnA, columnB).trim(); + Assertions.assertEquals( + "+---+------------+-----------+\n" + + "| | METALAKE | COMMENT |\n" + + "+---+------------+-----------+\n" + + "| 1 | METALAKE-1 | COMMENT-1 |\n" + + "| 2 | METALAKE-2 | COMMENT-2 |\n" + + "| 3 | METALAKE-3 | COMMENT-3 |\n" + + "| 4 | METALAKE-4 | COMMENT-4 |\n" + + "| 5 | METALAKE-5 | COMMENT-5 |\n" + + "| 6 | … | … |\n" + + "+---+------------+-----------+", + outputString); + } + + @Test + void testTableOutputWithHeaderLeftAlignment() { + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setStyle(Style.BASIC2); + property.setRowNumbersEnabled(false); + property.setHeaderAlign(HorizontalAlign.LEFT); + + Column columnA = new Column("METALAKE", null, property); + Column columnB = new Column("comment", null, property); + + columnA.addCell("cell1").addCell("cell2").addCell("cell3").addCell("very long cell"); + columnB.addCell("cell4").addCell("cell5").addCell("cell6").addCell("very long cell"); + + TableFormat tableFormat = + new TableFormat(property) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(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 testTableOutputWithHeaderRightAlignment() { + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setStyle(Style.BASIC2); + property.setRowNumbersEnabled(false); + property.setHeaderAlign(HorizontalAlign.RIGHT); + + Column columnA = new Column("METALAKE", null, property); + Column columnB = new Column("comment", null, property); + + columnA.addCell("cell1").addCell("cell2").addCell("cell3").addCell("very long cell"); + columnB.addCell("cell4").addCell("cell5").addCell("cell6").addCell("very long cell"); + + TableFormat tableFormat = + new TableFormat(property) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(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 testTableOutputWithDataCenterAlignment() { + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setStyle(Style.BASIC2); + property.setRowNumbersEnabled(false); + property.setDataAlign(HorizontalAlign.CENTER); + + Column columnA = new Column("METALAKE", null, property); + Column columnB = new Column("comment", null, property); + + columnA.addCell("cell1").addCell("cell2").addCell("cell3").addCell("very long cell"); + columnB.addCell("cell4").addCell("cell5").addCell("cell6").addCell("very long cell"); + + TableFormat tableFormat = + new TableFormat(property) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(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 testTableOutputWithDataRightAlignment() { + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setStyle(Style.BASIC2); + property.setRowNumbersEnabled(false); + property.setDataAlign(HorizontalAlign.RIGHT); + + Column columnA = new Column("METALAKE", null, property); + Column columnB = new Column("comment", null, property); + + columnA.addCell("cell1").addCell("cell2").addCell("cell3").addCell("very long cell"); + columnB.addCell("cell4").addCell("cell5").addCell("cell6").addCell("very long cell"); + + TableFormat tableFormat = + new TableFormat(property) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(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 testTableOutputWithEmptyColumn() { + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setStyle(Style.FANCY); + property.setRowNumbersEnabled(false); + + Column columnA = new Column("METALAKE", null, property); + Column columnB = new Column("comment", null, property); + + TableFormat tableFormat = + new TableFormat(property) { + @Override + public String getOutput(String entity) { + return null; + } + }; + + String outputString = tableFormat.formatTable(columnA, columnB).trim(); + + Assertions.assertEquals( + "╔══════════╤═════════╗\n" + + "║ METALAKE │ COMMENT ║\n" + + "╠══════════╪═════════╣\n" + + "╚══════════╧═════════╝", + outputString); + } + + @Test + void testMetalakeTableOutput() {} + + @Test + void testMetalakesTableOutput() {} + + @Test + void testCatalogTableOutput() {} + + @Test + void testCatalogsTableOutput() {} + + @Test + void testSchemaTableOutput() {} + + @Test + void testSchemasTableOutput() {} + + @Test + void testTableTableOutput() {} + + @Test + void testTablesTableOutput() {} + + private void addRepeatedCells(Column column, int count) { + for (int i = 0; i < count; i++) { + column.addCell(column.getHeader() + "-" + (i + 1)); + } + } +} From 36b07158707d154de6cea3d4b33d59e6ad15f4e3 Mon Sep 17 00:00:00 2001 From: pancx Date: Sun, 19 Jan 2025 12:42:19 +0800 Subject: [PATCH 2/4] [#6326] improve(CLI): Make CLI more extendable and maintainable. Make CLI more extendable and maintainable. --- .../gravitino/cli/GravitinoCommandLine.java | 3 +- .../gravitino/cli/commands/Command.java | 1 - .../outputs/{Style.java => BorderStyle.java} | 6 +- .../gravitino/cli/outputs/OutputProperty.java | 20 +- .../gravitino/cli/outputs/TableFormat.java | 40 ++-- .../gravitino/cli/output/TestPlainFormat.java | 3 +- .../gravitino/cli/output/TestTableFormat.java | 192 +++++++++++++++--- 7 files changed, 195 insertions(+), 70 deletions(-) rename clients/cli/src/main/java/org/apache/gravitino/cli/outputs/{Style.java => BorderStyle.java} (93%) diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java index 6020bb589a6..84c7fafdbec 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java @@ -27,7 +27,6 @@ import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; -import org.apache.gravitino.cli.outputs.OutputProperty; /* Gravitino Command line */ public class GravitinoCommandLine extends TestableCommandLine { @@ -108,7 +107,7 @@ public static void displayHelp(Options options) { /** Executes the appropriate command based on the command type. */ private void executeCommand() { - OutputProperty outputProperty = OutputProperty.fromLine(line); + if (CommandActions.HELP.equals(command)) { handleHelpCommand(); } else if (line.hasOption(GravitinoOptions.OWNER)) { 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 0a501ceddbc..62bf5193c8f 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 @@ -26,7 +26,6 @@ import com.google.common.base.Joiner; import java.io.File; import java.io.OutputStream; - import org.apache.gravitino.cli.ErrorMessages; import org.apache.gravitino.cli.GravitinoConfig; import org.apache.gravitino.cli.KerberosData; diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Style.java b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/BorderStyle.java similarity index 93% rename from clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Style.java rename to clients/cli/src/main/java/org/apache/gravitino/cli/outputs/BorderStyle.java index 16989f0a866..d03d9d2d065 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/Style.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/BorderStyle.java @@ -29,7 +29,7 @@ * Defines different styles for formatting and displaying data. Each style contains a specific set * of characters for rendering and configuration for whether to show boundaries between data rows. */ -public enum Style { +public enum BorderStyle { FANCY(FANCY_ASCII, false), FANCY2(FANCY_ASCII, true), BASIC(BASIC_ASCII, true), @@ -44,13 +44,13 @@ public enum Style { private final boolean showRowBoundaries; /** - * Constructs a {@link Style} instance. + * Constructs a {@link BorderStyle} instance. * * @param characters the list of characters to use for the style * @param showRowBoundaries {@code true} to show boundaries between data rows, {@code false} * otherwise */ - Style(ImmutableList characters, boolean showRowBoundaries) { + BorderStyle(ImmutableList characters, boolean showRowBoundaries) { this.characters = characters; this.showRowBoundaries = showRowBoundaries; } diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputProperty.java b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputProperty.java index d4a003d486f..e7e1782691b 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputProperty.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputProperty.java @@ -37,7 +37,7 @@ public class OutputProperty { false, false, -1, - Style.BASIC2, + BorderStyle.BASIC2, HorizontalAlign.CENTER, HorizontalAlign.LEFT, HorizontalAlign.CENTER, @@ -48,7 +48,7 @@ public class OutputProperty { private boolean sort; private boolean quiet; private int limit; - private Style style; + private BorderStyle borderStyle; private HorizontalAlign headerAlign; private HorizontalAlign dataAlign; private final HorizontalAlign footerAlign; @@ -62,7 +62,7 @@ public class OutputProperty { * @param sort Whether to sort the output. * @param quiet Whether to suppress output. * @param limit Maximum number of rows (-1 for unlimited). - * @param style Border style for tables. + * @param borderStyle Border style for tables. * @param headerAlign Header text alignment. * @param dataAlign Data cell alignment. * @param footerAlign Footer text alignment. @@ -74,7 +74,7 @@ public OutputProperty( boolean sort, boolean quiet, int limit, - Style style, + BorderStyle borderStyle, HorizontalAlign headerAlign, HorizontalAlign dataAlign, HorizontalAlign footerAlign, @@ -84,7 +84,7 @@ public OutputProperty( this.sort = sort; this.quiet = quiet; this.limit = limit; - this.style = style; + this.borderStyle = borderStyle; this.headerAlign = headerAlign; this.dataAlign = dataAlign; this.footerAlign = footerAlign; @@ -130,8 +130,8 @@ public int getLimit() { return limit; } - public Style getStyle() { - return style; + public BorderStyle getStyle() { + return borderStyle; } public HorizontalAlign getHeaderAlign() { @@ -174,8 +174,8 @@ public void setLimit(int limit) { this.limit = limit; } - public void setStyle(Style style) { - this.style = style; + public void setStyle(BorderStyle borderStyle) { + this.borderStyle = borderStyle; } public void setRowNumbersEnabled(boolean rowNumbersEnabled) { @@ -200,7 +200,7 @@ public OutputProperty copy() { sort, quiet, limit, - style, + borderStyle, headerAlign, dataAlign, footerAlign, 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 4ec7f649e56..25ee3ad053a 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 @@ -61,7 +61,7 @@ public abstract class TableFormat extends BaseOutputFormat { public static final int PADDING = 1; - private final Style style; + private final BorderStyle borderStyle; private final boolean rowNumbersEnabled; protected final OutputProperty property; @@ -99,7 +99,7 @@ public static void output(Object entity, OutputProperty property) { public TableFormat(OutputProperty property) { super(property.isQuiet(), property.getLimit(), property.isSort()); this.property = property; - this.style = property.getStyle(); + this.borderStyle = property.getStyle(); this.rowNumbersEnabled = property.isRowNumbersEnabled(); } @@ -109,7 +109,7 @@ public TableFormat(OutputProperty property) { * @param columns the columns to print. * @return the table formatted output string. */ - public String formatTable(Column... columns) { + public String getTableFormat(Column... columns) { checkColumns(columns); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -125,7 +125,7 @@ public String formatTable(Column... columns) { .filter(StringUtils::isNotBlank) .toArray(String[]::new); - List borders = style.getCharacters(); + List borders = borderStyle.getCharacters(); checkHeaders(headers, columns); if (headers.length != columns.length) { @@ -149,10 +149,10 @@ public String formatTable(Column... columns) { writeUpperBorder(osw, borders, System.lineSeparator(), columns); writeHeader(osw, borders, System.lineSeparator(), columns); writeHeaderBorder(osw, borders, System.lineSeparator(), columns); - writeData(osw, style, columns, System.lineSeparator()); + writeData(osw, borderStyle, columns, System.lineSeparator()); if (footers.length > 0) { - writeRowSeparator(osw, style, System.lineSeparator(), columns); + writeRowSeparator(osw, borderStyle, System.lineSeparator(), columns); writeFooter(osw, borders, columns); } @@ -266,15 +266,15 @@ private static void writeHeaderBorder( * Writes the separator line between data rows. * * @param writer the writer for output - * @param style the table style containing border characters + * @param borderStyle the table style containing border characters * @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, Style style, String lineSeparator, Column[] columns) + OutputStreamWriter writer, BorderStyle borderStyle, String lineSeparator, Column[] columns) throws IOException { - List borders = style.getCharacters(); + List borders = borderStyle.getCharacters(); writeHorizontalLine( writer, borders.get(DATA_ROW_BORDER_LEFT_IDX), @@ -319,15 +319,15 @@ private static void writeBottomBorder( * * * @param writer the writer for output - * @param style the table style containing border characters and row boundary settings + * @param borderStyle the table style containing border characters and row boundary settings * @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, Style style, Column[] columns, String lineSeparator) + OutputStreamWriter writer, BorderStyle borderStyle, Column[] columns, String lineSeparator) throws IOException { - List borders = style.getCharacters(); + List borders = borderStyle.getCharacters(); int dataSize = columns[0].getCellCount(); HorizontalAlign[] dataAligns = Arrays.stream(columns).map(Column::getDataAlign).toArray(HorizontalAlign[]::new); @@ -344,8 +344,8 @@ private void writeData( dataAligns, lineSeparator); - if (i < dataSize - 1 && style.isRowBoundariesEnabled()) { - writeRowSeparator(writer, style, lineSeparator, columns); + if (i < dataSize - 1 && borderStyle.isRowBoundariesEnabled()) { + writeRowSeparator(writer, borderStyle, lineSeparator, columns); } } } @@ -594,7 +594,7 @@ public String getOutput(Metalake metalake) { columnA.addCell(metalake.name()); columnB.addCell(metalake.comment()); - return formatTable(columnA, columnB); + return getTableFormat(columnA, columnB); } } @@ -618,7 +618,7 @@ public String getOutput(Metalake[] metalakes) { Column columnA = new Column("METALAKE", null, property); Arrays.stream(metalakes).forEach(metalake -> columnA.addCell(metalake.name())); - return formatTable(columnA); + return getTableFormat(columnA); } } } @@ -646,7 +646,7 @@ public String getOutput(Catalog catalog) { columnC.addCell(catalog.provider()); columnD.addCell(catalog.comment()); - return formatTable(columnA, columnB, columnC, columnD); + return getTableFormat(columnA, columnB, columnC, columnD); } } @@ -670,7 +670,7 @@ public String getOutput(Catalog[] catalogs) { Column columnA = new Column("METALAKE", null, property); Arrays.stream(catalogs).forEach(metalake -> columnA.addCell(metalake.name())); - return formatTable(columnA); + return getTableFormat(columnA); } } } @@ -693,7 +693,7 @@ public String getOutput(Schema schema) { columnA.addCell(schema.name()); columnB.addCell(schema.comment()); - return formatTable(columnA, columnB); + return getTableFormat(columnA, columnB); } } @@ -716,7 +716,7 @@ public String getOutput(Schema[] schemas) { Column column = new Column("SCHEMA", null, property); Arrays.stream(schemas).forEach(schema -> column.addCell(schema.name())); - return formatTable(column); + return getTableFormat(column); } } } 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 index 006a8c2b91e..2671b8cb2a8 100644 --- 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 @@ -43,8 +43,7 @@ public class TestPlainFormat { public static final Joiner COMMA_JOINER = Joiner.on(", ").skipNulls(); - com.google.common.base.Joiner NEWLINE_JOINER = - com.google.common.base.Joiner.on(System.lineSeparator()).skipNulls(); + public static final Joiner NEWLINE_JOINER = Joiner.on(System.lineSeparator()).skipNulls(); private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); private final PrintStream originalOut = System.out; 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 index d08183d8978..19a608e8ca6 100644 --- 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 @@ -19,15 +19,43 @@ package org.apache.gravitino.cli.output; +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.Schema; +import org.apache.gravitino.cli.outputs.BorderStyle; import org.apache.gravitino.cli.outputs.Column; import org.apache.gravitino.cli.outputs.HorizontalAlign; import org.apache.gravitino.cli.outputs.OutputProperty; -import org.apache.gravitino.cli.outputs.Style; 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 testTableOutputWithFooter() { @@ -45,7 +73,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "+---+----------+---------+\n" + "| | METALAKE | COMMENT |\n" @@ -75,7 +103,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "+---+----------+---------+\n" + "| | METALAKE | COMMENT |\n" @@ -105,7 +133,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "+----------+---------+\n" + "| METALAKE | COMMENT |\n" @@ -120,7 +148,7 @@ public String getOutput(String entity) { @Test void testTableOutputWithFancyStyle() { OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setStyle(Style.FANCY); + property.setStyle(BorderStyle.FANCY); Column columnA = new Column("METALAKE", null, property); Column columnB = new Column("comment", null, property); @@ -136,7 +164,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "╔═══╤══════════╤═════════╗\n" + "║ │ METALAKE │ COMMENT ║\n" @@ -151,7 +179,7 @@ public String getOutput(String entity) { @Test void testTableOutputWithFancy2Style() { OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setStyle(Style.FANCY2); + property.setStyle(BorderStyle.FANCY2); property.setRowNumbersEnabled(false); Column columnA = new Column("METALAKE", null, property); @@ -168,7 +196,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "╔══════════╤═════════╗\n" + "║ METALAKE │ COMMENT ║\n" @@ -185,7 +213,7 @@ public String getOutput(String entity) { @Test void testTableOutputWithBasic2Style() { OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setStyle(Style.BASIC2); + property.setStyle(BorderStyle.BASIC2); property.setRowNumbersEnabled(false); Column columnA = new Column("METALAKE", null, property); @@ -202,7 +230,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "+----------+---------+\n" + "| METALAKE | COMMENT |\n" @@ -217,7 +245,7 @@ public String getOutput(String entity) { @Test void testTableOutputWithLimit() { OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setStyle(Style.BASIC2); + property.setStyle(BorderStyle.BASIC2); property.setLimit(5); Column columnA = new Column("METALAKE", null, property); @@ -234,7 +262,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "+---+------------+-----------+\n" + "| | METALAKE | COMMENT |\n" @@ -252,7 +280,7 @@ public String getOutput(String entity) { @Test void testTableOutputWithHeaderLeftAlignment() { OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setStyle(Style.BASIC2); + property.setStyle(BorderStyle.BASIC2); property.setRowNumbersEnabled(false); property.setHeaderAlign(HorizontalAlign.LEFT); @@ -270,7 +298,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "+----------------+----------------+\n" + "| METALAKE | COMMENT |\n" @@ -286,7 +314,7 @@ public String getOutput(String entity) { @Test void testTableOutputWithHeaderRightAlignment() { OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setStyle(Style.BASIC2); + property.setStyle(BorderStyle.BASIC2); property.setRowNumbersEnabled(false); property.setHeaderAlign(HorizontalAlign.RIGHT); @@ -304,7 +332,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "+----------------+----------------+\n" + "| METALAKE | COMMENT |\n" @@ -320,7 +348,7 @@ public String getOutput(String entity) { @Test void testTableOutputWithDataCenterAlignment() { OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setStyle(Style.BASIC2); + property.setStyle(BorderStyle.BASIC2); property.setRowNumbersEnabled(false); property.setDataAlign(HorizontalAlign.CENTER); @@ -338,7 +366,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "+----------------+----------------+\n" + "| METALAKE | COMMENT |\n" @@ -354,7 +382,7 @@ public String getOutput(String entity) { @Test void testTableOutputWithDataRightAlignment() { OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setStyle(Style.BASIC2); + property.setStyle(BorderStyle.BASIC2); property.setRowNumbersEnabled(false); property.setDataAlign(HorizontalAlign.RIGHT); @@ -372,7 +400,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "+----------------+----------------+\n" + "| METALAKE | COMMENT |\n" @@ -388,7 +416,7 @@ public String getOutput(String entity) { @Test void testTableOutputWithEmptyColumn() { OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setStyle(Style.FANCY); + property.setStyle(BorderStyle.FANCY); property.setRowNumbersEnabled(false); Column columnA = new Column("METALAKE", null, property); @@ -402,7 +430,7 @@ public String getOutput(String entity) { } }; - String outputString = tableFormat.formatTable(columnA, columnB).trim(); + String outputString = tableFormat.getTableFormat(columnA, columnB).trim(); Assertions.assertEquals( "╔══════════╤═════════╗\n" @@ -413,28 +441,128 @@ public String getOutput(String entity) { } @Test - void testMetalakeTableOutput() {} + void testMetalakeTableOutput() { + Metalake mockMetalake = mock(Metalake.class); + when(mockMetalake.name()).thenReturn("demo_metalake"); + when(mockMetalake.comment()).thenReturn("metalake comment"); - @Test - void testMetalakesTableOutput() {} + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setRowNumbersEnabled(false); + TableFormat.output(mockMetalake, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + "+---------------+------------------+\n" + + "| METALAKE | COMMENT |\n" + + "+---------------+------------------+\n" + + "| demo_metalake | metalake comment |\n" + + "+---------------+------------------+", + output); + } @Test - void testCatalogTableOutput() {} + void testMetalakesTableOutput() { + Metalake mockMetalake1 = mock(Metalake.class); + Metalake mockMetalake2 = mock(Metalake.class); - @Test - void testCatalogsTableOutput() {} + when(mockMetalake1.name()).thenReturn("demo_metalake1"); + when(mockMetalake2.name()).thenReturn("demo_metalake2"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setRowNumbersEnabled(false); + TableFormat.output(new Metalake[] {mockMetalake1, mockMetalake2}, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + "+----------------+\n" + + "| METALAKE |\n" + + "+----------------+\n" + + "| demo_metalake1 |\n" + + "| demo_metalake2 |\n" + + "+----------------+", + output); + } @Test - void testSchemaTableOutput() {} + void testCatalogTableOutput() { + Catalog mockCatalog = mock(Catalog.class); + when(mockCatalog.name()).thenReturn("demo_catalog"); + when(mockCatalog.type()).thenReturn(Catalog.Type.RELATIONAL); + when(mockCatalog.provider()).thenReturn("demo_provider"); + when(mockCatalog.comment()).thenReturn("catalog comment"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + TableFormat.output(mockCatalog, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + "+---+--------------+------------+---------------+-----------------+\n" + + "| | CATALOG | TYPE | PROVIDER | COMMENT |\n" + + "+---+--------------+------------+---------------+-----------------+\n" + + "| 1 | demo_catalog | RELATIONAL | demo_provider | catalog comment |\n" + + "+---+--------------+------------+---------------+-----------------+", + output); + } @Test - void testSchemasTableOutput() {} + void testCatalogsTableOutput() { + Catalog mockCatalog1 = mock(Catalog.class); + Catalog mockCatalog2 = mock(Catalog.class); + + when(mockCatalog1.name()).thenReturn("demo_catalog1"); + when(mockCatalog2.name()).thenReturn("demo_catalog2"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setRowNumbersEnabled(false); + TableFormat.output(new Catalog[] {mockCatalog1, mockCatalog2}, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + "+---------------+\n" + + "| METALAKE |\n" + + "+---------------+\n" + + "| demo_catalog1 |\n" + + "| demo_catalog2 |\n" + + "+---------------+", + output); + } @Test - void testTableTableOutput() {} + void testSchemaTableOutput() { + Schema mmockSchema = mock(Schema.class); + when(mmockSchema.name()).thenReturn("demo_schema"); + when(mmockSchema.comment()).thenReturn("schema comment"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setRowNumbersEnabled(false); + TableFormat.output(mmockSchema, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + "+-------------+----------------+\n" + + "| SCHEMA | COMMENT |\n" + + "+-------------+----------------+\n" + + "| demo_schema | schema comment |\n" + + "+-------------+----------------+", + output); + } @Test - void testTablesTableOutput() {} + void testSchemasTableOutput() { + Schema mockSchema1 = mock(Schema.class); + Schema mockSchema2 = mock(Schema.class); + + when(mockSchema1.name()).thenReturn("demo_schema1"); + when(mockSchema2.name()).thenReturn("demo_schema2"); + + OutputProperty property = OutputProperty.defaultOutputProperty(); + property.setRowNumbersEnabled(false); + TableFormat.output(new Schema[] {mockSchema1, mockSchema2}, property); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + "+--------------+\n" + + "| SCHEMA |\n" + + "+--------------+\n" + + "| demo_schema1 |\n" + + "| demo_schema2 |\n" + + "+--------------+", + output); + } private void addRepeatedCells(Column column, int count) { for (int i = 0; i < count; i++) { From 6d0b7422b187fa42f3c5ee624d73ef72963d6019 Mon Sep 17 00:00:00 2001 From: pancx Date: Sun, 19 Jan 2025 13:21:07 +0800 Subject: [PATCH 3/4] [#6326] improve(CLI): Make CLI more extendable and maintainable. fix some error. --- .../org/apache/gravitino/cli/CatalogCommandHandler.java | 8 ++------ .../org/apache/gravitino/cli/MetalakeCommandHandler.java | 8 ++------ .../org/apache/gravitino/cli/outputs/OutputProperty.java | 6 +++++- .../org/apache/gravitino/cli/outputs/TableFormat.java | 2 +- .../cli/integration/test/TableFormatOutputIT.java | 2 +- .../org/apache/gravitino/cli/output/TestTableFormat.java | 2 +- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/CatalogCommandHandler.java b/clients/cli/src/main/java/org/apache/gravitino/cli/CatalogCommandHandler.java index 37e6b50461a..a3b0057184d 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/CatalogCommandHandler.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/CatalogCommandHandler.java @@ -39,7 +39,6 @@ public class CatalogCommandHandler extends CommandHandler { private final FullName name; private final String metalake; private String catalog; - private final String outputFormat; /** * Constructs a {@link CatalogCommandHandler} instance. @@ -59,7 +58,6 @@ public CatalogCommandHandler( this.url = getUrl(line); this.name = new FullName(line); this.metalake = name.getMetalakeName(); - this.outputFormat = line.getOptionValue(GravitinoOptions.OUTPUT); } /** Handles the command execution logic based on the provided command. */ @@ -130,8 +128,7 @@ private void handleDetailsCommand() { gravitinoCommandLine.newCatalogAudit(url, ignore, metalake, catalog).validate().handle(); } else { // TODO: move this to GravitinoCommandLine class - OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setOutputFormat(outputFormat); + OutputProperty property = OutputProperty.fromLine(line); gravitinoCommandLine .newCatalogDetails(url, ignore, property, metalake, catalog) .validate() @@ -224,8 +221,7 @@ private void handleUpdateCommand() { /** Handles the "LIST" command. */ private void handleListCommand() { // TODO: move this to GravitinoCommandLine class - OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setOutputFormat(outputFormat); + OutputProperty property = OutputProperty.fromLine(line); gravitinoCommandLine.newListCatalogs(url, ignore, property, metalake).validate().handle(); } } diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/MetalakeCommandHandler.java b/clients/cli/src/main/java/org/apache/gravitino/cli/MetalakeCommandHandler.java index 25c02919364..d490822adb9 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/MetalakeCommandHandler.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/MetalakeCommandHandler.java @@ -113,22 +113,18 @@ private boolean executeCommand() { /** Handles the "LIST" command. */ private void handleListCommand() { - String outputFormat = line.getOptionValue(GravitinoOptions.OUTPUT); // TODO: move this to GravitinoCommandLine class - OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setOutputFormat(outputFormat); + OutputProperty property = OutputProperty.fromLine(line); gravitinoCommandLine.newListMetalakes(url, ignore, property).validate().handle(); } /** Handles the "DETAILS" command. */ private void handleDetailsCommand() { + OutputProperty property = OutputProperty.fromLine(line); if (line.hasOption(GravitinoOptions.AUDIT)) { gravitinoCommandLine.newMetalakeAudit(url, ignore, metalake).validate().handle(); } else { // TODO: move this to GravitinoCommandLine class - String outputFormat = line.getOptionValue(GravitinoOptions.OUTPUT); - OutputProperty property = OutputProperty.defaultOutputProperty(); - property.setOutputFormat(outputFormat); gravitinoCommandLine.newMetalakeDetails(url, ignore, property, metalake).validate().handle(); } } diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputProperty.java b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputProperty.java index e7e1782691b..f470abad147 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputProperty.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputProperty.java @@ -110,10 +110,14 @@ public static OutputProperty defaultOutputProperty() { */ public static OutputProperty fromLine(CommandLine line) { OutputProperty outputProperty = defaultOutputProperty(); + String outputFormat = line.getOptionValue(GravitinoOptions.OUTPUT); + if (outputFormat != null) { + outputProperty.setOutputFormat(outputFormat); + } + if (line.hasOption(GravitinoOptions.QUIET)) { outputProperty.setQuiet(true); } - // TODO: implement other options. return outputProperty; } 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 25ee3ad053a..5cdc5e3ae97 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 @@ -667,7 +667,7 @@ public String getOutput(Catalog[] catalogs) { output("No metalakes exist.", System.err); return null; } else { - Column columnA = new Column("METALAKE", null, property); + Column columnA = new Column("CATALOG", null, property); Arrays.stream(catalogs).forEach(metalake -> columnA.addCell(metalake.name())); return getTableFormat(columnA); 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 389971cbd76..54c5020fef4 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 @@ -171,7 +171,7 @@ public void testCatalogListCommand() { String output = new String(outputStream.toByteArray(), StandardCharsets.UTF_8).trim(); assertEquals( "+---+-----------+\n" - + "| | METALAKE |\n" + + "| | CATALOG |\n" + "+---+-----------+\n" + "| 1 | postgres |\n" + "| 2 | postgres2 |\n" 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 index 19a608e8ca6..00bff7ad7f5 100644 --- 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 @@ -515,7 +515,7 @@ void testCatalogsTableOutput() { String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); Assertions.assertEquals( "+---------------+\n" - + "| METALAKE |\n" + + "| CATALOG |\n" + "+---------------+\n" + "| demo_catalog1 |\n" + "| demo_catalog2 |\n" From 3fa8051c8025e1ce394293786971e7ae7358b3fc Mon Sep 17 00:00:00 2001 From: pancx Date: Mon, 20 Jan 2025 09:24:04 +0800 Subject: [PATCH 4/4] [#6326] improve(CLI): Make CLI more extendable and maintainable. fix some error. --- .../main/java/org/apache/gravitino/cli/outputs/TableFormat.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5cdc5e3ae97..05d1dc6b4c6 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 @@ -162,7 +162,7 @@ public String getTableFormat(Column... columns) { throw new RuntimeException(e); } - return baos.toString(); + return new String(baos.toByteArray(), StandardCharsets.UTF_8); } private void checkColumns(Column... columns) {