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

jmclean pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 7e93ab094e [#6798] feat(CLI): Support TableFormat and PlainFormat for 
Model, User and Group. (#6800)
7e93ab094e is described below

commit 7e93ab094ed88c80c19e95e14688dcd37425a55b
Author: Lord of Abyss <[email protected]>
AuthorDate: Wed May 7 15:39:43 2025 +0800

    [#6798] feat(CLI): Support TableFormat and PlainFormat for Model, User and 
Group. (#6800)
    
    ### What changes were proposed in this pull request?
    
    Uniform CLI output format for Model, User and Group commands.
    
    
    ### Why are the changes needed?
    
    Uniform CLI output format for Model, User and Group commands.
    
    Fix: #6798
    
    ### Does this PR introduce _any_ user-facing change?
    
    Users can output multiple entities using the CLI's table format.
    
    ### How was this patch tested?
    
    local test + ut.
    TableFormat test.
    ```bash
    bin/gcli.sh model list -m demo_metalake --name model_catalog.schema 
--output table
    +--------+
    |  Name  |
    +--------+
    | model2 |
    +--------+
    
    bin/gcli.sh model details -m demo_metalake --name 
model_catalog.schema.model2 --output table
    +--------+-------------+----------------+
    |  Name  |   Comment   | Latest version |
    +--------+-------------+----------------+
    | model2 | test rename | 0              |
    +--------+-------------+----------------+
    
    
    bin/gcli.sh user list -m demo_metalake --output table
    +-----------+
    |   Name    |
    +-----------+
    | anonymous |
    | testRole  |
    | test_user |
    +-----------+
    
    bin/gcli.sh user details -m demo_metalake --user testRole --output table
    The user has no roles.
    
    bin/gcli.sh group list -m demo_metalake --output table
    +---------------+
    |     Name      |
    +---------------+
    | group_no_role |
    | test_group    |
    +---------------+
    
    bin/gcli.sh group details -m demo_metalake --group test_group --output table
    The group has no roles.
    ```
    
    PlainFormat test.
    ```bash
    bin/gcli.sh model list -m demo_metalake --name model_catalog.schema
    model2
    
    bin/gcli.sh model list -m demo_metalake --name model_catalog.schema
    Model name model2, comment: test rename, latest version: 0
    
    bin/gcli.sh user list -m demo_metalake
    anonymous
    testRole
    test_user
    
    bin/gcli.sh user details -m demo_metalake --user testRole
    The user has no roles.
    
    bin/gcli.sh group list -m demo_metalake
    group_no_role
    test_group
    
    bin/gcli.sh group details -m demo_metalake --group test_group
    The group has no roles.
    ```
    
    ---------
    
    Signed-off-by: dependabot[bot] <[email protected]>
    Signed-off-by: George T. C. Lai <[email protected]>
    Co-authored-by: roryqi <[email protected]>
    Co-authored-by: dependabot[bot] 
<49699333+dependabot[bot]@users.noreply.github.com>
    Co-authored-by: mchades <[email protected]>
    Co-authored-by: George T. C. Lai <[email protected]>
    Co-authored-by: yangyang zhong <[email protected]>
    Co-authored-by: AndreVale69 <[email protected]>
    Co-authored-by: Mini Yu <[email protected]>
    Co-authored-by: Jerry Shao <[email protected]>
    Co-authored-by: FANNG <[email protected]>
    Co-authored-by: Eric Chang <[email protected]>
    Co-authored-by: gavin.wang <[email protected]>
    Co-authored-by: Kang <[email protected]>
    Co-authored-by: Zhengke Zhou <[email protected]>
    Co-authored-by: Justin Mclean <[email protected]>
    Co-authored-by: Danhua Wang <[email protected]>
    Co-authored-by: yunchi <[email protected]>
    Co-authored-by: RickyMa <[email protected]>
    Co-authored-by: Yuhui <[email protected]>
    Co-authored-by: Qiming Teng <[email protected]>
    Co-authored-by: Tian Lu <[email protected]>
    Co-authored-by: tian bao <[email protected]>
    Co-authored-by: Jimmy Lee <[email protected]>
    Co-authored-by: Brijesh Thummar <[email protected]>
    Co-authored-by: Qian Xia <[email protected]>
    Co-authored-by: Xiaojian Sun <[email protected]>
    Co-authored-by: Cyber Star <[email protected]>
---
 .../gravitino/cli/commands/GroupDetails.java       |   9 +-
 .../apache/gravitino/cli/commands/ListGroups.java  |  26 ++-
 .../apache/gravitino/cli/commands/ListModel.java   |  32 +++-
 .../apache/gravitino/cli/commands/ListUsers.java   |  26 ++-
 .../gravitino/cli/commands/ModelDetails.java       |   9 +-
 .../apache/gravitino/cli/commands/UserDetails.java |   9 +-
 .../apache/gravitino/cli/outputs/PlainFormat.java  | 131 +++++++++++++++
 .../apache/gravitino/cli/outputs/TableFormat.java  | 175 +++++++++++++++++++++
 .../gravitino/cli/output/TestPlainFormat.java      |  94 +++++++++++
 .../gravitino/cli/output/TestTableFormat.java      | 138 ++++++++++++++++
 10 files changed, 628 insertions(+), 21 deletions(-)

diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/GroupDetails.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/GroupDetails.java
index 8667dc4b67..002f69ef8e 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/GroupDetails.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/GroupDetails.java
@@ -20,6 +20,7 @@
 package org.apache.gravitino.cli.commands;
 
 import java.util.List;
+import org.apache.gravitino.authorization.Group;
 import org.apache.gravitino.cli.CommandContext;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
@@ -48,10 +49,12 @@ public class GroupDetails extends Command {
   @Override
   public void handle() {
     List<String> roles = null;
+    Group groupObject = null;
 
     try {
       GravitinoClient client = buildClient(metalake);
-      roles = client.getGroup(group).roles();
+      groupObject = client.getGroup(group);
+      roles = groupObject.roles();
     } catch (NoSuchMetalakeException err) {
       exitWithError(ErrorMessages.UNKNOWN_METALAKE);
     } catch (NoSuchUserException err) {
@@ -60,10 +63,10 @@ public class GroupDetails extends Command {
       exitWithError(exp.getMessage());
     }
 
-    if (roles == null) {
+    if (roles == null || roles.isEmpty()) {
       printInformation("The group has no roles.");
     } else {
-      printResults(String.join(",", roles));
+      printResults(groupObject);
     }
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
index b737498115..67d7c268d5 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
@@ -19,6 +19,10 @@
 
 package org.apache.gravitino.cli.commands;
 
+import java.util.Arrays;
+import java.util.List;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.authorization.Group;
 import org.apache.gravitino.cli.CommandContext;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
@@ -56,7 +60,27 @@ public class ListGroups extends Command {
     if (groups.length == 0) {
       printInformation("No groups found in metalake " + metalake);
     } else {
-      printResults(String.join(",", groups));
+      Group[] groupObjects = 
Arrays.stream(groups).map(this::getGroup).toArray(Group[]::new);
+      printResults(groupObjects);
     }
   }
+
+  private Group getGroup(String name) {
+    return new Group() {
+      @Override
+      public String name() {
+        return name;
+      }
+
+      @Override
+      public List<String> roles() {
+        return null;
+      }
+
+      @Override
+      public Audit auditInfo() {
+        return null;
+      }
+    };
+  }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListModel.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListModel.java
index fb39e08a93..00d74499a0 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListModel.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListModel.java
@@ -18,8 +18,8 @@
  */
 package org.apache.gravitino.cli.commands;
 
-import com.google.common.base.Joiner;
 import java.util.Arrays;
+import org.apache.gravitino.Audit;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.cli.CommandContext;
@@ -28,6 +28,7 @@ import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.exceptions.NoSuchCatalogException;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.model.Model;
 
 /** List the names of all models in a schema. */
 public class ListModel extends Command {
@@ -69,11 +70,30 @@ public class ListModel extends Command {
       exitWithError(err.getMessage());
     }
 
-    String output =
-        models.length == 0
-            ? "No models exist."
-            : Joiner.on(",").join(Arrays.stream(models).map(model -> 
model.name()).iterator());
+    if (models.length == 0) {
+      printInformation("No models exist.");
+    } else {
+      Model[] modelsArr = 
Arrays.stream(models).map(this::getModel).toArray(Model[]::new);
+      printResults(modelsArr);
+    }
+  }
+
+  private Model getModel(NameIdentifier modelIdent) {
+    return new Model() {
+      @Override
+      public String name() {
+        return modelIdent.name();
+      }
+
+      @Override
+      public int latestVersion() {
+        return 0;
+      }
 
-    printResults(output);
+      @Override
+      public Audit auditInfo() {
+        return null;
+      }
+    };
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListUsers.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListUsers.java
index 250d71e2f1..6f3905f158 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListUsers.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListUsers.java
@@ -19,6 +19,10 @@
 
 package org.apache.gravitino.cli.commands;
 
+import java.util.Arrays;
+import java.util.List;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.authorization.User;
 import org.apache.gravitino.cli.CommandContext;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
@@ -56,7 +60,27 @@ public class ListUsers extends Command {
     if (users.length == 0) {
       printInformation("No users exist.");
     } else {
-      printResults(String.join(",", users));
+      User[] userObjects = 
Arrays.stream(users).map(this::getUser).toArray(User[]::new);
+      printResults(userObjects);
     }
   }
+
+  private User getUser(String user) {
+    return new User() {
+      @Override
+      public String name() {
+        return user;
+      }
+
+      @Override
+      public List<String> roles() {
+        return null;
+      }
+
+      @Override
+      public Audit auditInfo() {
+        return null;
+      }
+    };
+  }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ModelDetails.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ModelDetails.java
index 4c5f1a57d4..a25f069f8a 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ModelDetails.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ModelDetails.java
@@ -19,7 +19,6 @@
 
 package org.apache.gravitino.cli.commands;
 
-import java.util.Arrays;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.cli.CommandContext;
 import org.apache.gravitino.cli.ErrorMessages;
@@ -61,13 +60,11 @@ public class ModelDetails extends Command {
   public void handle() {
     NameIdentifier name = NameIdentifier.of(schema, model);
     Model gModel = null;
-    int[] versions = new int[0];
 
     try {
       GravitinoClient client = buildClient(metalake);
       ModelCatalog modelCatalog = client.loadCatalog(catalog).asModelCatalog();
       gModel = modelCatalog.getModel(name);
-      versions = modelCatalog.listModelVersions(name);
     } catch (NoSuchMetalakeException noSuchMetalakeException) {
       exitWithError(ErrorMessages.UNKNOWN_METALAKE);
     } catch (NoSuchCatalogException noSuchCatalogException) {
@@ -79,9 +76,7 @@ public class ModelDetails extends Command {
     } catch (Exception err) {
       exitWithError(err.getMessage());
     }
-    String basicInfo =
-        String.format("Model name %s, latest version: %s%n", gModel.name(), 
gModel.latestVersion());
-    String versionInfo = Arrays.toString(versions);
-    printResults(basicInfo + "versions: " + versionInfo);
+
+    printResults(gModel);
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UserDetails.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UserDetails.java
index 26c11a0e12..ae7cb3a77d 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UserDetails.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UserDetails.java
@@ -20,6 +20,7 @@
 package org.apache.gravitino.cli.commands;
 
 import java.util.List;
+import org.apache.gravitino.authorization.User;
 import org.apache.gravitino.cli.CommandContext;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
@@ -48,10 +49,12 @@ public class UserDetails extends Command {
   @Override
   public void handle() {
     List<String> roles = null;
+    User userObject = null;
 
     try {
       GravitinoClient client = buildClient(metalake);
-      roles = client.getUser(user).roles();
+      userObject = client.getUser(user);
+      roles = userObject.roles();
     } catch (NoSuchMetalakeException err) {
       exitWithError(ErrorMessages.UNKNOWN_METALAKE);
     } catch (NoSuchUserException err) {
@@ -60,10 +63,10 @@ public class UserDetails extends Command {
       exitWithError(exp.getMessage());
     }
 
-    if (roles.isEmpty()) {
+    if (roles == null || roles.isEmpty()) {
       printInformation("The user has no roles.");
     } else {
-      printResults(String.join(",", roles));
+      printResults(userObject);
     }
   }
 }
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 fcbcb2f35b..649c676e0b 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
@@ -25,7 +25,10 @@ import org.apache.gravitino.Audit;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Metalake;
 import org.apache.gravitino.Schema;
+import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.User;
 import org.apache.gravitino.cli.CommandContext;
+import org.apache.gravitino.model.Model;
 import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.Table;
 
@@ -57,6 +60,18 @@ public abstract class PlainFormat<T> extends 
BaseOutputFormat<T> {
       new TablePlainFormat(context).output((Table) entity);
     } else if (entity instanceof Table[]) {
       new TableListPlainFormat(context).output((Table[]) entity);
+    } else if (entity instanceof Model) {
+      new ModelDetailPlainFormat(context).output((Model) entity);
+    } else if (entity instanceof Model[]) {
+      new ModelListPlainFormat(context).output((Model[]) entity);
+    } else if (entity instanceof User) {
+      new UserDetailsPlainFormat(context).output((User) entity);
+    } else if (entity instanceof User[]) {
+      new UserListPlainFormat(context).output((User[]) entity);
+    } else if (entity instanceof Group) {
+      new GroupDetailsPlainFormat(context).output((Group) entity);
+    } else if (entity instanceof Group[]) {
+      new GroupListPlainFormat(context).output((Group[]) entity);
     } else if (entity instanceof Audit) {
       new AuditPlainFormat(context).output((Audit) entity);
     } else if (entity instanceof Column[]) {
@@ -273,4 +288,120 @@ public abstract class PlainFormat<T> extends 
BaseOutputFormat<T> {
       return NEWLINE_JOINER.join(header, data.toString());
     }
   }
+
+  /**
+   * Format a {@link Model} instance with detailed information. Output format: 
model_name,
+   * model_comment and latest_version
+   */
+  static final class ModelDetailPlainFormat extends PlainFormat<Model> {
+
+    /**
+     * Creates a new {@link PlainFormat} with the specified command context.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public ModelDetailPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Model model) {
+      return String.format(
+          "Model name %s, latest version: %s%n", model.name(), 
model.latestVersion());
+    }
+  }
+
+  /** Format an array of {@link Model} instances with their names. Output 
format: model_name */
+  static final class ModelListPlainFormat extends PlainFormat<Model[]> {
+
+    /**
+     * Creates a new {@link PlainFormat} with the specified command context.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public ModelListPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Model[] models) {
+      return 
COMMA_JOINER.join(Arrays.stream(models).map(Model::name).collect(Collectors.toList()));
+    }
+  }
+
+  /** Format a {@link User} instance with their details. Output format: 
username, role */
+  static final class UserDetailsPlainFormat extends PlainFormat<User> {
+
+    /**
+     * Creates a new {@link PlainFormat} with the specified command context.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public UserDetailsPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(User user) {
+      return COMMA_JOINER.join(user.roles());
+    }
+  }
+
+  /** Format an array of {@link User} instances with their names. Output 
format: username */
+  static final class UserListPlainFormat extends PlainFormat<User[]> {
+
+    /**
+     * Creates a new {@link PlainFormat} with the specified command context.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public UserListPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(User[] users) {
+      return 
COMMA_JOINER.join(Arrays.stream(users).map(User::name).collect(Collectors.toList()));
+    }
+  }
+
+  /** Format a {@link Group} instance with their details. Output format: group 
name, role */
+  static final class GroupDetailsPlainFormat extends PlainFormat<Group> {
+    /**
+     * Constructs a new {@link GroupDetailsPlainFormat} instance.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public GroupDetailsPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Group group) {
+      return COMMA_JOINER.join(group.roles());
+    }
+  }
+
+  /** Format an array of {@link Group} instances with their names. Output 
format: group name */
+  static final class GroupListPlainFormat extends PlainFormat<Group[]> {
+    /**
+     * Constructs a new {@link GroupListPlainFormat} instance.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public GroupListPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Group[] groups) {
+      return 
COMMA_JOINER.join(Arrays.stream(groups).map(Group::name).collect(Collectors.toList()));
+    }
+  }
 }
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 e9ab7fd13b..c4e556abd3 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
@@ -49,7 +49,11 @@ import org.apache.gravitino.Audit;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Metalake;
 import org.apache.gravitino.Schema;
+import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.User;
 import org.apache.gravitino.cli.CommandContext;
+import org.apache.gravitino.cli.commands.Command;
+import org.apache.gravitino.model.Model;
 import org.apache.gravitino.rel.Table;
 
 /**
@@ -86,6 +90,18 @@ public abstract class TableFormat<T> extends 
BaseOutputFormat<T> {
       new TableDetailsTableFormat(context).output((Table) entity);
     } else if (entity instanceof Table[]) {
       new TableListTableFormat(context).output((Table[]) entity);
+    } else if (entity instanceof Model) {
+      new ModelDetailsTableFormat(context).output((Model) entity);
+    } else if (entity instanceof Model[]) {
+      new ModelListTableFormat(context).output((Model[]) entity);
+    } else if (entity instanceof User) {
+      new UserDetailsTableFormat(context).output((User) entity);
+    } else if (entity instanceof User[]) {
+      new UserListTableFormat(context).output((User[]) entity);
+    } else if (entity instanceof Group) {
+      new GroupDetailsTableFormat(context).output((Group) entity);
+    } else if (entity instanceof Group[]) {
+      new GroupListTableFormat(context).output((Group[]) entity);
     } else if (entity instanceof Audit) {
       new AuditTableFormat(context).output((Audit) entity);
     } else if (entity instanceof org.apache.gravitino.rel.Column[]) {
@@ -727,4 +743,163 @@ public abstract class TableFormat<T> extends 
BaseOutputFormat<T> {
           columnComment);
     }
   }
+
+  /**
+   * Formats a single {@link Model} instance into a three-column table 
display. Displays model
+   * details, including name, comment, and latest version.
+   */
+  static final class ModelDetailsTableFormat extends TableFormat<Model> {
+    /**
+     * Constructs a new {@link ModelDetailsTableFormat} with the specified 
CommandContext.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public ModelDetailsTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Model model) {
+      Column modelName = new Column(context, "name");
+      Column modelComment = new Column(context, "comment");
+      Column modelLatestVersion = new Column(context, "latest version");
+
+      modelName.addCell(model.name());
+      modelComment.addCell(model.comment());
+      modelLatestVersion.addCell(model.latestVersion());
+
+      return getTableFormat(modelName, modelComment, modelLatestVersion);
+    }
+  }
+
+  /**
+   * Formats an array of {@link Model} into a single-column table display. 
Lists all model names in
+   * a vertical format.
+   */
+  static final class ModelListTableFormat extends TableFormat<Model[]> {
+    /**
+     * Constructs a new {@link ModelListTableFormat} with the specified 
CommandContext.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public ModelListTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Model[] models) {
+      Column modelName = new Column(context, "name");
+      Arrays.stream(models).forEach(model -> modelName.addCell(model.name()));
+
+      return getTableFormat(modelName);
+    }
+  }
+
+  /**
+   * Formats a single {@link User} instance into a two-column table display. 
Displays user details,
+   * including name and roles.
+   */
+  static final class UserDetailsTableFormat extends TableFormat<User> {
+
+    /**
+     * Constructs a new {@link UserDetailsTableFormat} with the specified 
CommandContext.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public UserDetailsTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(User user) {
+      Column columnName = new Column(context, "name");
+      Column columnRoles = new Column(context, "roles");
+
+      columnName.addCell(user.name());
+      columnRoles.addCell(Command.COMMA_JOINER.join(user.roles()));
+
+      return getTableFormat(columnName, columnRoles);
+    }
+  }
+
+  /**
+   * Formats an array of {@link User} into a single-column table display. 
Lists all usernames in a
+   * vertical format.
+   */
+  static final class UserListTableFormat extends TableFormat<User[]> {
+
+    /**
+     * Constructs a new {@link UserListTableFormat} with the specified 
CommandContext.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public UserListTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(User[] users) {
+      Column name = new Column(context, "name");
+      Arrays.stream(users).forEach(user -> name.addCell(user.name()));
+
+      return getTableFormat(name);
+    }
+  }
+
+  /**
+   * Formats a single {@link Group} instance into a two-column table display. 
Displays group
+   * details, including name and roles.
+   */
+  static final class GroupDetailsTableFormat extends TableFormat<Group> {
+
+    /**
+     * Constructs a new {@link GroupDetailsTableFormat} with the specified 
CommandContext.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public GroupDetailsTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Group group) {
+      Column columnName = new Column(context, "name");
+      Column columnRoles = new Column(context, "roles");
+
+      columnName.addCell(group.name());
+      columnRoles.addCell(Command.COMMA_JOINER.join(group.roles()));
+
+      return getTableFormat(columnName, columnRoles);
+    }
+  }
+
+  /**
+   * Formats an array of {@link Group} into a single-column table display. 
Lists all group names in
+   * a vertical format.
+   */
+  static final class GroupListTableFormat extends TableFormat<Group[]> {
+
+    /**
+     * Constructs a new {@link GroupListTableFormat} with the specified 
CommandContext.
+     *
+     * @param context the {@link CommandContext} instance.
+     */
+    public GroupListTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Group[] groups) {
+      Column name = new Column(context, "name");
+      Arrays.stream(groups).forEach(group -> name.addCell(group.name()));
+
+      return getTableFormat(name);
+    }
+  }
 }
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 9e81154ff6..c69424e116 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
@@ -27,12 +27,16 @@ import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
+import java.util.List;
 import org.apache.gravitino.Audit;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Metalake;
 import org.apache.gravitino.Schema;
+import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.User;
 import org.apache.gravitino.cli.CommandContext;
 import org.apache.gravitino.cli.outputs.PlainFormat;
+import org.apache.gravitino.model.Model;
 import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.rel.expressions.Expression;
@@ -43,6 +47,7 @@ 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.collect.ImmutableList;
 
 public class TestPlainFormat {
 
@@ -339,6 +344,70 @@ public class TestPlainFormat {
     return mockTable;
   }
 
+  @Test
+  void testModelDetailsWithPlainFormat() {
+    CommandContext mockContext = getMockContext();
+    Model mockModel = getMockModel("demo_model", "This is a demo model", 1);
+
+    PlainFormat.output(mockModel, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals("Model name demo_model, latest version: 1", 
output);
+  }
+
+  @Test
+  void testListModelWithPlainFormat() {
+    CommandContext mockContext = getMockContext();
+    Model model1 = getMockModel("model1", "This is a model", 1);
+    Model model2 = getMockModel("model2", "This is another model", 2);
+    Model model3 = getMockModel("model3", "This is a third model", 3);
+
+    PlainFormat.output(new Model[] {model1, model2, model3}, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals("model1,model2,model3", output);
+  }
+
+  @Test
+  void testUserDetailsWithPlainFormat() {
+    CommandContext mockContext = getMockContext();
+    User mockUser = getMockUser("demo_user", ImmutableList.of("admin", 
"user"));
+    PlainFormat.output(mockUser, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals("admin,user", output);
+  }
+
+  @Test
+  void testListUsersWithPlainFormat() {
+    CommandContext mockContext = getMockContext();
+    User user1 = getMockUser("user1", ImmutableList.of("admin", "user"));
+    User user2 = getMockUser("user2", ImmutableList.of("admin"));
+    User user3 = getMockUser("user3", ImmutableList.of("user"));
+
+    PlainFormat.output(new User[] {user1, user2, user3}, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals("user1,user2,user3", output);
+  }
+
+  @Test
+  void testGroupDetailsWithPlainFormat() {
+    CommandContext mockContext = getMockContext();
+    Group mockGroup = getMockGroup("demo_group", ImmutableList.of("admin", 
"scientist"));
+    PlainFormat.output(mockGroup, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals("admin,scientist", output);
+  }
+
+  @Test
+  void testListGroupsWithPlainFormat() {
+    CommandContext mockContext = getMockContext();
+    Group group1 = getMockGroup("group1", ImmutableList.of("admin", "user"));
+    Group group2 = getMockGroup("group2", ImmutableList.of("admin"));
+    Group group3 = getMockGroup("group3", ImmutableList.of("user"));
+
+    PlainFormat.output(new Group[] {group1, group2, group3}, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals("group1,group2,group3", output);
+  }
+
   private org.apache.gravitino.rel.Column getMockColumn(
       String name,
       Type dataType,
@@ -357,4 +426,29 @@ public class TestPlainFormat {
 
     return mockColumn;
   }
+
+  private Model getMockModel(String name, String comment, int lastVersion) {
+    Model mockModel = mock(Model.class);
+    when(mockModel.name()).thenReturn(name);
+    when(mockModel.comment()).thenReturn(comment);
+    when(mockModel.latestVersion()).thenReturn(lastVersion);
+
+    return mockModel;
+  }
+
+  private User getMockUser(String name, List<String> roles) {
+    User mockUser = mock(User.class);
+    when(mockUser.name()).thenReturn(name);
+    when(mockUser.roles()).thenReturn(roles);
+
+    return mockUser;
+  }
+
+  private Group getMockGroup(String name, List<String> roles) {
+    Group mockGroup = mock(Group.class);
+    when(mockGroup.name()).thenReturn(name);
+    when(mockGroup.roles()).thenReturn(roles);
+
+    return mockGroup;
+  }
 }
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 3f45459bc5..45909b89ce 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
@@ -29,13 +29,18 @@ import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
+import java.util.Arrays;
+import java.util.List;
 import org.apache.gravitino.Audit;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Metalake;
 import org.apache.gravitino.Schema;
+import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.User;
 import org.apache.gravitino.cli.CommandContext;
 import org.apache.gravitino.cli.outputs.Column;
 import org.apache.gravitino.cli.outputs.TableFormat;
+import org.apache.gravitino.model.Model;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.rel.expressions.Expression;
 import org.apache.gravitino.rel.expressions.FunctionExpression;
@@ -559,6 +564,114 @@ public class TestTableFormat {
         output);
   }
 
+  @Test
+  void testListModelWithTableFormat() {
+    CommandContext mockContext = getMockContext();
+    Model model1 = getMockModel("model1", "This is a demo model", 1);
+    Model model2 = getMockModel("model2", "This is another demo model", 2);
+    Model model3 = getMockModel("model3", "This is a third demo model", 3);
+
+    TableFormat.output(new Model[] {model1, model2, model3}, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+--------+\n"
+            + "|  Name  |\n"
+            + "+--------+\n"
+            + "| model1 |\n"
+            + "| model2 |\n"
+            + "| model3 |\n"
+            + "+--------+",
+        output);
+  }
+
+  @Test
+  void testModelDetailsWithTableFormat() {
+    CommandContext mockContext = getMockContext();
+    Model mockModel = getMockModel("demo_model", "This is a demo model", 1);
+
+    TableFormat.output(mockModel, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+------------+----------------------+----------------+\n"
+            + "|    Name    |       Comment        | Latest version |\n"
+            + "+------------+----------------------+----------------+\n"
+            + "| demo_model | This is a demo model | 1              |\n"
+            + "+------------+----------------------+----------------+",
+        output);
+  }
+
+  @Test
+  void testListUserWithTableFormat() {
+    CommandContext mockContext = getMockContext();
+    User user1 = getMockUser("user1", Arrays.asList("role1", "role2"));
+    User user2 = getMockUser("user2", Arrays.asList("role3", "role4"));
+    User user3 = getMockUser("user3", Arrays.asList("role5"));
+
+    TableFormat.output(new User[] {user1, user2, user3}, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+-------+\n"
+            + "| Name  |\n"
+            + "+-------+\n"
+            + "| user1 |\n"
+            + "| user2 |\n"
+            + "| user3 |\n"
+            + "+-------+",
+        output);
+  }
+
+  @Test
+  void testUserDetailsWithTableFormat() {
+    CommandContext mockContext = getMockContext();
+    User mockUser = getMockUser("demo_user", Arrays.asList("role1", "role2"));
+
+    TableFormat.output(mockUser, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+-----------+--------------+\n"
+            + "|   Name    |    Roles     |\n"
+            + "+-----------+--------------+\n"
+            + "| demo_user | role1, role2 |\n"
+            + "+-----------+--------------+",
+        output);
+  }
+
+  @Test
+  void testListGroupWithTableFormat() {
+    CommandContext mockContext = getMockContext();
+    Group group1 = getMockGroup("group1", Arrays.asList("role1", "role2"));
+    Group group2 = getMockGroup("group2", Arrays.asList("role3", "role4"));
+    Group group3 = getMockGroup("group3", Arrays.asList("role5"));
+
+    TableFormat.output(new Group[] {group1, group2, group3}, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+--------+\n"
+            + "|  Name  |\n"
+            + "+--------+\n"
+            + "| group1 |\n"
+            + "| group2 |\n"
+            + "| group3 |\n"
+            + "+--------+",
+        output);
+  }
+
+  @Test
+  void testGroupDetailsWithTableFormat() {
+    CommandContext mockContext = getMockContext();
+    Group mockGroup = getMockGroup("demo_group", Arrays.asList("role1", 
"role2"));
+
+    TableFormat.output(mockGroup, mockContext);
+    String output = new String(outContent.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+------------+--------------+\n"
+            + "|    Name    |    Roles     |\n"
+            + "+------------+--------------+\n"
+            + "| demo_group | role1, role2 |\n"
+            + "+------------+--------------+",
+        output);
+  }
+
   @Test
   void testOutputWithUnsupportType() {
     CommandContext mockContext = getMockContext();
@@ -665,4 +778,29 @@ public class TestTableFormat {
 
     return mockColumn;
   }
+
+  private Model getMockModel(String name, String comment, int lastVersion) {
+    Model mockModel = mock(Model.class);
+    when(mockModel.name()).thenReturn(name);
+    when(mockModel.comment()).thenReturn(comment);
+    when(mockModel.latestVersion()).thenReturn(lastVersion);
+
+    return mockModel;
+  }
+
+  private User getMockUser(String name, List<String> roles) {
+    User mockUser = mock(User.class);
+    when(mockUser.name()).thenReturn(name);
+    when(mockUser.roles()).thenReturn(roles);
+
+    return mockUser;
+  }
+
+  private Group getMockGroup(String name, List<String> roles) {
+    Group mockGroup = mock(Group.class);
+    when(mockGroup.name()).thenReturn(name);
+    when(mockGroup.roles()).thenReturn(roles);
+
+    return mockGroup;
+  }
 }


Reply via email to