Repository: apache/gravitino
Pull request #7123: [#6799] feat(core): Support TableFormat and PlainFormat for Topic, Fileset and Role
Author: login: Abyss-lord
Created: 2025-05-07T10:32:46Z | Merged: 2025-05-26T00:18:40Z

### What changes were proposed in this pull request?

Support TableFormat and PlainFormat for Topic, Fileset and Role

### Why are the changes needed?

Fix: #6799 

### Does this PR introduce _any_ user-facing change?

`TableFormat` now support Topic, Role and FileSet Entities.

### How was this patch tested?

local test.


Diff:
diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/FilesetDetails.java b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/FilesetDetails.java
index 5654f44a627..e99d37df78b 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/FilesetDetails.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/FilesetDetails.java
@@ -77,15 +77,7 @@ public void handle() {
     }
 
     if (result != null) {
-      String filesetType = (result.type() == Fileset.Type.MANAGED) ? "managed" : "external";
-      printResults(
-          result.name()
-              + ","
-              + filesetType
-              + ","
-              + result.comment()
-              + ","
-              + result.storageLocation());
+      printResults(result);
     }
   }
 }
diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListFilesets.java b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListFilesets.java
index ab5e97a5493..c7ec6f5f97d 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListFilesets.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListFilesets.java
@@ -19,7 +19,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 +29,7 @@
 import org.apache.gravitino.exceptions.NoSuchCatalogException;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.file.Fileset;
 
 /** List all fileset names in a schema. */
 public class ListFilesets extends Command {
@@ -70,8 +72,31 @@ public void handle() {
       exitWithError(exp.getMessage());
     }
 
-    String all = filesets.length == 0 ? "No filesets exist." : Joiner.on(",").join(filesets);
+    if (filesets.length == 0) {
+      printInformation("No filesets exist.");
+    } else {
+      Fileset[] filesetObjects =
+          Arrays.stream(filesets).map(ident -> getFileset(ident.name())).toArray(Fileset[]::new);
+      printResults(filesetObjects);
+    }
+  }
+
+  private Fileset getFileset(String name) {
+    return new Fileset() {
+      @Override
+      public String name() {
+        return name;
+      }
+
+      @Override
+      public Type type() {
+        return null;
+      }
 
-    printResults(all);
+      @Override
+      public Audit auditInfo() {
+        return null;
+      }
+    };
   }
 }
diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListRoles.java b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListRoles.java
index d859c377c7f..ee6e79beec9 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListRoles.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListRoles.java
@@ -19,6 +19,12 @@
 
 package org.apache.gravitino.cli.commands;
 
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.authorization.Role;
+import org.apache.gravitino.authorization.SecurableObject;
 import org.apache.gravitino.cli.CommandContext;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
@@ -55,7 +61,32 @@ public void handle() {
     if (roles.length == 0) {
       printInformation("No roles exist.");
     } else {
-      printResults(String.join(",", roles));
+      Role[] roleObjects = Arrays.stream(roles).map(this::getRole).toArray(Role[]::new);
+      printResults(roleObjects);
     }
   }
+
+  private Role getRole(String name) {
+    return new Role() {
+      @Override
+      public String name() {
+        return name;
+      }
+
+      @Override
+      public Map<String, String> properties() {
+        return null;
+      }
+
+      @Override
+      public List<SecurableObject> securableObjects() {
+        return null;
+      }
+
+      @Override
+      public Audit auditInfo() {
+        return null;
+      }
+    };
+  }
 }
diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListTopics.java b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListTopics.java
index 9612ec0900b..f9c52b16a12 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListTopics.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListTopics.java
@@ -19,14 +19,15 @@
 
 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;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.messaging.Topic;
 
 /** List the topics. */
 public class ListTopics extends Command {
@@ -65,10 +66,26 @@ public void handle() {
       exitWithError(exp.getMessage());
     }
 
-    String all =
-        topics.length == 0
-            ? "No topics exist."
-            : Joiner.on(",").join(Arrays.stream(topics).map(topic -> topic.name()).iterator());
-    printResults(all);
+    if (topics.length == 0) {
+      printInformation("No topics exist.");
+    } else {
+      Topic[] topicObjects =
+          Arrays.stream(topics).map(ident -> getTopic(ident.name())).toArray(Topic[]::new);
+      printResults(topicObjects);
+    }
+  }
+
+  private Topic getTopic(String name) {
+    return new Topic() {
+      @Override
+      public String name() {
+        return name;
+      }
+
+      @Override
+      public Audit auditInfo() {
+        return null;
+      }
+    };
   }
 }
diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RoleDetails.java b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RoleDetails.java
index 94af016df37..1ab9ec854b0 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RoleDetails.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RoleDetails.java
@@ -20,7 +20,6 @@
 package org.apache.gravitino.cli.commands;
 
 import java.util.List;
-import org.apache.gravitino.authorization.Privilege;
 import org.apache.gravitino.authorization.SecurableObject;
 import org.apache.gravitino.cli.CommandContext;
 import org.apache.gravitino.cli.ErrorMessages;
@@ -62,12 +61,10 @@ public void handle() {
       exitWithError(exp.getMessage());
     }
 
-    for (SecurableObject object : objects) {
-      printInformation(object.name() + "," + object.type() + ",");
-      for (Privilege privilege : object.privileges()) {
-        printInformation(privilege.simpleString() + " ");
-      }
+    if (objects == null || objects.isEmpty()) {
+      printInformation("No securable objects found for role: " + role);
+    } else {
+      printResults(objects);
     }
-    printInformation("");
   }
 }
diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/TopicDetails.java b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/TopicDetails.java
index d72002419d6..79b7dd98d1c 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/TopicDetails.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/TopicDetails.java
@@ -76,6 +76,8 @@ public void handle() {
       exitWithError(exp.getMessage());
     }
 
-    printResults(gTopic.name() + "," + gTopic.comment());
+    if (gTopic != null) {
+      printResults(gTopic);
+    }
   }
 }
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 6418021c29f..330c898fb8f 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
@@ -27,8 +27,13 @@
 import org.apache.gravitino.Metalake;
 import org.apache.gravitino.Schema;
 import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.authorization.Role;
+import org.apache.gravitino.authorization.SecurableObject;
 import org.apache.gravitino.authorization.User;
 import org.apache.gravitino.cli.CommandContext;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.messaging.Topic;
 import org.apache.gravitino.model.Model;
 import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.Table;
@@ -46,6 +51,7 @@ public abstract class PlainFormat<T> extends BaseOutputFormat<T> {
    * @throws IllegalArgumentException if the object type is not supported
    */
   public static void output(Object entity, CommandContext context) {
+
     if (entity instanceof Metalake) {
       new MetalakePlainFormat(context).output((Metalake) entity);
     } else if (entity instanceof Metalake[]) {
@@ -78,6 +84,18 @@ public static void output(Object entity, CommandContext context) {
       new AuditPlainFormat(context).output((Audit) entity);
     } else if (entity instanceof Column[]) {
       new ColumnListPlainFormat(context).output((Column[]) entity);
+    } else if (entity instanceof Role) {
+      new RoleDetailsPlainFormat(context).output((Role) entity);
+    } else if (entity instanceof Role[]) {
+      new RoleListPlainFormat(context).output((Role[]) entity);
+    } else if (entity instanceof Fileset) {
+      new FilesetDetailsPlainFormat(context).output((Fileset) entity);
+    } else if (entity instanceof Fileset[]) {
+      new FilesetListPlainFormat(context).output((Fileset[]) entity);
+    } else if (entity instanceof Topic) {
+      new TopicDetailsPlainFormat(context).output((Topic) entity);
+    } else if (entity instanceof Topic[]) {
+      new TopicListPlainFormat(context).output((Topic[]) entity);
     } else if (entity instanceof Tag) {
       new TagDetailsPlainFormat(context).output((Tag) entity);
     } else if (entity instanceof Tag[]) {
@@ -91,7 +109,7 @@ public static void output(Object entity, CommandContext context) {
   }
 
   /**
-   * Creates a new {@link PlainFormat} with the specified output properties.
+   * Creates a new {@link PlainFormat} with the specified command context.
    *
    * @param context The command context.
    */
@@ -267,7 +285,7 @@ public String getOutput(Audit audit) {
   static final class ColumnListPlainFormat extends PlainFormat<Column[]> {
 
     /**
-     * Creates a new {@link ColumnListPlainFormat} with the specified output properties.
+     * Creates a new {@link ColumnListPlainFormat} with the specified command context.
      *
      * @param context The command context.
      */
@@ -485,4 +503,144 @@ public String getOutput(Map<?, ?> properties) {
       return data.toString();
     }
   }
+
+  /**
+   * Format a {@link Role} instance with their details. Output format: securable object details
+   * which belongs to the role,
+   */
+  static final class RoleDetailsPlainFormat extends PlainFormat<Role> {
+
+    /**
+     * Creates a new {@link PlainFormat} with the specified CommandContext.
+     *
+     * @param context The command context.
+     */
+    public RoleDetailsPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Role entity) {
+      List<SecurableObject> objects = entity.securableObjects();
+      StringBuilder sb = new StringBuilder();
+      for (SecurableObject object : objects) {
+        sb.append(object.name()).append(",").append(object.type()).append(",").append("\n");
+        sb.append(
+            String.join(
+                "\n",
+                object.privileges().stream().map(Privilege::simpleString).toArray(String[]::new)));
+        sb.append("\n");
+      }
+
+      return sb.toString();
+    }
+  }
+
+  /** Format an array of {@link Role} instances with their names. Output format: role name */
+  static final class RoleListPlainFormat extends PlainFormat<Role[]> {
+    /**
+     * Creates a new {@link PlainFormat} with the specified CommandContext.
+     *
+     * @param context The command context.
+     */
+    public RoleListPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Role[] entity) {
+      List<String> roleList = Arrays.stream(entity).map(Role::name).collect(Collectors.toList());
+      return String.join(",", roleList);
+    }
+  }
+
+  /**
+   * Format a {@link Fileset} instance with their details. Output format: fileset name, type,
+   * comment and storage location
+   */
+  static final class FilesetDetailsPlainFormat extends PlainFormat<Fileset> {
+
+    /**
+     * Creates a new {@link PlainFormat} with the specified CommandContext.
+     *
+     * @param context The command context.
+     */
+    public FilesetDetailsPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    @Override
+    public String getOutput(Fileset entity) {
+      String filesetType = (entity.type() == Fileset.Type.MANAGED) ? "managed" : "external";
+      return entity.name()
+          + ","
+          + filesetType
+          + ","
+          + entity.comment()
+          + ","
+          + entity.storageLocation();
+    }
+  }
+
+  /** Format an array of {@link Fileset} instances with their names. Output format: fileset name */
+  static final class FilesetListPlainFormat extends PlainFormat<Fileset[]> {
+
+    /**
+     * Creates a new {@link PlainFormat} with the specified CommandContext.
+     *
+     * @param context The command context.
+     */
+    public FilesetListPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Fileset[] entity) {
+      List<String> filesetList =
+          Arrays.stream(entity).map(Fileset::name).collect(Collectors.toList());
+      return String.join(",", filesetList);
+    }
+  }
+
+  /** Format a {@link Topic} instance with their details. Output format: topic name, comment */
+  static final class TopicDetailsPlainFormat extends PlainFormat<Topic> {
+
+    /**
+     * Creates a new {@link PlainFormat} with the specified CommandContext.
+     *
+     * @param context The command context.
+     */
+    public TopicDetailsPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Topic entity) {
+      return entity.name() + "," + entity.comment();
+    }
+  }
+
+  /** Format an array of {@link Topic} instances with their names. Output format: topic name */
+  static final class TopicListPlainFormat extends PlainFormat<Topic[]> {
+
+    /**
+     * Creates a new {@link PlainFormat} with the specified CommandContext.
+     *
+     * @param context The command context.
+     */
+    public TopicListPlainFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Topic[] entity) {
+      List<String> topicList = Arrays.stream(entity).map(Topic::name).collect(Collectors.toList());
+      return String.join(",", topicList);
+    }
+  }
 }
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 a34602f11bb..54ab6150f64 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
@@ -46,14 +46,20 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.stream.Collectors;
 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.Privilege;
+import org.apache.gravitino.authorization.Role;
+import org.apache.gravitino.authorization.SecurableObject;
 import org.apache.gravitino.authorization.User;
 import org.apache.gravitino.cli.CommandContext;
 import org.apache.gravitino.cli.commands.Command;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.messaging.Topic;
 import org.apache.gravitino.model.Model;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.tag.Tag;
@@ -108,6 +114,18 @@ public static void output(Object entity, CommandContext context) {
       new AuditTableFormat(context).output((Audit) entity);
     } else if (entity instanceof org.apache.gravitino.rel.Column[]) {
       new ColumnListTableFormat(context).output((org.apache.gravitino.rel.Column[]) entity);
+    } else if (entity instanceof Role) {
+      new RoleDetailsTableFormat(context).output((Role) entity);
+    } else if (entity instanceof Role[]) {
+      new RoleListTableFormat(context).output((Role[]) entity);
+    } else if (entity instanceof Fileset) {
+      new FilesetDetailsTableFormat(context).output((Fileset) entity);
+    } else if (entity instanceof Fileset[]) {
+      new FilesetListTableFormat(context).output((Fileset[]) entity);
+    } else if (entity instanceof Topic) {
+      new TopicDetailsTableFormat(context).output((Topic) entity);
+    } else if (entity instanceof Topic[]) {
+      new TopicListTableFormat(context).output((Topic[]) entity);
     } else if (entity instanceof Tag) {
       new TagDetailsTableFormat(context).output((Tag) entity);
     } else if (entity instanceof Tag[]) {
@@ -121,7 +139,7 @@ public static void output(Object entity, CommandContext context) {
   }
 
   /**
-   * Creates a new {@link TableFormat} with the specified properties.
+   * Creates a new {@link TableFormat} with the specified command context.
    *
    * @param context the command context.
    */
@@ -492,7 +510,7 @@ private static void writeRepeated(OutputStreamWriter osw, char c, int num) throw
   static final class MetalakeTableFormat extends TableFormat<Metalake> {
 
     /**
-     * Creates a new {@link TableFormat} with the specified properties.
+     * Creates a new {@link TableFormat} with the specified command context.
      *
      * @param context the command context.
      */
@@ -716,7 +734,7 @@ public String getOutput(Audit audit) {
   static final class ColumnListTableFormat extends TableFormat<org.apache.gravitino.rel.Column[]> {
 
     /**
-     * Creates a new {@link TableFormat} with the specified properties.
+     * Creates a new {@link TableFormat} with the specified command context.
      *
      * @param context the command context.
      */
@@ -938,7 +956,7 @@ public String getOutput(Tag tag) {
   static final class TagListTableFormat extends TableFormat<Tag[]> {
 
     /**
-     * Creates a new {@link TableFormat} with the specified properties.
+     * Creates a new {@link TableFormat} with the specified command context.
      *
      * @param context the command context.
      */
@@ -966,7 +984,7 @@ public String getOutput(Tag[] tags) {
   static final class PropertiesListTableFormat extends TableFormat<Map<?, ?>> {
 
     /**
-     * Creates a new {@link TableFormat} with the specified properties.
+     * Creates a new {@link TableFormat} with the specified command context.
      *
      * @param context the command context.
      */
@@ -995,4 +1013,174 @@ public String getOutput(Map<?, ?> properties) {
       return getTableFormat(columnKey, columnValue);
     }
   }
+
+  /**
+   * Formats a single {@link Role} instance into a two-column table display. Displays role details,
+   */
+  static final class RoleDetailsTableFormat extends TableFormat<Role> {
+
+    /**
+     * Creates a new {@link TableFormat} with the specified CommandContext.
+     *
+     * @param context the command context.
+     */
+    public RoleDetailsTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    @Override
+    public String getOutput(Role entity) {
+      Column objectName = new Column(context, "name");
+      Column objectType = new Column(context, "type");
+      Column privileges = new Column(context, "privileges");
+
+      List<SecurableObject> securableObjects = entity.securableObjects();
+      for (SecurableObject object : securableObjects) {
+        List<String> privilegeStrings =
+            object.privileges().stream().map(Privilege::simpleString).collect(Collectors.toList());
+        for (int i = 0; i < privilegeStrings.size(); i++) {
+          objectName.addCell(i == 0 ? object.name() : "");
+          objectType.addCell(i == 0 ? object.type().name() : "");
+          privileges.addCell(privilegeStrings.get(i));
+        }
+      }
+
+      return getTableFormat(objectName, objectType, privileges);
+    }
+  }
+
+  /**
+   * Formats an array of {@link Role} into a single-column table display. Lists all role names in a
+   * vertical format.
+   */
+  static final class RoleListTableFormat extends TableFormat<Role[]> {
+
+    /**
+     * Creates a new {@link TableFormat} with the specified CommandContext
+     *
+     * @param context the command context.
+     */
+    public RoleListTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Role[] entity) {
+      Column roleName = new Column(context, "name");
+      Arrays.stream(entity).forEach(role -> roleName.addCell(role.name()));
+
+      return getTableFormat(roleName);
+    }
+  }
+
+  /**
+   * Format a single {@link Fileset} instace into a four-column table display. Displays fileset
+   * name, type, comment and location.
+   */
+  static final class FilesetDetailsTableFormat extends TableFormat<Fileset> {
+
+    /**
+     * Creates a new {@link TableFormat} with the specified CommandContext.
+     *
+     * @param context the command context.
+     */
+    public FilesetDetailsTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Fileset entity) {
+      Column name = new Column(context, "name");
+      Column type = new Column(context, "type");
+      Column comment = new Column(context, "comment");
+      Column location = new Column(context, "location");
+
+      String filesetType = (entity.type() == Fileset.Type.MANAGED) ? "managed" : "external";
+      name.addCell(entity.name());
+      type.addCell(filesetType);
+      comment.addCell(entity.comment());
+      location.addCell(entity.storageLocation());
+
+      return getTableFormat(name, type, comment, location);
+    }
+  }
+
+  /**
+   * Formats an array of {@link Fileset} into a single-column table display. Lists all fileset names
+   * in a vertical format.
+   */
+  static final class FilesetListTableFormat extends TableFormat<Fileset[]> {
+
+    /**
+     * Creates a new {@link TableFormat} with the specified CommandContext.
+     *
+     * @param context the command context.
+     */
+    public FilesetListTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Fileset[] entity) {
+      Column filesetName = new Column(context, "name");
+      Arrays.stream(entity).forEach(fs -> filesetName.addCell(fs.name()));
+
+      return getTableFormat(filesetName);
+    }
+  }
+
+  /**
+   * Formats a single {@link Topic} instance into a two-column table display. Displays topic name
+   * and comment.
+   */
+  static final class TopicDetailsTableFormat extends TableFormat<Topic> {
+
+    /**
+     * Creates a new {@link TableFormat} with the specified CommandContext.
+     *
+     * @param context the command context.
+     */
+    public TopicDetailsTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Topic entity) {
+      Column name = new Column(context, "name");
+      Column comment = new Column(context, "comment");
+
+      name.addCell(entity.name());
+      comment.addCell(entity.comment());
+
+      return getTableFormat(name, comment);
+    }
+  }
+
+  /**
+   * Formats an array of {@link Topic} into a single-column table display. Lists all topic names in
+   * a vertical format.
+   */
+  static final class TopicListTableFormat extends TableFormat<Topic[]> {
+    /**
+     * Creates a new {@link TableFormat} with the specified CommandContext.
+     *
+     * @param context the command context.
+     */
+    public TopicListTableFormat(CommandContext context) {
+      super(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getOutput(Topic[] entity) {
+      Column name = new Column(context, "name");
+      Arrays.stream(entity).forEach(topic -> name.addCell(topic.name()));
+
+      return getTableFormat(name);
+    }
+  }
 }
diff --git a/clients/cli/src/test/java/org/apache/gravitino/cli/TestCliUtil.java b/clients/cli/src/test/java/org/apache/gravitino/cli/TestCliUtil.java
new file mode 100644
index 00000000000..042d8e5ca45
--- /dev/null
+++ b/clients/cli/src/test/java/org/apache/gravitino/cli/TestCliUtil.java
@@ -0,0 +1,333 @@
+/*
+ * 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;
+
+import static org.apache.gravitino.rel.Column.DEFAULT_VALUE_NOT_SET;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.Metalake;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.Privileges;
+import org.apache.gravitino.authorization.Role;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.authorization.User;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.messaging.Topic;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.expressions.Expression;
+import org.apache.gravitino.rel.types.Type;
+import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.tag.Tag;
+import org.testcontainers.shaded.com.google.common.collect.ImmutableList;
+
+public class TestCliUtil {
+  private TestCliUtil() {
+    // Utility class, no public constructor
+  }
+
+  /**
+   * Creates a mock CommandContext object.
+   *
+   * @return A mock CommandContext object
+   */
+  public static CommandContext getMockContext() {
+    CommandContext mockContext = mock(CommandContext.class);
+
+    return mockContext;
+  }
+
+  /**
+   * Create a mock Metalake object with the given name and comment.
+   *
+   * @param name The name of the Metalake object
+   * @param comment The comment of the Metalake object
+   * @return A mock Metalake object
+   */
+  public static Metalake getMockMetalake(String name, String comment) {
+    Metalake mockMetalake = mock(Metalake.class);
+    when(mockMetalake.name()).thenReturn(name);
+    when(mockMetalake.comment()).thenReturn(comment);
+
+    return mockMetalake;
+  }
+
+  /**
+   * Create a mock catalog object with the given name, type, provider, and comment.
+   *
+   * @param name The name of the catalog object
+   * @param type The type of the catalog object
+   * @param provider The provider of the catalog object
+   * @param comment The comment of the catalog object
+   * @return A mock catalog object
+   */
+  public static Catalog getMockCatalog(
+      String name, Catalog.Type type, String provider, String comment) {
+    Catalog mockCatalog = mock(Catalog.class);
+    when(mockCatalog.name()).thenReturn(name);
+    when(mockCatalog.type()).thenReturn(type);
+    when(mockCatalog.provider()).thenReturn(provider);
+    when(mockCatalog.comment()).thenReturn(comment);
+
+    return mockCatalog;
+  }
+
+  /**
+   * Create a mock schema object with the given name and comment.
+   *
+   * @param name The name of the schema object
+   * @param comment The comment of the schema object
+   * @return A mock schema object
+   */
+  public static Schema getMockSchema(String name, String comment) {
+    Schema mockSchema = mock(Schema.class);
+    when(mockSchema.name()).thenReturn(name);
+    when(mockSchema.comment()).thenReturn(comment);
+
+    return mockSchema;
+  }
+
+  /**
+   * Create a mock table object with the given name and comment.
+   *
+   * @param name The name of the table object
+   * @param comment The comment of the table object
+   * @return A mock table object.
+   */
+  public static Table getMockTable(String name, String comment) {
+    Table mockTable = mock(Table.class);
+    org.apache.gravitino.rel.Column mockColumnInt =
+        getMockColumn(
+            "id",
+            Types.IntegerType.get(),
+            "This is a int column",
+            false,
+            true,
+            DEFAULT_VALUE_NOT_SET);
+    org.apache.gravitino.rel.Column mockColumnString =
+        getMockColumn(
+            "name",
+            Types.StringType.get(),
+            "This is a string column",
+            true,
+            false,
+            DEFAULT_VALUE_NOT_SET);
+
+    when(mockTable.name()).thenReturn(name);
+    when(mockTable.comment()).thenReturn(comment);
+    when(mockTable.columns())
+        .thenReturn(new org.apache.gravitino.rel.Column[] {mockColumnInt, mockColumnString});
+
+    return mockTable;
+  }
+
+  /**
+   * Create a new mock column object with the given name, data type, comment, nullable,
+   * auto-increment, and default value.
+   *
+   * @param name The name of the column object
+   * @param dataType The data type of the column object
+   * @param comment The comment of the column object
+   * @param nullable Whether the column is nullable or not
+   * @param autoIncrement Whether the column is auto-increment or not
+   * @param defaultValue The default value of the column object
+   * @return A new mock column object
+   */
+  public static org.apache.gravitino.rel.Column getMockColumn(
+      String name,
+      Type dataType,
+      String comment,
+      boolean nullable,
+      boolean autoIncrement,
+      Expression defaultValue) {
+
+    org.apache.gravitino.rel.Column mockColumn = mock(org.apache.gravitino.rel.Column.class);
+    when(mockColumn.name()).thenReturn(name);
+    when(mockColumn.dataType()).thenReturn(dataType);
+    when(mockColumn.comment()).thenReturn(comment);
+    when(mockColumn.nullable()).thenReturn(nullable);
+    when(mockColumn.defaultValue()).thenReturn(defaultValue);
+    when(mockColumn.autoIncrement()).thenReturn(autoIncrement);
+
+    return mockColumn;
+  }
+
+  /**
+   * Create a mock model object with the given name, comment, and last version.
+   *
+   * @param name The name of the model object
+   * @param comment The comment of the model object
+   * @param lastVersion The last version of the model object
+   * @return A mock model object
+   */
+  public static 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;
+  }
+
+  /**
+   * Create a mock user object with the given name and roles.
+   *
+   * @param name The name of the user object
+   * @param roles The roles of the user object
+   * @return A mock user object
+   */
+  public static User getMockUser(String name, List<String> roles) {
+    User mockUser = mock(User.class);
+    when(mockUser.name()).thenReturn(name);
+    when(mockUser.roles()).thenReturn(roles);
+
+    return mockUser;
+  }
+
+  /**
+   * Create a mock group object with the given name and roles.
+   *
+   * @param name The name of the group object
+   * @param roles The roles belonging to the group object
+   * @return A mock group object
+   */
+  public static Group getMockGroup(String name, List<String> roles) {
+    Group mockGroup = mock(Group.class);
+    when(mockGroup.name()).thenReturn(name);
+    when(mockGroup.roles()).thenReturn(roles);
+
+    return mockGroup;
+  }
+
+  /**
+   * Create a mock Topic object with the given name and comment.
+   *
+   * @param name The name of the topic object
+   * @param comment The comment of the topic object
+   * @return A mock topic object
+   */
+  public static Topic getMockTopic(String name, String comment) {
+    Topic mockTopic = mock(Topic.class);
+    when(mockTopic.name()).thenReturn(name);
+    when(mockTopic.comment()).thenReturn(comment);
+
+    return mockTopic;
+  }
+
+  /**
+   * Create a mock Fileset object with the given name, type, comment, and location.
+   *
+   * @param name The name of the fileset object
+   * @param type The type of the fileset object
+   * @param comment The comment of the fileset object
+   * @param location The location of the fileset object
+   * @return A mock fileset object
+   */
+  public static Fileset getMockFileset(
+      String name, Fileset.Type type, String comment, String location) {
+    Fileset mockFileset = mock(Fileset.class);
+    when(mockFileset.name()).thenReturn(name);
+    when(mockFileset.type()).thenReturn(type);
+    when(mockFileset.comment()).thenReturn(comment);
+    when(mockFileset.storageLocation()).thenReturn(location);
+
+    return mockFileset;
+  }
+
+  /**
+   * Create a mock Role object with the given name and securable objects.
+   *
+   * @param name The name of the role object
+   * @return A mock role object
+   */
+  public static Role getMockRole(String name) {
+    Role mockRole = mock(Role.class);
+    ImmutableList<SecurableObject> securableObjects =
+        ImmutableList.of(
+            getMockSecurableObject("demo_table", MetadataObject.Type.TABLE),
+            getMockSecurableObject("demo_fileset", MetadataObject.Type.FILESET));
+
+    when(mockRole.name()).thenReturn(name);
+    when(mockRole.securableObjects()).thenReturn(securableObjects);
+
+    return mockRole;
+  }
+
+  /**
+   * Create a mock SecurableObject object with the given name and type.
+   *
+   * @param name The name of the securable object
+   * @param type The type of the securable object
+   * @return A mock securable object
+   */
+  public static SecurableObject getMockSecurableObject(String name, MetadataObject.Type type) {
+    SecurableObject mockObject = mock(SecurableObject.class);
+    when(mockObject.name()).thenReturn(name);
+    when(mockObject.type()).thenReturn(type);
+    if (type == MetadataObject.Type.TABLE) {
+      when(mockObject.privileges())
+          .thenReturn(
+              ImmutableList.of(Privileges.CreateTable.allow(), Privileges.SelectTable.allow()));
+    } else if (type == MetadataObject.Type.FILESET) {
+      when(mockObject.privileges())
+          .thenReturn(
+              ImmutableList.of(Privileges.CreateFileset.allow(), Privileges.ReadFileset.allow()));
+    } else {
+      throw new IllegalArgumentException("Unsupported type: " + type);
+    }
+
+    return mockObject;
+  }
+
+  /**
+   * Create a mock Tag object with the given name and comment.
+   *
+   * @param name The name of the tag object
+   * @param comment The comment of the tag object
+   * @return A mock tag object
+   */
+  public static Tag getMockTag(String name, String comment) {
+    return getMockTag(name, comment, ImmutableMap.of("k1", "v2", "k2", "v2"));
+  }
+
+  /**
+   * Create a mock Tag object with the given name, comment, and properties.
+   *
+   * @param name The name of the tag object
+   * @param comment The comment of the tag object
+   * @param properties The properties of the tag object
+   * @return A mock tag object
+   */
+  public static Tag getMockTag(String name, String comment, Map<String, String> properties) {
+    Tag mockTag = mock(Tag.class);
+    when(mockTag.name()).thenReturn(name);
+    when(mockTag.comment()).thenReturn(comment);
+    when(mockTag.properties()).thenReturn(properties);
+
+    return mockTag;
+  }
+}
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 7e4309391d5..5e45167f5fa 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,20 +27,21 @@
 import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
-import java.util.List;
-import java.util.Map;
 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.Role;
 import org.apache.gravitino.authorization.User;
 import org.apache.gravitino.cli.CommandContext;
+import org.apache.gravitino.cli.TestCliUtil;
 import org.apache.gravitino.cli.outputs.PlainFormat;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.messaging.Topic;
 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;
 import org.apache.gravitino.rel.expressions.literals.Literal;
 import org.apache.gravitino.rel.types.Type;
 import org.apache.gravitino.rel.types.Types;
@@ -188,7 +189,7 @@ void testListTableWithPlainFormat() {
   void testListColumnWithTableFormat() {
     CommandContext mockContext = getMockContext();
     org.apache.gravitino.rel.Column mockColumn1 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column1",
             Types.IntegerType.get(),
             "This is a int " + "column",
@@ -206,7 +207,7 @@ public Type dataType() {
               }
             });
     org.apache.gravitino.rel.Column mockColumn2 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column2",
             Types.StringType.get(),
             "This is a string " + "column",
@@ -238,10 +239,10 @@ public Type dataType() {
   void testListColumnWithTableFormatAndEmptyDefaultValues() {
     CommandContext mockContext = getMockContext();
     org.apache.gravitino.rel.Column mockColumn1 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column1", Types.IntegerType.get(), "", false, true, Column.DEFAULT_VALUE_NOT_SET);
     org.apache.gravitino.rel.Column mockColumn2 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column2",
             Types.StringType.get(),
             "",
@@ -262,7 +263,8 @@ void testListColumnWithTableFormatAndEmptyDefaultValues() {
   @Test
   void testTagDetailsWithPlainFormat() {
     CommandContext mockContext = getMockContext();
-    Tag mockTag = getMockTag("tag1", "comment for tag1", ImmutableMap.of("k1", "v1", "k2", "v2"));
+    Tag mockTag =
+        TestCliUtil.getMockTag("tag1", "comment for tag1", ImmutableMap.of("k1", "v1", "k2", "v2"));
 
     PlainFormat.output(mockTag, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -272,7 +274,7 @@ void testTagDetailsWithPlainFormat() {
   @Test
   void testTagDetailsWithPlainFormatWithNullValues() {
     CommandContext mockContext = getMockContext();
-    Tag mockTag = getMockTag("tag1", null);
+    Tag mockTag = TestCliUtil.getMockTag("tag1", null);
 
     PlainFormat.output(mockTag, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -283,9 +285,9 @@ void testTagDetailsWithPlainFormatWithNullValues() {
   void testListAllTagsWithPlainFormat() {
     CommandContext mockContext = getMockContext();
 
-    Tag mockTag1 = getMockTag("tag1", "comment for tag1");
-    Tag mockTag2 = getMockTag("tag2", "comment for tag2");
-    Tag mockTag3 = getMockTag("tag3", "comment for tag3");
+    Tag mockTag1 = TestCliUtil.getMockTag("tag1", "comment for tag1");
+    Tag mockTag2 = TestCliUtil.getMockTag("tag2", "comment for tag2");
+    Tag mockTag3 = TestCliUtil.getMockTag("tag3", "comment for tag3");
 
     PlainFormat.output(new Tag[] {mockTag1, mockTag2, mockTag3}, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -296,7 +298,8 @@ void testListAllTagsWithPlainFormat() {
   void testListTagPropertiesWithPlainFormat() {
     CommandContext mockContext = getMockContext();
 
-    Tag mockTag1 = getMockTag("tag1", "comment for tag1", ImmutableMap.of("k1", "v1", "k2", "v2"));
+    Tag mockTag1 =
+        TestCliUtil.getMockTag("tag1", "comment for tag1", ImmutableMap.of("k1", "v1", "k2", "v2"));
 
     PlainFormat.output(mockTag1.properties(), mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -367,7 +370,7 @@ private Table getMockTable() {
   private Table getMockTable(String name, String comment) {
     Table mockTable = mock(Table.class);
     org.apache.gravitino.rel.Column mockColumnInt =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "id",
             Types.IntegerType.get(),
             "This is a int column",
@@ -375,7 +378,7 @@ private Table getMockTable(String name, String comment) {
             true,
             Column.DEFAULT_VALUE_NOT_SET);
     org.apache.gravitino.rel.Column mockColumnString =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "name",
             Types.StringType.get(),
             "This is a string column",
@@ -394,7 +397,7 @@ private Table getMockTable(String name, String comment) {
   @Test
   void testModelDetailsWithPlainFormat() {
     CommandContext mockContext = getMockContext();
-    Model mockModel = getMockModel("demo_model", "This is a demo model", 1);
+    Model mockModel = TestCliUtil.getMockModel("demo_model", "This is a demo model", 1);
 
     PlainFormat.output(mockModel, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -404,9 +407,9 @@ void testModelDetailsWithPlainFormat() {
   @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);
+    Model model1 = TestCliUtil.getMockModel("model1", "This is a model", 1);
+    Model model2 = TestCliUtil.getMockModel("model2", "This is another model", 2);
+    Model model3 = TestCliUtil.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();
@@ -416,7 +419,7 @@ void testListModelWithPlainFormat() {
   @Test
   void testUserDetailsWithPlainFormat() {
     CommandContext mockContext = getMockContext();
-    User mockUser = getMockUser("demo_user", ImmutableList.of("admin", "user"));
+    User mockUser = TestCliUtil.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);
@@ -425,9 +428,9 @@ void testUserDetailsWithPlainFormat() {
   @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"));
+    User user1 = TestCliUtil.getMockUser("user1", ImmutableList.of("admin", "user"));
+    User user2 = TestCliUtil.getMockUser("user2", ImmutableList.of("admin"));
+    User user3 = TestCliUtil.getMockUser("user3", ImmutableList.of("user"));
 
     PlainFormat.output(new User[] {user1, user2, user3}, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -437,7 +440,8 @@ void testListUsersWithPlainFormat() {
   @Test
   void testGroupDetailsWithPlainFormat() {
     CommandContext mockContext = getMockContext();
-    Group mockGroup = getMockGroup("demo_group", ImmutableList.of("admin", "scientist"));
+    Group mockGroup =
+        TestCliUtil.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);
@@ -446,69 +450,103 @@ void testGroupDetailsWithPlainFormat() {
   @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"));
+    Group group1 = TestCliUtil.getMockGroup("group1", ImmutableList.of("admin", "user"));
+    Group group2 = TestCliUtil.getMockGroup("group2", ImmutableList.of("admin"));
+    Group group3 = TestCliUtil.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,
-      String comment,
-      boolean nullable,
-      boolean autoIncrement,
-      Expression defaultValue) {
-
-    org.apache.gravitino.rel.Column mockColumn = mock(org.apache.gravitino.rel.Column.class);
-    when(mockColumn.name()).thenReturn(name);
-    when(mockColumn.dataType()).thenReturn(dataType);
-    when(mockColumn.comment()).thenReturn(comment);
-    when(mockColumn.nullable()).thenReturn(nullable);
-    when(mockColumn.autoIncrement()).thenReturn(autoIncrement);
-    when(mockColumn.defaultValue()).thenReturn(defaultValue);
+  @Test
+  void testRoleDetailsWithPlainFormat() {
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Role admin = TestCliUtil.getMockRole("admin");
 
-    return mockColumn;
+    PlainFormat.output(admin, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "demo_table,TABLE,\n"
+            + "ALLOW create table\n"
+            + "ALLOW select table\n"
+            + "demo_fileset,FILESET,\n"
+            + "ALLOW create fileset\n"
+            + "ALLOW read fileset",
+        output);
   }
 
-  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);
+  @Test
+  void testListRolesWithPlainFormat() {
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Role admin = TestCliUtil.getMockRole("admin");
+    Role user = TestCliUtil.getMockRole("user");
 
-    return mockModel;
+    PlainFormat.output(new Role[] {admin, user}, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals("admin,user", output);
   }
 
-  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;
+  @Test
+  void testTopicDetailsWithPlainFormat() {
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Topic mockTopic = TestCliUtil.getMockTopic("demo_topic", "This is a demo topic");
+    PlainFormat.output(mockTopic, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals("demo_topic,This is a demo topic", output);
   }
 
-  private Group getMockGroup(String name, List<String> roles) {
-    Group mockGroup = mock(Group.class);
-    when(mockGroup.name()).thenReturn(name);
-    when(mockGroup.roles()).thenReturn(roles);
+  @Test
+  void testListTopicsWithPlainFormat() {
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Topic topic1 = TestCliUtil.getMockTopic("topic1", "This is a topic");
+    Topic topic2 = TestCliUtil.getMockTopic("topic2", "This is another topic");
+    Topic topic3 = TestCliUtil.getMockTopic("topic3", "This is a third topic");
 
-    return mockGroup;
+    PlainFormat.output(new Topic[] {topic1, topic2, topic3}, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals("topic1,topic2,topic3", output);
   }
 
-  private Tag getMockTag(String name, String comment) {
-    return getMockTag(name, comment, ImmutableMap.of("k1", "v2", "k2", "v2"));
+  @Test
+  void testFilesetDetailsWithPlainFormat() {
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Fileset mockFileset =
+        TestCliUtil.getMockFileset(
+            "demo_fileset",
+            Fileset.Type.MANAGED,
+            "This is a demo fileset",
+            "hdfs" + "://demo_location");
+    PlainFormat.output(mockFileset, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "demo_fileset,managed,This is a demo fileset,hdfs://demo_location", output);
   }
 
-  private Tag getMockTag(String name, String comment, Map<String, String> properties) {
-    Tag mockTag = mock(Tag.class);
-    when(mockTag.name()).thenReturn(name);
-    when(mockTag.comment()).thenReturn(comment);
-    when(mockTag.properties()).thenReturn(properties);
-
-    return mockTag;
+  @Test
+  void testListFilesetsWithPlainFormat() {
+    Fileset fileset1 =
+        TestCliUtil.getMockFileset(
+            "fileset1",
+            Fileset.Type.MANAGED,
+            "This is a demo fileset",
+            "hdfs" + "://demo_location");
+    Fileset fileset2 =
+        TestCliUtil.getMockFileset(
+            "fileset2",
+            Fileset.Type.EXTERNAL,
+            "This is another demo fileset",
+            "s3" + "://demo_location");
+    Fileset fileset3 =
+        TestCliUtil.getMockFileset(
+            "fileset3",
+            Fileset.Type.MANAGED,
+            "This is a third demo fileset",
+            "hdfs" + "://demo_location");
+
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    PlainFormat.output(new Fileset[] {fileset1, fileset2, fileset3}, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals("fileset1,fileset2,fileset3", 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
index fe6b97f8977..5b9cb880398 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
@@ -31,17 +31,19 @@
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.List;
-import java.util.Map;
 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.Role;
 import org.apache.gravitino.authorization.User;
 import org.apache.gravitino.cli.CommandContext;
+import org.apache.gravitino.cli.TestCliUtil;
 import org.apache.gravitino.cli.outputs.Column;
 import org.apache.gravitino.cli.outputs.TableFormat;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.messaging.Topic;
 import org.apache.gravitino.model.Model;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.rel.expressions.Expression;
@@ -54,7 +56,6 @@
 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.ImmutableMap;
 
 public class TestTableFormat {
   private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
@@ -76,7 +77,7 @@ public void restoreStreams() {
 
   @Test
   void testCreateDefaultTableFormat() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
 
     Column columnA = new Column(mockContext, "metalake");
     Column columnB = new Column(mockContext, "comment");
@@ -106,7 +107,7 @@ public String getOutput(String entity) {
 
   @Test
   void testTitleWithLeftAlign() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
 
     Column columnA =
         new Column(
@@ -141,7 +142,7 @@ public String getOutput(String entity) {
 
   @Test
   void testTitleWithRightAlign() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
 
     Column columnA =
         new Column(
@@ -176,7 +177,7 @@ public String getOutput(String entity) {
 
   @Test
   void testDataWithCenterAlign() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
 
     Column columnA =
         new Column(
@@ -211,7 +212,7 @@ public String getOutput(String entity) {
 
   @Test
   void testDataWithRightAlign() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
 
     Column columnA =
         new Column(
@@ -246,9 +247,9 @@ public String getOutput(String entity) {
 
   @Test
   void testMetalakeDetailsWithTableFormat() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
 
-    Metalake mockMetalake = getMockMetalake();
+    Metalake mockMetalake = TestCliUtil.getMockMetalake("demo_metalake", "This is a demo metalake");
     TableFormat.output(mockMetalake, mockContext);
 
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -263,9 +264,9 @@ void testMetalakeDetailsWithTableFormat() {
 
   @Test
   void testListMetalakeWithTableFormat() {
-    CommandContext mockContext = getMockContext();
-    Metalake mockMetalake1 = getMockMetalake("metalake1", "This is a metalake");
-    Metalake mockMetalake2 = getMockMetalake("metalake2", "This is another metalake");
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Metalake mockMetalake1 = TestCliUtil.getMockMetalake("metalake1", "This is a metalake");
+    Metalake mockMetalake2 = TestCliUtil.getMockMetalake("metalake2", "This is another metalake");
 
     TableFormat.output(new Metalake[] {mockMetalake1, mockMetalake2}, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -281,8 +282,10 @@ void testListMetalakeWithTableFormat() {
 
   @Test
   void testCatalogDetailsWithTableFormat() {
-    CommandContext mockContext = getMockContext();
-    Catalog mockCatalog = getMockCatalog();
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Catalog mockCatalog =
+        TestCliUtil.getMockCatalog(
+            "demo_catalog", Catalog.Type.RELATIONAL, "demo_provider", "This is a demo catalog");
 
     TableFormat.output(mockCatalog, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -297,12 +300,12 @@ void testCatalogDetailsWithTableFormat() {
 
   @Test
   void testListCatalogWithTableFormat() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
     Catalog mockCatalog1 =
-        getMockCatalog(
+        TestCliUtil.getMockCatalog(
             "catalog1", Catalog.Type.RELATIONAL, "demo_provider", "This is a demo catalog");
     Catalog mockCatalog2 =
-        getMockCatalog(
+        TestCliUtil.getMockCatalog(
             "catalog2", Catalog.Type.RELATIONAL, "demo_provider", "This is another demo catalog");
 
     TableFormat.output(new Catalog[] {mockCatalog1, mockCatalog2}, mockContext);
@@ -320,8 +323,8 @@ void testListCatalogWithTableFormat() {
 
   @Test
   void testSchemaDetailsWithTableFormat() {
-    CommandContext mockContext = getMockContext();
-    Schema mockSchema = getMockSchema();
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Schema mockSchema = TestCliUtil.getMockSchema("demo_schema", "This is a demo schema");
 
     TableFormat.output(mockSchema, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -336,9 +339,9 @@ void testSchemaDetailsWithTableFormat() {
 
   @Test
   void testListSchemaWithTableFormat() {
-    CommandContext mockContext = getMockContext();
-    Schema mockSchema1 = getMockSchema("demo_schema1", "This is a demo schema");
-    Schema mockSchema2 = getMockSchema("demo_schema2", "This is another demo schema");
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Schema mockSchema1 = TestCliUtil.getMockSchema("demo_schema1", "This is a demo schema");
+    Schema mockSchema2 = TestCliUtil.getMockSchema("demo_schema2", "This is another demo schema");
 
     TableFormat.output(new Schema[] {mockSchema1, mockSchema2}, mockContext);
 
@@ -355,8 +358,8 @@ void testListSchemaWithTableFormat() {
 
   @Test
   void testTableDetailsWithTableFormat() {
-    CommandContext mockContext = getMockContext();
-    Table mockTable = getMockTable();
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Table mockTable = TestCliUtil.getMockTable("demo_table", "This is a demo table");
 
     TableFormat.output(mockTable, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -372,10 +375,10 @@ void testTableDetailsWithTableFormat() {
 
   @Test
   void testListTableWithTableFormat() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
 
-    Table mockTable1 = getMockTable("table1", "This is a demo table");
-    Table mockTable2 = getMockTable("table2", "This is another demo table");
+    Table mockTable1 = TestCliUtil.getMockTable("table1", "This is a demo table");
+    Table mockTable2 = TestCliUtil.getMockTable("table2", "This is another demo table");
     TableFormat.output(new Table[] {mockTable1, mockTable2}, mockContext);
 
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -391,7 +394,7 @@ void testListTableWithTableFormat() {
 
   @Test
   void testAuditWithTableFormat() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
     Audit mockAudit = mock(Audit.class);
     when(mockAudit.creator()).thenReturn("demo_user");
     when(mockAudit.createTime()).thenReturn(Instant.ofEpochMilli(1611111111111L));
@@ -412,7 +415,7 @@ void testAuditWithTableFormat() {
 
   @Test
   void testAuditWithTableFormatWithNullValues() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
     Audit mockAudit = mock(Audit.class);
     when(mockAudit.creator()).thenReturn("demo_user");
     when(mockAudit.createTime()).thenReturn(null);
@@ -433,9 +436,9 @@ void testAuditWithTableFormatWithNullValues() {
 
   @Test
   void testListColumnWithTableFormat() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
     org.apache.gravitino.rel.Column mockColumn1 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column1",
             Types.IntegerType.get(),
             "This is a int column",
@@ -453,7 +456,7 @@ public Type dataType() {
               }
             });
     org.apache.gravitino.rel.Column mockColumn2 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column2",
             Types.StringType.get(),
             "This is a string column",
@@ -471,7 +474,7 @@ public Type dataType() {
               }
             });
     org.apache.gravitino.rel.Column mockColumn3 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column2",
             Types.StringType.get(),
             "This is a string column",
@@ -490,7 +493,7 @@ public Type dataType() {
             });
 
     org.apache.gravitino.rel.Column mockColumn4 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column2",
             Types.StringType.get(),
             "This is a string column",
@@ -499,7 +502,7 @@ public Type dataType() {
             FunctionExpression.of("current_timestamp"));
 
     org.apache.gravitino.rel.Column mockColumn5 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column2",
             Types.StringType.get(),
             "This is a string column",
@@ -528,9 +531,9 @@ public Type dataType() {
 
   @Test
   void testListColumnWithTableFormatAndEmptyDefaultValues() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
     org.apache.gravitino.rel.Column mockColumn1 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column1",
             Types.IntegerType.get(),
             "This is a int column",
@@ -538,7 +541,7 @@ void testListColumnWithTableFormatAndEmptyDefaultValues() {
             true,
             DEFAULT_VALUE_NOT_SET);
     org.apache.gravitino.rel.Column mockColumn2 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column2",
             Types.StringType.get(),
             "This is a string column",
@@ -546,7 +549,7 @@ void testListColumnWithTableFormatAndEmptyDefaultValues() {
             false,
             DEFAULT_VALUE_NOT_SET);
     org.apache.gravitino.rel.Column mockColumn3 =
-        getMockColumn(
+        TestCliUtil.getMockColumn(
             "column3",
             Types.BooleanType.get(),
             "this is a boolean column",
@@ -570,10 +573,10 @@ void testListColumnWithTableFormatAndEmptyDefaultValues() {
 
   @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);
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Model model1 = TestCliUtil.getMockModel("model1", "This is a demo model", 1);
+    Model model2 = TestCliUtil.getMockModel("model2", "This is another demo model", 2);
+    Model model3 = TestCliUtil.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();
@@ -590,8 +593,8 @@ void testListModelWithTableFormat() {
 
   @Test
   void testModelDetailsWithTableFormat() {
-    CommandContext mockContext = getMockContext();
-    Model mockModel = getMockModel("demo_model", "This is a demo model", 1);
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Model mockModel = TestCliUtil.getMockModel("demo_model", "This is a demo model", 1);
 
     TableFormat.output(mockModel, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -606,10 +609,10 @@ void testModelDetailsWithTableFormat() {
 
   @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"));
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    User user1 = TestCliUtil.getMockUser("user1", Arrays.asList("role1", "role2"));
+    User user2 = TestCliUtil.getMockUser("user2", Arrays.asList("role3", "role4"));
+    User user3 = TestCliUtil.getMockUser("user3", Arrays.asList("role5"));
 
     TableFormat.output(new User[] {user1, user2, user3}, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -626,8 +629,8 @@ void testListUserWithTableFormat() {
 
   @Test
   void testUserDetailsWithTableFormat() {
-    CommandContext mockContext = getMockContext();
-    User mockUser = getMockUser("demo_user", Arrays.asList("role1", "role2"));
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    User mockUser = TestCliUtil.getMockUser("demo_user", Arrays.asList("role1", "role2"));
 
     TableFormat.output(mockUser, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -642,10 +645,10 @@ void testUserDetailsWithTableFormat() {
 
   @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"));
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Group group1 = TestCliUtil.getMockGroup("group1", Arrays.asList("role1", "role2"));
+    Group group2 = TestCliUtil.getMockGroup("group2", Arrays.asList("role3", "role4"));
+    Group group3 = TestCliUtil.getMockGroup("group3", Arrays.asList("role5"));
 
     TableFormat.output(new Group[] {group1, group2, group3}, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -662,8 +665,8 @@ void testListGroupWithTableFormat() {
 
   @Test
   void testGroupDetailsWithTableFormat() {
-    CommandContext mockContext = getMockContext();
-    Group mockGroup = getMockGroup("demo_group", Arrays.asList("role1", "role2"));
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Group mockGroup = TestCliUtil.getMockGroup("demo_group", Arrays.asList("role1", "role2"));
 
     TableFormat.output(mockGroup, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -678,8 +681,8 @@ void testGroupDetailsWithTableFormat() {
 
   @Test
   void testTagDetailsWithTableFormat() {
-    CommandContext mockContext = getMockContext();
-    Tag mockTag = getMockTag("tag1", "comment for tag1");
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Tag mockTag = TestCliUtil.getMockTag("tag1", "comment for tag1");
 
     TableFormat.output(mockTag, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -694,7 +697,7 @@ void testTagDetailsWithTableFormat() {
 
   @Test
   void testTagDetailsWithTableFormatWithNullValues() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
     Tag mockTag = mock(Tag.class);
     when(mockTag.name()).thenReturn("tag1");
     when(mockTag.comment()).thenReturn(null);
@@ -712,11 +715,11 @@ void testTagDetailsWithTableFormatWithNullValues() {
 
   @Test
   void testListAllTagsWithTableFormat() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
 
-    Tag mockTag1 = getMockTag("tag1", "comment for tag1");
-    Tag mockTag2 = getMockTag("tag2", "comment for tag2");
-    Tag mockTag3 = getMockTag("tag3", "comment for tag3");
+    Tag mockTag1 = TestCliUtil.getMockTag("tag1", "comment for tag1");
+    Tag mockTag2 = TestCliUtil.getMockTag("tag2", "comment for tag2");
+    Tag mockTag3 = TestCliUtil.getMockTag("tag3", "comment for tag3");
 
     TableFormat.output(new Tag[] {mockTag1, mockTag2, mockTag3}, mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -733,9 +736,9 @@ void testListAllTagsWithTableFormat() {
 
   @Test
   void testListTagPropertiesWithTableFormat() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
 
-    Tag mockTag1 = getMockTag("tag1", "comment for tag1");
+    Tag mockTag1 = TestCliUtil.getMockTag("tag1", "comment for tag1");
 
     TableFormat.output(mockTag1.properties(), mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -743,7 +746,7 @@ void testListTagPropertiesWithTableFormat() {
         "+-----+-------+\n"
             + "| Key | Value |\n"
             + "+-----+-------+\n"
-            + "| k1  | v1    |\n"
+            + "| k1  | v2    |\n"
             + "| k2  | v2    |\n"
             + "+-----+-------+",
         output);
@@ -751,9 +754,9 @@ void testListTagPropertiesWithTableFormat() {
 
   @Test
   void testListTagPropertiesWithTableFormatWithEmptyMap() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
 
-    Tag mockTag1 = getMockTag("tag1", "comment for tag1", Collections.emptyMap());
+    Tag mockTag1 = TestCliUtil.getMockTag("tag1", "comment for tag1", Collections.emptyMap());
 
     TableFormat.output(mockTag1.properties(), mockContext);
     String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
@@ -768,7 +771,7 @@ void testListTagPropertiesWithTableFormatWithEmptyMap() {
 
   @Test
   void testOutputWithUnsupportType() {
-    CommandContext mockContext = getMockContext();
+    CommandContext mockContext = TestCliUtil.getMockContext();
     Object mockObject = new Object();
 
     assertThrows(
@@ -778,136 +781,133 @@ void testOutputWithUnsupportType() {
         });
   }
 
-  private CommandContext getMockContext() {
-    CommandContext mockContext = mock(CommandContext.class);
-
-    return mockContext;
-  }
-
-  private Metalake getMockMetalake() {
-    return getMockMetalake("demo_metalake", "This is a demo metalake");
-  }
-
-  private Metalake getMockMetalake(String name, String comment) {
-    Metalake mockMetalake = mock(Metalake.class);
-    when(mockMetalake.name()).thenReturn(name);
-    when(mockMetalake.comment()).thenReturn(comment);
-
-    return mockMetalake;
-  }
-
-  private Catalog getMockCatalog() {
-    return getMockCatalog(
-        "demo_catalog", Catalog.Type.RELATIONAL, "demo_provider", "This is a demo catalog");
-  }
-
-  private Catalog getMockCatalog(String name, Catalog.Type type, String provider, String comment) {
-    Catalog mockCatalog = mock(Catalog.class);
-    when(mockCatalog.name()).thenReturn(name);
-    when(mockCatalog.type()).thenReturn(type);
-    when(mockCatalog.provider()).thenReturn(provider);
-    when(mockCatalog.comment()).thenReturn(comment);
-
-    return mockCatalog;
-  }
-
-  private Schema getMockSchema() {
-    return getMockSchema("demo_schema", "This is a demo schema");
-  }
-
-  private Schema getMockSchema(String name, String comment) {
-    Schema mockSchema = mock(Schema.class);
-    when(mockSchema.name()).thenReturn(name);
-    when(mockSchema.comment()).thenReturn(comment);
-
-    return mockSchema;
-  }
-
-  private Table getMockTable() {
-    return getMockTable("demo_table", "This is a demo table");
-  }
-
-  private Table getMockTable(String name, String comment) {
-    Table mockTable = mock(Table.class);
-    org.apache.gravitino.rel.Column mockColumnInt =
-        getMockColumn(
-            "id",
-            Types.IntegerType.get(),
-            "This is a int column",
-            false,
-            true,
-            DEFAULT_VALUE_NOT_SET);
-    org.apache.gravitino.rel.Column mockColumnString =
-        getMockColumn(
-            "name",
-            Types.StringType.get(),
-            "This is a string column",
-            true,
-            false,
-            DEFAULT_VALUE_NOT_SET);
-
-    when(mockTable.name()).thenReturn(name);
-    when(mockTable.comment()).thenReturn(comment);
-    when(mockTable.columns())
-        .thenReturn(new org.apache.gravitino.rel.Column[] {mockColumnInt, mockColumnString});
+  @Test
+  void testRoleDetailsWithTableFormat() {
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Role admin = TestCliUtil.getMockRole("admin");
 
-    return mockTable;
+    TableFormat.output(admin, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+--------------+---------+----------------------+\n"
+            + "|     Name     |  Type   |      Privileges      |\n"
+            + "+--------------+---------+----------------------+\n"
+            + "| demo_table   | TABLE   | ALLOW create table   |\n"
+            + "|              |         | ALLOW select table   |\n"
+            + "| demo_fileset | FILESET | ALLOW create fileset |\n"
+            + "|              |         | ALLOW read fileset   |\n"
+            + "+--------------+---------+----------------------+",
+        output);
   }
 
-  private org.apache.gravitino.rel.Column getMockColumn(
-      String name,
-      Type dataType,
-      String comment,
-      boolean nullable,
-      boolean autoIncrement,
-      Expression defaultValue) {
-
-    org.apache.gravitino.rel.Column mockColumn = mock(org.apache.gravitino.rel.Column.class);
-    when(mockColumn.name()).thenReturn(name);
-    when(mockColumn.dataType()).thenReturn(dataType);
-    when(mockColumn.comment()).thenReturn(comment);
-    when(mockColumn.nullable()).thenReturn(nullable);
-    when(mockColumn.defaultValue()).thenReturn(defaultValue);
-    when(mockColumn.autoIncrement()).thenReturn(autoIncrement);
+  @Test
+  void testRoleListWithTableFormat() {
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Role admin = TestCliUtil.getMockRole("admin");
+    Role user = TestCliUtil.getMockRole("user");
+    Role guest = TestCliUtil.getMockRole("guest");
 
-    return mockColumn;
+    TableFormat.output(new Role[] {admin, user, guest}, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+-------+\n"
+            + "| Name  |\n"
+            + "+-------+\n"
+            + "| admin |\n"
+            + "| user  |\n"
+            + "| guest |\n"
+            + "+-------+",
+        output);
   }
 
-  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;
+  @Test
+  void testFilesetDetailsWithTableFormat() {
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Fileset mockFileset =
+        TestCliUtil.getMockFileset(
+            "demo_fileset",
+            Fileset.Type.MANAGED,
+            "This is a demo fileset",
+            "hdfs" + "://demo_location");
+
+    TableFormat.output(mockFileset, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+--------------+---------+------------------------+----------------------+\n"
+            + "|     Name     |  Type   |        Comment         |       Location       |\n"
+            + "+--------------+---------+------------------------+----------------------+\n"
+            + "| demo_fileset | managed | This is a demo fileset | hdfs://demo_location |\n"
+            + "+--------------+---------+------------------------+----------------------+",
+        output);
   }
 
-  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;
+  @Test
+  void testListFilesetWithTableFormat() {
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Fileset fileset1 =
+        TestCliUtil.getMockFileset(
+            "fileset1",
+            Fileset.Type.MANAGED,
+            "This is a demo fileset",
+            "hdfs" + "://demo_location");
+    Fileset fileset2 =
+        TestCliUtil.getMockFileset(
+            "fileset2",
+            Fileset.Type.EXTERNAL,
+            "This is another demo fileset",
+            "s3" + "://demo_location");
+    Fileset fileset3 =
+        TestCliUtil.getMockFileset(
+            "fileset3",
+            Fileset.Type.MANAGED,
+            "This is a third demo fileset",
+            "hdfs" + "://demo_location");
+    TableFormat.output(new Fileset[] {fileset1, fileset2, fileset3}, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+----------+\n"
+            + "|   Name   |\n"
+            + "+----------+\n"
+            + "| fileset1 |\n"
+            + "| fileset2 |\n"
+            + "| fileset3 |\n"
+            + "+----------+",
+        output);
   }
 
-  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;
-  }
+  @Test
+  void testTopicDetailsWithTableFormat() {
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Topic mockTopic = TestCliUtil.getMockTopic("demo_topic", "This is a demo topic");
 
-  private Tag getMockTag(String name, String comment) {
-    return getMockTag(name, comment, ImmutableMap.of("k1", "v1", "k2", "v2"));
+    TableFormat.output(mockTopic, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+------------+----------------------+\n"
+            + "|    Name    |       Comment        |\n"
+            + "+------------+----------------------+\n"
+            + "| demo_topic | This is a demo topic |\n"
+            + "+------------+----------------------+",
+        output);
   }
 
-  private Tag getMockTag(String name, String comment, Map<String, String> properties) {
-    Tag mockTag = mock(Tag.class);
-    when(mockTag.name()).thenReturn(name);
-    when(mockTag.comment()).thenReturn(comment);
-    when(mockTag.properties()).thenReturn(properties);
+  @Test
+  void testListTopicWithTableFormat() {
+    CommandContext mockContext = TestCliUtil.getMockContext();
+    Topic topic1 = TestCliUtil.getMockTopic("topic1", "This is a demo topic");
+    Topic topic2 = TestCliUtil.getMockTopic("topic2", "This is another demo topic");
+    Topic topic3 = TestCliUtil.getMockTopic("topic3", "This is a third demo topic");
 
-    return mockTag;
+    TableFormat.output(new Topic[] {topic1, topic2, topic3}, mockContext);
+    String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim();
+    Assertions.assertEquals(
+        "+--------+\n"
+            + "|  Name  |\n"
+            + "+--------+\n"
+            + "| topic1 |\n"
+            + "| topic2 |\n"
+            + "| topic3 |\n"
+            + "+--------+",
+        output);
   }
 }


Reviews:
user: login: justinmclean
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T09:24:06Z
user: login: justinmclean
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T09:26:02Z
user: login: Abyss-lord
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T09:27:12Z
user: login: Abyss-lord
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T09:38:42Z
user: login: unknowntpo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T11:42:06Z
user: login: unknowntpo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T11:46:55Z
user: login: unknowntpo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T11:47:08Z
user: login: Abyss-lord
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T04:10:59Z
user: login: justinmclean
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T04:47:51Z
user: login: Abyss-lord
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T09:33:40Z
user: login: justinmclean
body: Thanks for the work here.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-26T00:18:32Z

Review comments:
diff_hunk: @@ -46,52 +51,88 @@ public abstract class PlainFormat<T> extends BaseOutputFormat<T> {
    * @throws IllegalArgumentException if the object type is not supported
    */
   public static void output(Object entity, CommandContext context) {
+
     if (entity instanceof Metalake) {
       new MetalakePlainFormat(context).output((Metalake) entity);
+
     } else if (entity instanceof Metalake[]) {
       new MetalakeListPlainFormat(context).output((Metalake[]) entity);
+
     } else if (entity instanceof Catalog) {
       new CatalogPlainFormat(context).output((Catalog) entity);
+
     } else if (entity instanceof Catalog[]) {
       new CatalogListPlainFormat(context).output((Catalog[]) entity);
+
     } else if (entity instanceof Schema) {
       new SchemaPlainFormat(context).output((Schema) entity);
+
     } else if (entity instanceof Schema[]) {
       new SchemaListPlainFormat(context).output((Schema[]) entity);
+
     } else if (entity instanceof Table) {
       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);
+
path: clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java
user: login: justinmclean
body: Just curious why are new lines added here?
created_at: 2025-05-14T09:24:05Z
author_association: MEMBER
diff_hunk: @@ -78,50 +84,86 @@ public abstract class TableFormat<T> extends BaseOutputFormat<T> {
   public static void output(Object entity, CommandContext context) {
     if (entity instanceof Metalake) {
       new MetalakeTableFormat(context).output((Metalake) entity);
+
     } else if (entity instanceof Metalake[]) {
       new MetalakeListTableFormat(context).output((Metalake[]) entity);
+
     } else if (entity instanceof Catalog) {
       new CatalogTableFormat(context).output((Catalog) entity);
+
     } else if (entity instanceof Catalog[]) {
       new CatalogListTableFormat(context).output((Catalog[]) entity);
+
     } else if (entity instanceof Schema) {
       new SchemaTableFormat(context).output((Schema) entity);
+
     } else if (entity instanceof Schema[]) {
       new SchemaListTableFormat(context).output((Schema[]) entity);
+
     } else if (entity instanceof Table) {
       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);
+
path: clients/cli/src/main/java/org/apache/gravitino/cli/outputs/TableFormat.java
user: login: justinmclean
body: Same with the new lines here - any reason for adding them?
created_at: 2025-05-14T09:26:02Z
author_association: MEMBER
diff_hunk: @@ -46,52 +51,88 @@ public abstract class PlainFormat<T> extends BaseOutputFormat<T> {
    * @throws IllegalArgumentException if the object type is not supported
    */
   public static void output(Object entity, CommandContext context) {
+
     if (entity instanceof Metalake) {
       new MetalakePlainFormat(context).output((Metalake) entity);
+
     } else if (entity instanceof Metalake[]) {
       new MetalakeListPlainFormat(context).output((Metalake[]) entity);
+
     } else if (entity instanceof Catalog) {
       new CatalogPlainFormat(context).output((Catalog) entity);
+
     } else if (entity instanceof Catalog[]) {
       new CatalogListPlainFormat(context).output((Catalog[]) entity);
+
     } else if (entity instanceof Schema) {
       new SchemaPlainFormat(context).output((Schema) entity);
+
     } else if (entity instanceof Schema[]) {
       new SchemaListPlainFormat(context).output((Schema[]) entity);
+
     } else if (entity instanceof Table) {
       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);
+
path: clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java
user: login: Abyss-lord
body: @justinmclean Doing so will make the structure look clearer in the case of multiple of if-else structure. 
created_at: 2025-05-14T09:27:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,50 +84,86 @@ public abstract class TableFormat<T> extends BaseOutputFormat<T> {
   public static void output(Object entity, CommandContext context) {
     if (entity instanceof Metalake) {
       new MetalakeTableFormat(context).output((Metalake) entity);
+
     } else if (entity instanceof Metalake[]) {
       new MetalakeListTableFormat(context).output((Metalake[]) entity);
+
     } else if (entity instanceof Catalog) {
       new CatalogTableFormat(context).output((Catalog) entity);
+
     } else if (entity instanceof Catalog[]) {
       new CatalogListTableFormat(context).output((Catalog[]) entity);
+
     } else if (entity instanceof Schema) {
       new SchemaTableFormat(context).output((Schema) entity);
+
     } else if (entity instanceof Schema[]) {
       new SchemaListTableFormat(context).output((Schema[]) entity);
+
     } else if (entity instanceof Table) {
       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);
+
path: clients/cli/src/main/java/org/apache/gravitino/cli/outputs/TableFormat.java
user: login: Abyss-lord
body: @justinmclean Consistent with the above reasons.
created_at: 2025-05-14T09:38:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,333 @@
+/*
+ * 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;
+
+import static org.apache.gravitino.rel.Column.DEFAULT_VALUE_NOT_SET;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.Metalake;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.Privileges;
+import org.apache.gravitino.authorization.Role;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.authorization.User;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.messaging.Topic;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.expressions.Expression;
+import org.apache.gravitino.rel.types.Type;
+import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.tag.Tag;
+import org.testcontainers.shaded.com.google.common.collect.ImmutableList;
+
+public class TestCliUtil {
+  private TestCliUtil() {
+    // Utility class, no public constructor
+  }
path: clients/cli/src/test/java/org/apache/gravitino/cli/TestCliUtil.java
user: login: unknowntpo
body: I think this constructor is not needed.
created_at: 2025-05-14T11:28:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,333 @@
+/*
+ * 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;
+
+import static org.apache.gravitino.rel.Column.DEFAULT_VALUE_NOT_SET;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.Metalake;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.Privileges;
+import org.apache.gravitino.authorization.Role;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.authorization.User;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.messaging.Topic;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.expressions.Expression;
+import org.apache.gravitino.rel.types.Type;
+import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.tag.Tag;
+import org.testcontainers.shaded.com.google.common.collect.ImmutableList;
+
+public class TestCliUtil {
+  private TestCliUtil() {
+    // Utility class, no public constructor
+  }
path: clients/cli/src/test/java/org/apache/gravitino/cli/TestCliUtil.java
user: login: unknowntpo
body: Oh, this is a valid pattern to prevent others creating new instance on this util class.

[5. Using Private Constructors to Create Uninstantiable Classes](https://www.baeldung.com/java-private-constructors#using-private-constructors-to-create-uninstantiable-classes).

created_at: 2025-05-14T11:46:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,333 @@
+/*
+ * 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;
+
+import static org.apache.gravitino.rel.Column.DEFAULT_VALUE_NOT_SET;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.Metalake;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.Privileges;
+import org.apache.gravitino.authorization.Role;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.authorization.User;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.messaging.Topic;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.expressions.Expression;
+import org.apache.gravitino.rel.types.Type;
+import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.tag.Tag;
+import org.testcontainers.shaded.com.google.common.collect.ImmutableList;
+
+public class TestCliUtil {
+  private TestCliUtil() {
+    // Utility class, no public constructor
+  }
path: clients/cli/src/test/java/org/apache/gravitino/cli/TestCliUtil.java
user: login: unknowntpo
body: @Abyss-lord LGTM.
created_at: 2025-05-14T11:47:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,52 +51,88 @@ public abstract class PlainFormat<T> extends BaseOutputFormat<T> {
    * @throws IllegalArgumentException if the object type is not supported
    */
   public static void output(Object entity, CommandContext context) {
+
     if (entity instanceof Metalake) {
       new MetalakePlainFormat(context).output((Metalake) entity);
+
     } else if (entity instanceof Metalake[]) {
       new MetalakeListPlainFormat(context).output((Metalake[]) entity);
+
     } else if (entity instanceof Catalog) {
       new CatalogPlainFormat(context).output((Catalog) entity);
+
     } else if (entity instanceof Catalog[]) {
       new CatalogListPlainFormat(context).output((Catalog[]) entity);
+
     } else if (entity instanceof Schema) {
       new SchemaPlainFormat(context).output((Schema) entity);
+
     } else if (entity instanceof Schema[]) {
       new SchemaListPlainFormat(context).output((Schema[]) entity);
+
     } else if (entity instanceof Table) {
       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);
+
path: clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java
user: login: Abyss-lord
body: @justinmclean I think doing this way make code more clear, WDYT
![image](https://github.com/user-attachments/assets/e42f0645-6ed2-4217-950e-b9f87af414bc)

created_at: 2025-05-15T04:10:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,52 +51,88 @@ public abstract class PlainFormat<T> extends BaseOutputFormat<T> {
    * @throws IllegalArgumentException if the object type is not supported
    */
   public static void output(Object entity, CommandContext context) {
+
     if (entity instanceof Metalake) {
       new MetalakePlainFormat(context).output((Metalake) entity);
+
     } else if (entity instanceof Metalake[]) {
       new MetalakeListPlainFormat(context).output((Metalake[]) entity);
+
     } else if (entity instanceof Catalog) {
       new CatalogPlainFormat(context).output((Catalog) entity);
+
     } else if (entity instanceof Catalog[]) {
       new CatalogListPlainFormat(context).output((Catalog[]) entity);
+
     } else if (entity instanceof Schema) {
       new SchemaPlainFormat(context).output((Schema) entity);
+
     } else if (entity instanceof Schema[]) {
       new SchemaListPlainFormat(context).output((Schema[]) entity);
+
     } else if (entity instanceof Table) {
       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);
+
path: clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java
user: login: justinmclean
body: I would keep the same style as the rest of the codebase and not add teh extra blank lines
created_at: 2025-05-15T04:47:51Z
author_association: MEMBER
diff_hunk: @@ -46,52 +51,88 @@ public abstract class PlainFormat<T> extends BaseOutputFormat<T> {
    * @throws IllegalArgumentException if the object type is not supported
    */
   public static void output(Object entity, CommandContext context) {
+
     if (entity instanceof Metalake) {
       new MetalakePlainFormat(context).output((Metalake) entity);
+
     } else if (entity instanceof Metalake[]) {
       new MetalakeListPlainFormat(context).output((Metalake[]) entity);
+
     } else if (entity instanceof Catalog) {
       new CatalogPlainFormat(context).output((Catalog) entity);
+
     } else if (entity instanceof Catalog[]) {
       new CatalogListPlainFormat(context).output((Catalog[]) entity);
+
     } else if (entity instanceof Schema) {
       new SchemaPlainFormat(context).output((Schema) entity);
+
     } else if (entity instanceof Schema[]) {
       new SchemaListPlainFormat(context).output((Schema[]) entity);
+
     } else if (entity instanceof Table) {
       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);
+
path: clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java
user: login: Abyss-lord
body: @justinmclean fix, i already remove these blank lines.
created_at: 2025-05-23T09:33:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: Abyss-lord
created_at: 2025-05-07T12:43:45Z
body: Hi @justinmclean , could you please review this PR when you have time? I’d really appreciate your feedback.
author_association: CONTRIBUTOR
user: login: Abyss-lord
created_at: 2025-05-14T07:42:15Z
body: Hi @justinmclean @unknowntpo , I've completed the code updates and would appreciate your review of the PR when you have a moment. Here's a summary of the commits:
1. Resolve the conflicts.
author_association: CONTRIBUTOR
user: login: Abyss-lord
created_at: 2025-05-15T07:13:33Z
body: Hi @justinmclean , I've completed the code updates and would appreciate your review of the PR when you have a moment. Here's a summary of the commits:
1. Remove blank lines.
author_association: CONTRIBUTOR
user: login: Abyss-lord
created_at: 2025-05-22T15:27:57Z
body: Hi @justinmclean , I've completed the code updates and would appreciate your review of the PR when you have a moment. Here's a summary of the commits:
1. Fix some comments.
author_association: CONTRIBUTOR