You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by je...@apache.org on 2018/05/01 21:26:03 UTC

[geode] branch develop updated: GEODE-5010: Introduce *ResultModel objects to replace *ResultData (#1870)

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

jensdeppe pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git


The following commit(s) were added to refs/heads/develop by this push:
     new 6d13e8c  GEODE-5010: Introduce *ResultModel objects to replace *ResultData (#1870)
6d13e8c is described below

commit 6d13e8c203efcf10dcd8e585cc1a894fa8d48e27
Author: Jens Deppe <jd...@pivotal.io>
AuthorDate: Tue May 1 14:25:57 2018 -0700

    GEODE-5010: Introduce *ResultModel objects to replace *ResultData (#1870)
    
    
    - The changes here are intended to allow for support of both the old and new
      model for producing results. As such the interface structures are a mess but
      will be cleaned up once all commands have been converted.
    - Not all types of return values are supported but will be added as commands
      are converted. In particular commands that return actual result data in the
      form of files or file content.
    - Sub-sections are not supported anymore.
    - All commands should now return a ResultModel object
---
 .../internal/beans/MemberMBeanBridge.java          |  11 +-
 .../internal/cli/CliAroundInterceptor.java         |   2 +-
 .../management/internal/cli/CommandResponse.java   |  72 +-
 .../internal/cli/CommandResponseBuilder.java       |  22 +-
 .../cli/commands/AlterAsyncEventQueueCommand.java  |  30 +-
 .../cli/commands/DescribeClientCommand.java        |  69 +-
 .../cli/commands/DescribeConfigCommand.java        |  46 +-
 .../cli/commands/DescribeJndiBindingCommand.java   |  24 +-
 .../internal/cli/commands/ExportLogsCommand.java   |   4 +-
 .../internal/cli/commands/ListMembersCommand.java  |  15 +-
 .../internal/cli/commands/QueryInterceptor.java    |   5 +-
 .../internal/cli/remote/CommandStatementImpl.java  |   2 +-
 .../internal/cli/remote/MemberCommandService.java  |   2 +-
 .../cli/remote/OnlineCommandProcessor.java         |  13 +-
 .../internal/cli/result/CommandResult.java         | 588 ++------------
 ...CommandResult.java => LegacyCommandResult.java} | 182 ++---
 .../internal/cli/result/ModelCommandResult.java    | 357 +++++++++
 .../internal/cli/result/ResultBuilder.java         |  25 +-
 .../management/internal/cli/result/ResultData.java |  33 +-
 .../internal/cli/result/TabularResultData.java     |   2 +-
 .../cli/result/model/AbstractResultModel.java      |  53 ++
 .../DataResultModel.java}                          |  39 +-
 .../InfoResultModel.java}                          |  40 +-
 .../internal/cli/result/model/ResultModel.java     | 184 +++++
 .../TabularResultModel.java}                       |  49 +-
 .../internal/cli/shell/GfshExecutionStrategy.java  |  52 +-
 .../cli/commands/CreateRegionCommandTest.java      |   2 +-
 .../cli/commands/DescribeRegionDUnitTest.java      |   8 +-
 .../cli/commands/DescribeRegionJUnitTest.java      |  12 +-
 .../cli/commands/DiskStoreCommandsDUnitTest.java   |   2 +-
 .../internal/cli/commands/ExportLogsTestSuite.java |  37 -
 .../commands/ListJndiBindingCommandDUnitTest.java  |   2 +-
 .../cli/commands/ListMembersCommandDUnitTest.java  |  23 +-
 .../cli/commands/ListMembersCommandTest.java       |  28 +-
 .../cli/commands/LogLevelInterceptorTest.java      |   6 +-
 .../cli/remote/OnlineCommandProcessorTest.java     |   7 +-
 .../internal/cli/result/CommandResultTest.java     |  10 +-
 .../model/LegacyVsResultModelComparisonTest.java   | 204 +++++
 .../internal/security/MultiGfshDUnitTest.java      |  17 +-
 .../ShellCommandsControllerProcessCommandTest.java |   7 +-
 .../test/junit/assertions/CommandResultAssert.java |  12 +-
 .../geode/test/junit/rules/GfshCommandRule.java    |   1 -
 .../geode/test/junit/rules/GfshParserRule.java     |  21 +-
 .../commands/DescribeClientCommandDUnitTest.java   | 855 ++++-----------------
 .../internal/cli/commands/CommandOverHttpTest.java |   2 +-
 45 files changed, 1433 insertions(+), 1744 deletions(-)

diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/beans/MemberMBeanBridge.java b/geode-core/src/main/java/org/apache/geode/management/internal/beans/MemberMBeanBridge.java
index f8434d7..aa77434 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/beans/MemberMBeanBridge.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/beans/MemberMBeanBridge.java
@@ -98,7 +98,6 @@ import org.apache.geode.internal.tcp.ConnectionTable;
 import org.apache.geode.management.GemFireProperties;
 import org.apache.geode.management.JVMMetrics;
 import org.apache.geode.management.OSMetrics;
-import org.apache.geode.management.cli.Result;
 import org.apache.geode.management.internal.ManagementConstants;
 import org.apache.geode.management.internal.ManagementStrings;
 import org.apache.geode.management.internal.SystemManagementService;
@@ -115,6 +114,7 @@ import org.apache.geode.management.internal.beans.stats.VMStatsMonitor;
 import org.apache.geode.management.internal.cli.CommandResponseBuilder;
 import org.apache.geode.management.internal.cli.remote.OnlineCommandProcessor;
 import org.apache.geode.management.internal.cli.result.CommandResult;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
 
 /**
  * This class acts as an Bridge between MemberMBean and GemFire Cache and Distributed System
@@ -1511,8 +1511,13 @@ public class MemberMBeanBridge {
               + commandServiceInitError);
     }
 
-    Result result = commandProcessor.executeCommand(commandString, env, stagedFilePaths);
-    return CommandResponseBuilder.createCommandResponseJson(getMember(), (CommandResult) result);
+    Object result = commandProcessor.executeCommand(commandString, env, stagedFilePaths);
+
+    if (result instanceof CommandResult) {
+      return CommandResponseBuilder.createCommandResponseJson(getMember(), (CommandResult) result);
+    } else {
+      return CommandResponseBuilder.createCommandResponseJson(getMember(), (ResultModel) result);
+    }
   }
 
   public long getTotalDiskUsage() {
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/CliAroundInterceptor.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/CliAroundInterceptor.java
index 880ba6a..ccd60a0 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/CliAroundInterceptor.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/CliAroundInterceptor.java
@@ -32,7 +32,7 @@ public interface CliAroundInterceptor {
   /**
    * called by the OperationInvoker before the command is executed
    */
-  default Result preExecution(GfshParseResult parseResult) {
+  default Object preExecution(GfshParseResult parseResult) {
     return ResultBuilder.createInfoResult("");
   }
 
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/CommandResponse.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/CommandResponse.java
index a6db280..83744a5 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/CommandResponse.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/CommandResponse.java
@@ -19,6 +19,7 @@ import java.text.DateFormat;
 
 import org.apache.geode.internal.GemFireVersion;
 import org.apache.geode.management.internal.cli.json.GfJsonObject;
+import org.apache.geode.management.internal.cli.result.ResultData;
 
 /**
  * @since GemFire 7.0
@@ -33,16 +34,10 @@ public class CommandResponse {
   private final String when;
   private final String tokenAccessor;
   private final String debugInfo;
-  private final Data data;
+  private final ResultData data;
   private final boolean failedToPersist;
   private final String fileToDownload;
-
-  CommandResponse(String sender, String contentType, int status, String page, String tokenAccessor,
-      String debugInfo, String header, GfJsonObject content, String footer,
-      boolean failedToPersist) {
-    this(sender, contentType, status, page, tokenAccessor, debugInfo, header, content, footer,
-        failedToPersist, null);
-  }
+  private final boolean isLegacy;
 
   CommandResponse(String sender, String contentType, int status, String page, String tokenAccessor,
       String debugInfo, String header, GfJsonObject content, String footer, boolean failedToPersist,
@@ -53,7 +48,7 @@ public class CommandResponse {
     this.page = page;
     this.tokenAccessor = tokenAccessor;
     this.debugInfo = debugInfo;
-    this.data = new Data(header, content, footer);
+    this.data = new LegacyData(header, content, footer);
     this.when = DateFormat.getInstance().format(new java.util.Date());
     this.version = GemFireVersion.getGemFireVersion();
     this.failedToPersist = failedToPersist;
@@ -62,6 +57,7 @@ public class CommandResponse {
     } else {
       this.fileToDownload = null;
     }
+    this.isLegacy = true;
   }
 
   // For de-serializing
@@ -72,11 +68,12 @@ public class CommandResponse {
     this.page = jsonObject.getString("page");
     this.tokenAccessor = jsonObject.getString("tokenAccessor");
     this.debugInfo = jsonObject.getString("debugInfo");
-    this.data = new Data(jsonObject.getJSONObject("data"));
+    this.data = new LegacyData(jsonObject.getJSONObject("data"));
     this.when = jsonObject.getString("when");
     this.version = jsonObject.getString("version");
     this.failedToPersist = jsonObject.getBoolean("failedToPersist");
     this.fileToDownload = jsonObject.getString("fileToDownload");
+    this.isLegacy = true;
   }
 
   /**
@@ -135,7 +132,7 @@ public class CommandResponse {
   /**
    * @return the data
    */
-  public Data getData() {
+  public ResultData getData() {
     return data;
   }
 
@@ -150,18 +147,22 @@ public class CommandResponse {
     return failedToPersist;
   }
 
-  public static class Data {
+  public boolean isLegacy() {
+    return isLegacy;
+  }
+
+  public static class LegacyData implements ResultData {
     private String header;
     private GfJsonObject content;
     private String footer;
 
-    public Data(String header, GfJsonObject content, String footer) {
+    public LegacyData(String header, GfJsonObject content, String footer) {
       this.header = header;
       this.content = content;
       this.footer = footer;
     }
 
-    public Data(GfJsonObject dataJsonObject) {
+    public LegacyData(GfJsonObject dataJsonObject) {
       this.header = dataJsonObject.getString("header");
       this.content = dataJsonObject.getJSONObject("content");
       this.footer = dataJsonObject.getString("footer");
@@ -195,46 +196,5 @@ public class CommandResponse {
       return builder.toString();
     }
   }
-}
-
 
-/*
- ** TABLE
- *
- * { "sender": "member1", "version": "gemfire70", "contentType": "table", "page": "1/1",
- * "tokenAccessor": "__NULL__", "status": "OK", "when": "January 12 2012", "debugData": [ "val1",
- * "val2" ], "data": { "header": [ "Header1", "Header2", "Header3", "Header4" ], "content": [ [
- * "val00", "val01", "val02", "val03" ], [ "val10", "val11", "val12", "val13" ], [ "val20", "val21",
- * "val22", "val23" ] ] } }
- **
- * TABLE SCROLLABLE
- *
- * { "sender": "member1", "version": "gemfire70", "contentType": "table", "page": "1/5",
- * "tokenHolder": "TOKEN12345", "status": "OK", "when": "January 12 2012", "debugData": [ "val1",
- * "val2" ], "data": { "header": [ "Header1", "Header2", "Header3", "Header4" ], "content": [ [
- * "val00", "val01", "val02", "val03" ], [ "val10", "val11", "val12", "val13" ], [ "val20", "val21",
- * "val22", "val23" ] ] } }
- **
- *
- * CATALOG
- *
- * { "sender": "member1", "version": "gemfire70", "contentType": "catalog", "page": "1/1",
- * "tokenHolder": "__NULL__", "status": "OK", "when": "January 12 2012", "debugData": [ "val1",
- * "val2" ], "data": { "content": [ { "key1": "val1", "key2": "val2", "key3": "val3", "key4":
- * "val4", "key5": "val5", "key6": "val6", "key7": "val7" } ] } }
- **
- *
- * CATALOG SCROLLABLE
- *
- * { "sender": "member1", "version": "gemfire70", "contentType": "catalog", "page": "1/10",
- * "tokenHolder": "TOKEN1265765", "status": "OK", "when": "January 12 2012", "debugData": [ "val1",
- * "val2" ], "data": { "content": [ { "key1": "val1", "key2": "val2", "key3": "val3", "key4":
- * "val4", "key5": "val5", "key6": "val6", "key7": "val7" } ] } }
- **
- *
- * Object as argument
- *
- * { "com.foo.bar.Employee": { "id": 1234, "name": "Foo BAR", "department": { "id": 456, "name":
- * "support" } } }
- *
- */
+}
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/CommandResponseBuilder.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/CommandResponseBuilder.java
index aa6c011..36249e8 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/CommandResponseBuilder.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/CommandResponseBuilder.java
@@ -14,6 +14,8 @@
  */
 package org.apache.geode.management.internal.cli;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.commons.lang.exception.ExceptionUtils;
 
 import org.apache.geode.management.cli.CliMetaData;
@@ -21,6 +23,7 @@ import org.apache.geode.management.internal.cli.json.GfJsonException;
 import org.apache.geode.management.internal.cli.json.GfJsonObject;
 import org.apache.geode.management.internal.cli.remote.CommandExecutionContext;
 import org.apache.geode.management.internal.cli.result.CommandResult;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
 
 /**
  *
@@ -28,11 +31,11 @@ import org.apache.geode.management.internal.cli.result.CommandResult;
  */
 public class CommandResponseBuilder {
 
-  public static CommandResponse prepareCommandResponse(String memberName, CommandResult result) {
+  private static CommandResponse prepareCommandResponse(String memberName, CommandResult result) {
     GfJsonObject content;
     content = result.getContent();
     return new CommandResponse(memberName, getType(result), result.getStatus().getCode(), "1/1",
-        CliMetaData.ANNOTATION_NULL_VALUE, getDebugInfo(result), result.getHeader(), content,
+        CliMetaData.ANNOTATION_NULL_VALUE, getDebugInfo(), result.getHeader(), content,
         result.getFooter(), result.failedToPersist(), result.getFileToDownload());
   }
 
@@ -47,7 +50,7 @@ public class CommandResponseBuilder {
     return new CommandResponse(jsonObject);
   }
 
-  public static String getCommandResponseJson(CommandResponse commandResponse) {
+  private static String getCommandResponseJson(CommandResponse commandResponse) {
     return new GfJsonObject(commandResponse).toString();
   }
 
@@ -55,11 +58,22 @@ public class CommandResponseBuilder {
     return getCommandResponseJson(prepareCommandResponse(memberName, result));
   }
 
+  public static String createCommandResponseJson(String memberName, ResultModel result) {
+    ObjectMapper mapper = new ObjectMapper();
+
+    try {
+      String json = mapper.writeValueAsString(result);
+      return json;
+    } catch (JsonProcessingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   private static String getType(CommandResult result) {
     return result.getType();
   }
 
-  private static String getDebugInfo(CommandResult result) {
+  private static String getDebugInfo() {
     String debugInfo = "";
     if (CommandExecutionContext.isSetWrapperThreadLocal()) {
       CommandResponseWriter responseWriter = CommandExecutionContext.getCommandResponseWriter();
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/AlterAsyncEventQueueCommand.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/AlterAsyncEventQueueCommand.java
index d23fec2..ffdaa72 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/AlterAsyncEventQueueCommand.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/AlterAsyncEventQueueCommand.java
@@ -36,12 +36,11 @@ import org.xml.sax.SAXException;
 import org.apache.geode.cache.Region;
 import org.apache.geode.distributed.internal.InternalClusterConfigurationService;
 import org.apache.geode.management.cli.CliMetaData;
-import org.apache.geode.management.cli.Result;
 import org.apache.geode.management.internal.cli.AbstractCliAroundInterceptor;
 import org.apache.geode.management.internal.cli.GfshParseResult;
 import org.apache.geode.management.internal.cli.exceptions.EntityNotFoundException;
-import org.apache.geode.management.internal.cli.result.ResultBuilder;
-import org.apache.geode.management.internal.cli.result.TabularResultData;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.cli.result.model.TabularResultModel;
 import org.apache.geode.management.internal.configuration.domain.Configuration;
 import org.apache.geode.management.internal.configuration.utils.XmlUtils;
 import org.apache.geode.management.internal.security.ResourceOperation;
@@ -74,7 +73,7 @@ public class AlterAsyncEventQueueCommand extends InternalGfshCommand {
       interceptor = "org.apache.geode.management.internal.cli.commands.AlterAsyncEventQueueCommand$Interceptor")
   @ResourceOperation(resource = ResourcePermission.Resource.CLUSTER,
       operation = ResourcePermission.Operation.MANAGE, target = ResourcePermission.Target.DEPLOY)
-  public Result execute(@CliOption(key = ID, mandatory = true, help = ID_HELP) String id,
+  public ResultModel execute(@CliOption(key = ID, mandatory = true, help = ID_HELP) String id,
       @CliOption(key = BATCH_SIZE, help = BATCH_SIZE_HELP) Integer batchSize,
       @CliOption(key = BATCH_TIME_INTERVAL,
           help = BATCH_TIME_INTERVAL_HELP) Integer batchTimeInterval,
@@ -83,22 +82,25 @@ public class AlterAsyncEventQueueCommand extends InternalGfshCommand {
           unspecifiedDefaultValue = "false") boolean ifExists)
       throws IOException, SAXException, ParserConfigurationException, TransformerException {
 
+    ResultModel result = new ResultModel();
+
     // need not check if any running servers has this async-event-queue. A server with this queue id
     // may be shutdown, but we still need to update Cluster Configuration.
     InternalClusterConfigurationService service =
         (InternalClusterConfigurationService) getConfigurationService();
 
     if (service == null) {
-      return ResultBuilder.createUserErrorResult("Cluster Configuration Service is not available. "
+      return result.createError("Cluster Configuration Service is not available. "
           + "Please connect to a locator with running Cluster Configuration Service.");
     }
 
     boolean locked = service.lockSharedConfiguration();
     if (!locked) {
-      return ResultBuilder.createGemFireErrorResult("Unable to lock the cluster configuration.");
+      return result.createCommandProcessingError("Unable to lock the cluster configuration.");
     }
 
-    TabularResultData tableData = ResultBuilder.createTabularResultData();
+    TabularResultModel tableData = result.addTable();
+    boolean xmlUpdated = false;
     try {
       Region<String, Configuration> configRegion = service.getConfigurationRegion();
       for (String group : configRegion.keySet()) {
@@ -108,7 +110,6 @@ public class AlterAsyncEventQueueCommand extends InternalGfshCommand {
           continue;
         }
 
-        boolean xmlUpdated = false;
         Document document = XmlUtils.createDocumentFromXml(config.getCacheXmlContent());
         NodeList nodeList = document.getElementsByTagName("async-event-queue");
         for (int i = 0; i < nodeList.getLength(); i++) {
@@ -145,7 +146,7 @@ public class AlterAsyncEventQueueCommand extends InternalGfshCommand {
       service.unlockSharedConfiguration();
     }
 
-    if (tableData.rowSize("Group") == 0) {
+    if (!xmlUpdated) {
       String message = String.format("Can not find an async event queue with id '%s'.", id);
       throw new EntityNotFoundException(message, ifExists);
     }
@@ -154,21 +155,22 @@ public class AlterAsyncEventQueueCommand extends InternalGfshCommand {
     tableData.setFooter(System.lineSeparator()
         + "These changes won't take effect on the running servers. " + System.lineSeparator()
         + "Please restart the servers in these groups for the changes to take effect.");
-    return ResultBuilder.buildResult(tableData);
+
+    return result;
   }
 
   public static class Interceptor extends AbstractCliAroundInterceptor {
     @Override
-    public Result preExecution(GfshParseResult parseResult) {
+    public ResultModel preExecution(GfshParseResult parseResult) {
       Object batchSize = parseResult.getParamValue(BATCH_SIZE);
       Object batchTimeInterval = parseResult.getParamValue(BATCH_TIME_INTERVAL);
       Object maxQueueMemory = parseResult.getParamValue(MAX_QUEUE_MEMORY);
 
+      ResultModel resultModel = new ResultModel();
       if (batchSize == null && batchTimeInterval == null && maxQueueMemory == null) {
-        return ResultBuilder
-            .createUserErrorResult("need to specify at least one option to modify.");
+        resultModel.createError("need to specify at least one option to modify.");
       }
-      return ResultBuilder.createInfoResult("");
+      return resultModel;
     }
   }
 }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeClientCommand.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeClientCommand.java
index f14a992..66bc4aa 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeClientCommand.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeClientCommand.java
@@ -32,14 +32,13 @@ import org.apache.geode.management.CacheServerMXBean;
 import org.apache.geode.management.ClientHealthStatus;
 import org.apache.geode.management.ManagementService;
 import org.apache.geode.management.cli.CliMetaData;
-import org.apache.geode.management.cli.Result;
 import org.apache.geode.management.internal.cli.CliUtil;
 import org.apache.geode.management.internal.cli.LogWrapper;
 import org.apache.geode.management.internal.cli.functions.ContinuousQueryFunction;
 import org.apache.geode.management.internal.cli.i18n.CliStrings;
-import org.apache.geode.management.internal.cli.result.CompositeResultData;
-import org.apache.geode.management.internal.cli.result.ResultBuilder;
-import org.apache.geode.management.internal.cli.result.TabularResultData;
+import org.apache.geode.management.internal.cli.result.model.DataResultModel;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.cli.result.model.TabularResultModel;
 import org.apache.geode.management.internal.security.ResourceOperation;
 import org.apache.geode.security.ResourcePermission;
 
@@ -48,9 +47,10 @@ public class DescribeClientCommand extends InternalGfshCommand {
   @CliMetaData(relatedTopic = {CliStrings.TOPIC_CLIENT})
   @ResourceOperation(resource = ResourcePermission.Resource.CLUSTER,
       operation = ResourcePermission.Operation.READ)
-  public Result describeClient(@CliOption(key = CliStrings.DESCRIBE_CLIENT__ID, mandatory = true,
-      help = CliStrings.DESCRIBE_CLIENT__ID__HELP) String clientId) throws Exception {
-    Result result;
+  public ResultModel describeClient(@CliOption(key = CliStrings.DESCRIBE_CLIENT__ID,
+      mandatory = true, help = CliStrings.DESCRIBE_CLIENT__ID__HELP) String clientId)
+      throws Exception {
+    ResultModel result = new ResultModel();
 
     if (clientId.startsWith("\"")) {
       clientId = clientId.substring(1);
@@ -64,14 +64,10 @@ public class DescribeClientCommand extends InternalGfshCommand {
       clientId = clientId.substring(0, clientId.length() - 2);
     }
 
-    CompositeResultData compositeResultData = ResultBuilder.createCompositeResultData();
-    CompositeResultData.SectionResultData sectionResult =
-        compositeResultData.addSection("InfoSection");
-
     ManagementService service = getManagementService();
     ObjectName[] cacheServers = service.getDistributedSystemMXBean().listCacheServerObjectNames();
     if (cacheServers.length == 0) {
-      return ResultBuilder.createGemFireErrorResult(
+      return result.createCommandProcessingError(
           CliStrings.format(CliStrings.DESCRIBE_CLIENT_COULD_NOT_RETRIEVE_SERVER_LIST));
     }
 
@@ -86,11 +82,11 @@ public class DescribeClientCommand extends InternalGfshCommand {
           try {
             clientHealthStatus = serverMbean.showClientStats(clientId);
             if (clientHealthStatus == null) {
-              return ResultBuilder.createGemFireErrorResult(CliStrings.format(
+              return result.createCommandProcessingError(CliStrings.format(
                   CliStrings.DESCRIBE_CLIENT_COULD_NOT_RETRIEVE_STATS_FOR_CLIENT_0, clientId));
             }
           } catch (Exception eee) {
-            return ResultBuilder.createGemFireErrorResult(CliStrings.format(
+            return result.createCommandProcessingError(CliStrings.format(
                 CliStrings.DESCRIBE_CLIENT_COULD_NOT_RETRIEVE_STATS_FOR_CLIENT_0_REASON_1, clientId,
                 eee.getMessage()));
           }
@@ -99,7 +95,7 @@ public class DescribeClientCommand extends InternalGfshCommand {
     }
 
     if (clientHealthStatus == null) {
-      return ResultBuilder.createGemFireErrorResult(
+      return result.createCommandProcessingError(
           CliStrings.format(CliStrings.DESCRIBE_CLIENT__CLIENT__ID__NOT__FOUND__0, clientId));
     }
 
@@ -147,20 +143,17 @@ public class DescribeClientCommand extends InternalGfshCommand {
         }
       }
 
-      buildTableResult(sectionResult, clientHealthStatus, isDurable, primaryServers,
-          secondaryServers);
-      result = ResultBuilder.buildResult(compositeResultData);
+      buildTableResult(result, clientHealthStatus, isDurable, primaryServers, secondaryServers);
     } else {
-      return ResultBuilder.createGemFireErrorResult(CliStrings.DESCRIBE_CLIENT_NO_MEMBERS);
+      result.createCommandProcessingError(CliStrings.DESCRIBE_CLIENT_NO_MEMBERS);
     }
 
     LogWrapper.getInstance(getCache()).info("describe client result " + result);
     return result;
   }
 
-  private void buildTableResult(CompositeResultData.SectionResultData sectionResult,
-      ClientHealthStatus clientHealthStatus, String isDurable, List<String> primaryServers,
-      List<String> secondaryServers) {
+  private void buildTableResult(ResultModel result, ClientHealthStatus clientHealthStatus,
+      String isDurable, List<String> primaryServers, List<String> secondaryServers) {
 
     StringBuilder primServers = new StringBuilder();
     for (String primaryServer : primaryServers) {
@@ -171,36 +164,36 @@ public class DescribeClientCommand extends InternalGfshCommand {
     for (String secondServer : secondaryServers) {
       secondServers.append(secondServer);
     }
+
+    DataResultModel dataSection = result.addData("InfoSection");
     if (clientHealthStatus != null) {
-      sectionResult.addSeparator('-');
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_PRIMARY_SERVERS, primServers);
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_SECONDARY_SERVERS, secondServers);
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_CPU, clientHealthStatus.getCpus());
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_LISTENER_CALLS,
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_PRIMARY_SERVERS, primServers);
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_SECONDARY_SERVERS, secondServers);
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_CPU, clientHealthStatus.getCpus());
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_LISTENER_CALLS,
           clientHealthStatus.getNumOfCacheListenerCalls());
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_GETS,
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_GETS,
           clientHealthStatus.getNumOfGets());
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_MISSES,
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_MISSES,
           clientHealthStatus.getNumOfMisses());
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_PUTS,
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_PUTS,
           clientHealthStatus.getNumOfPuts());
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_THREADS,
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_THREADS,
           clientHealthStatus.getNumOfThreads());
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_PROCESS_CPU_TIME,
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_PROCESS_CPU_TIME,
           clientHealthStatus.getProcessCpuTime());
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_QUEUE_SIZE,
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_QUEUE_SIZE,
           clientHealthStatus.getQueueSize());
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_UP_TIME,
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_UP_TIME,
           clientHealthStatus.getUpTime());
-      sectionResult.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_DURABLE, isDurable);
-      sectionResult.addSeparator('-');
+      dataSection.addData(CliStrings.DESCRIBE_CLIENT_COLUMN_DURABLE, isDurable);
 
       Map<String, String> poolStats = clientHealthStatus.getPoolStats();
 
       if (poolStats.size() > 0) {
         for (Map.Entry<String, String> entry : poolStats.entrySet()) {
-          TabularResultData poolStatsResultTable =
-              sectionResult.addTable("Pool Stats For Pool Name = " + entry.getKey());
+          TabularResultModel poolStatsResultTable =
+              result.addTable("Pool Stats For Pool Name = " + entry.getKey());
           poolStatsResultTable.setHeader("Pool Stats For Pool Name = " + entry.getKey());
           String poolStatsStr = entry.getValue();
           String str[] = poolStatsStr.split(";");
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeConfigCommand.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeConfigCommand.java
index 2029584..8d31bd4 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeConfigCommand.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeConfigCommand.java
@@ -33,10 +33,9 @@ import org.apache.geode.management.cli.Result;
 import org.apache.geode.management.internal.cli.domain.MemberConfigurationInfo;
 import org.apache.geode.management.internal.cli.functions.GetMemberConfigInformationFunction;
 import org.apache.geode.management.internal.cli.i18n.CliStrings;
-import org.apache.geode.management.internal.cli.result.CompositeResultData;
-import org.apache.geode.management.internal.cli.result.ErrorResultData;
-import org.apache.geode.management.internal.cli.result.ResultBuilder;
-import org.apache.geode.management.internal.cli.result.TabularResultData;
+import org.apache.geode.management.internal.cli.result.model.DataResultModel;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.cli.result.model.TabularResultModel;
 import org.apache.geode.management.internal.security.ResourceOperation;
 import org.apache.geode.security.ResourcePermission;
 
@@ -48,14 +47,14 @@ public class DescribeConfigCommand extends InternalGfshCommand {
   @CliMetaData(relatedTopic = {CliStrings.TOPIC_GEODE_CONFIG})
   @ResourceOperation(resource = ResourcePermission.Resource.CLUSTER,
       operation = ResourcePermission.Operation.READ)
-  public Result describeConfig(
+  public ResultModel describeConfig(
       @CliOption(key = CliStrings.MEMBER, optionContext = ConverterHint.ALL_MEMBER_IDNAME,
           help = CliStrings.DESCRIBE_CONFIG__MEMBER__HELP, mandatory = true) String memberNameOrId,
       @CliOption(key = CliStrings.DESCRIBE_CONFIG__HIDE__DEFAULTS,
           help = CliStrings.DESCRIBE_CONFIG__HIDE__DEFAULTS__HELP, unspecifiedDefaultValue = "true",
           specifiedDefaultValue = "true") boolean hideDefaults) {
 
-    Result result = null;
+    ResultModel result = new ResultModel();
     try {
       DistributedMember targetMember = null;
 
@@ -71,11 +70,11 @@ public class DescribeConfigCommand extends InternalGfshCommand {
       if (obj != null && obj instanceof MemberConfigurationInfo) {
         MemberConfigurationInfo memberConfigInfo = (MemberConfigurationInfo) obj;
 
-        CompositeResultData crd = ResultBuilder.createCompositeResultData();
-        crd.setHeader(CliStrings.format(CliStrings.DESCRIBE_CONFIG__HEADER__TEXT, memberNameOrId));
+        result
+            .setHeader(CliStrings.format(CliStrings.DESCRIBE_CONFIG__HEADER__TEXT, memberNameOrId));
 
         List<String> jvmArgsList = memberConfigInfo.getJvmInputArguments();
-        TabularResultData jvmInputArgs = crd.addSection().addTable();
+        TabularResultModel jvmInputArgs = result.addTable();
 
         for (String jvmArg : jvmArgsList) {
           // This redaction should be redundant, since jvmArgs should have already been redacted in
@@ -83,48 +82,45 @@ public class DescribeConfigCommand extends InternalGfshCommand {
           jvmInputArgs.accumulate("JVM command line arguments", ArgumentRedactor.redact(jvmArg));
         }
 
-        addSection(crd, memberConfigInfo.getGfePropsSetUsingApi(),
+        addSection(result, memberConfigInfo.getGfePropsSetUsingApi(),
             "GemFire properties defined using the API");
-        addSection(crd, memberConfigInfo.getGfePropsRuntime(),
+        addSection(result, memberConfigInfo.getGfePropsRuntime(),
             "GemFire properties defined at the runtime");
-        addSection(crd, memberConfigInfo.getGfePropsSetFromFile(),
+        addSection(result, memberConfigInfo.getGfePropsSetFromFile(),
             "GemFire properties defined with the property file");
-        addSection(crd, memberConfigInfo.getGfePropsSetWithDefaults(),
+        addSection(result, memberConfigInfo.getGfePropsSetWithDefaults(),
             "GemFire properties using default values");
-        addSection(crd, memberConfigInfo.getCacheAttributes(), "Cache attributes");
+        addSection(result, memberConfigInfo.getCacheAttributes(), "Cache attributes");
 
         List<Map<String, String>> cacheServerAttributesList =
             memberConfigInfo.getCacheServerAttributes();
 
         if (cacheServerAttributesList != null && !cacheServerAttributesList.isEmpty()) {
           for (Map<String, String> cacheServerAttributes : cacheServerAttributesList) {
-            addSection(crd, cacheServerAttributes, "Cache-server attributes");
+            addSection(result, cacheServerAttributes, "Cache-server attributes");
           }
         }
-        result = ResultBuilder.buildResult(crd);
       }
 
     } catch (FunctionInvocationTargetException e) {
-      result = ResultBuilder.createGemFireErrorResult(CliStrings
+      result.createCommandProcessingError(CliStrings
           .format(CliStrings.COULD_NOT_EXECUTE_COMMAND_TRY_AGAIN, CliStrings.DESCRIBE_CONFIG));
     } catch (Exception e) {
-      ErrorResultData erd = ResultBuilder.createErrorResultData();
-      erd.addLine(e.getMessage());
-      result = ResultBuilder.buildResult(erd);
+      result.createError(e.getMessage());
+      result.setStatus(Result.Status.ERROR);
     }
     return result;
   }
 
-  private void addSection(CompositeResultData crd, Map<String, String> attrMap, String headerText) {
+  private void addSection(ResultModel model, Map<String, String> attrMap, String headerText) {
     if (attrMap != null && !attrMap.isEmpty()) {
-      CompositeResultData.SectionResultData section = crd.addSection();
-      section.setHeader(headerText);
-      section.addSeparator('.');
+      DataResultModel dataSection = model.addData();
+      dataSection.setHeader(headerText);
       Set<String> attributes = new TreeSet<>(attrMap.keySet());
 
       for (String attribute : attributes) {
         String attributeValue = attrMap.get(attribute);
-        section.addData(attribute, attributeValue);
+        dataSection.addData(attribute, attributeValue);
       }
     }
   }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeJndiBindingCommand.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeJndiBindingCommand.java
index 3e00ea2..4f1c97e 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeJndiBindingCommand.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/DescribeJndiBindingCommand.java
@@ -27,10 +27,9 @@ import org.apache.geode.cache.execute.Function;
 import org.apache.geode.distributed.internal.InternalClusterConfigurationService;
 import org.apache.geode.internal.logging.LogService;
 import org.apache.geode.management.cli.CliMetaData;
-import org.apache.geode.management.cli.Result;
 import org.apache.geode.management.internal.cli.functions.ListJndiBindingFunction;
-import org.apache.geode.management.internal.cli.result.ResultBuilder;
-import org.apache.geode.management.internal.cli.result.TabularResultData;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.cli.result.model.TabularResultModel;
 import org.apache.geode.management.internal.security.ResourceOperation;
 import org.apache.geode.security.ResourcePermission;
 
@@ -46,25 +45,24 @@ public class DescribeJndiBindingCommand extends InternalGfshCommand {
   @CliMetaData
   @ResourceOperation(resource = ResourcePermission.Resource.CLUSTER,
       operation = ResourcePermission.Operation.READ)
-  public Result describeJndiBinding(@CliOption(key = "name", mandatory = true,
+  public ResultModel describeJndiBinding(@CliOption(key = "name", mandatory = true,
       help = "Name of the binding to describe") String bindingName) {
-    Result result = null;
-    TabularResultData tabularData = ResultBuilder.createTabularResultData();
+
+    ResultModel crm = new ResultModel();
+    TabularResultModel tabularData = crm.addTable();
 
     InternalClusterConfigurationService ccService =
         (InternalClusterConfigurationService) getConfigurationService();
     if (ccService != null) {
       CacheConfig cacheConfig = ccService.getCacheConfig("cluster");
       if (cacheConfig == null) {
-        return ResultBuilder
-            .createUserErrorResult(String.format("JNDI binding : %s not found", bindingName));
+        return crm.createError(String.format("JNDI binding : %s not found", bindingName));
       }
       List<JndiBindingsType.JndiBinding> jndiBindings = cacheConfig.getJndiBindings();
 
       if (jndiBindings.stream().noneMatch(b -> b.getJndiName().equals(bindingName)
           || b.getJndiName().equals("java:" + bindingName))) {
-        return ResultBuilder
-            .createUserErrorResult(String.format("JNDI binding : %s not found", bindingName));
+        return crm.createError(String.format("JNDI binding : %s not found", bindingName));
       }
 
       for (JndiBindingsType.JndiBinding binding : jndiBindings) {
@@ -104,12 +102,10 @@ public class DescribeJndiBindingCommand extends InternalGfshCommand {
       }
     }
 
-    result = ResultBuilder.buildResult(tabularData);
-
-    return result;
+    return crm;
   }
 
-  private void addTableRow(TabularResultData table, String property, String value) {
+  private void addTableRow(TabularResultModel table, String property, String value) {
     table.accumulate("Property", property);
     table.accumulate("Value", value != null ? value : "");
   }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ExportLogsCommand.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ExportLogsCommand.java
index 72ff3ee..15740d6 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ExportLogsCommand.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ExportLogsCommand.java
@@ -42,7 +42,7 @@ import org.apache.geode.management.cli.Result;
 import org.apache.geode.management.internal.cli.functions.ExportLogsFunction;
 import org.apache.geode.management.internal.cli.functions.SizeExportLogsFunction;
 import org.apache.geode.management.internal.cli.i18n.CliStrings;
-import org.apache.geode.management.internal.cli.result.CommandResult;
+import org.apache.geode.management.internal.cli.result.LegacyCommandResult;
 import org.apache.geode.management.internal.cli.result.ResultBuilder;
 import org.apache.geode.management.internal.cli.util.ExportLogsCacheWriter;
 import org.apache.geode.management.internal.configuration.utils.ZipUtils;
@@ -196,7 +196,7 @@ public class ExportLogsCommand extends InternalGfshCommand {
       ZipUtils.zipDirectory(exportedLogsDir, exportedLogsZipFile);
       FileUtils.deleteDirectory(tempDir.toFile());
 
-      result = new CommandResult(exportedLogsZipFile);
+      result = new LegacyCommandResult(exportedLogsZipFile);
     } finally {
       ExportLogsFunction.destroyExportLogsRegion(cache);
     }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ListMembersCommand.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ListMembersCommand.java
index b68c962..a779189 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ListMembersCommand.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ListMembersCommand.java
@@ -26,10 +26,9 @@ import org.apache.geode.distributed.internal.InternalDistributedSystem;
 import org.apache.geode.distributed.internal.membership.MembershipManager;
 import org.apache.geode.management.cli.CliMetaData;
 import org.apache.geode.management.cli.ConverterHint;
-import org.apache.geode.management.cli.Result;
 import org.apache.geode.management.internal.cli.i18n.CliStrings;
-import org.apache.geode.management.internal.cli.result.ResultBuilder;
-import org.apache.geode.management.internal.cli.result.TabularResultData;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.cli.result.model.TabularResultModel;
 import org.apache.geode.management.internal.security.ResourceOperation;
 import org.apache.geode.security.ResourcePermission;
 
@@ -38,18 +37,20 @@ public class ListMembersCommand extends InternalGfshCommand {
   @CliMetaData(relatedTopic = CliStrings.TOPIC_GEODE_SERVER)
   @ResourceOperation(resource = ResourcePermission.Resource.CLUSTER,
       operation = ResourcePermission.Operation.READ)
-  public Result listMember(@CliOption(key = {CliStrings.GROUP, CliStrings.GROUPS},
+  public ResultModel listMember(@CliOption(key = {CliStrings.GROUP, CliStrings.GROUPS},
       optionContext = ConverterHint.MEMBERGROUP,
       help = CliStrings.LIST_MEMBER__GROUP__HELP) String[] groups) {
 
+    ResultModel crm = new ResultModel();
     Set<DistributedMember> memberSet = new TreeSet<>();
     memberSet.addAll(this.findMembersIncludingLocators(groups, null));
 
     if (memberSet.isEmpty()) {
-      return ResultBuilder.createInfoResult(CliStrings.LIST_MEMBER__MSG__NO_MEMBER_FOUND);
+      crm.addInfo().addLine(CliStrings.LIST_MEMBER__MSG__NO_MEMBER_FOUND);
+      return crm;
     }
 
-    TabularResultData resultData = ResultBuilder.createTabularResultData();
+    TabularResultModel resultData = crm.addTable();
     final DistributedMember coordinatorMember = getCoordinator();
     for (DistributedMember member : memberSet) {
       resultData.accumulate("Name", member.getName());
@@ -60,7 +61,7 @@ public class ListMembersCommand extends InternalGfshCommand {
       }
     }
 
-    return ResultBuilder.buildResult(resultData);
+    return crm;
   }
 
   DistributedMember getCoordinator() {
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/QueryInterceptor.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/QueryInterceptor.java
index 559965f..53f4ad9 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/QueryInterceptor.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/QueryInterceptor.java
@@ -30,6 +30,7 @@ import org.apache.geode.management.internal.cli.GfshParseResult;
 import org.apache.geode.management.internal.cli.result.CommandResult;
 import org.apache.geode.management.internal.cli.result.CompositeResultData;
 import org.apache.geode.management.internal.cli.result.InfoResultData;
+import org.apache.geode.management.internal.cli.result.LegacyCommandResult;
 import org.apache.geode.management.internal.cli.result.ResultBuilder;
 import org.apache.geode.management.internal.cli.result.TabularResultData;
 
@@ -69,7 +70,7 @@ public class QueryInterceptor extends AbstractCliAroundInterceptor {
     }
 
     TabularResultData tabularResultData = sectionResultData.retrieveTableByIndex(0);
-    CommandResult resultTable = new CommandResult(tabularResultData);
+    CommandResult resultTable = new LegacyCommandResult(tabularResultData);
     try {
       writeResultTableToFile(outputFile, resultTable);
       // return a result w/ message explaining limit
@@ -86,7 +87,7 @@ public class QueryInterceptor extends AbstractCliAroundInterceptor {
     infoResultData.addLine(SystemUtils.LINE_SEPARATOR);
     infoResultData.addLine("Query results output to " + outputFile.getAbsolutePath());
 
-    return new CommandResult(infoResultData);
+    return new LegacyCommandResult(infoResultData);
   }
 
   private File getOutputFile(ParseResult parseResult) {
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/CommandStatementImpl.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/CommandStatementImpl.java
index c93dce8..8467484 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/CommandStatementImpl.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/CommandStatementImpl.java
@@ -51,7 +51,7 @@ public class CommandStatementImpl implements CommandStatement {
 
   @Override
   public Result process() {
-    return cmdProcessor.executeCommand(commandString, env, null);
+    return (Result) cmdProcessor.executeCommand(commandString, env, null);
   }
 
   public boolean validate() {
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/MemberCommandService.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/MemberCommandService.java
index 3df27a3..617291d 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/MemberCommandService.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/MemberCommandService.java
@@ -47,7 +47,7 @@ public class MemberCommandService extends CommandService {
   }
 
   public Result processCommand(String commandString, Map<String, String> env) {
-    return onlineCommandProcessor.executeCommand(commandString, env, null);
+    return (Result) onlineCommandProcessor.executeCommand(commandString, env, null);
   }
 
   @Deprecated
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/OnlineCommandProcessor.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/OnlineCommandProcessor.java
index 93fd103..19640e1 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/OnlineCommandProcessor.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/remote/OnlineCommandProcessor.java
@@ -29,7 +29,6 @@ import org.apache.geode.internal.cache.InternalCache;
 import org.apache.geode.internal.security.SecurityService;
 import org.apache.geode.management.cli.CliMetaData;
 import org.apache.geode.management.cli.CommandProcessingException;
-import org.apache.geode.management.cli.Result;
 import org.apache.geode.management.internal.cli.CommandManager;
 import org.apache.geode.management.internal.cli.GfshParseResult;
 import org.apache.geode.management.internal.cli.GfshParser;
@@ -83,15 +82,11 @@ public class OnlineCommandProcessor {
     throw new IllegalStateException("Command String should not be null.");
   }
 
-  public Result executeCommand(String command) {
+  public Object executeCommand(String command) {
     return executeCommand(command, Collections.emptyMap(), null);
   }
 
-  public Result executeCommand(String command, Map<String, String> env) {
-    return executeCommand(command, env, null);
-  }
-
-  public Result executeCommand(String command, Map<String, String> env,
+  public Object executeCommand(String command, Map<String, String> env,
       List<String> stagedFilePaths) {
     CommentSkipHelper commentSkipper = new CommentSkipHelper();
     String commentLessLine = commentSkipper.skipComments(command);
@@ -118,13 +113,13 @@ public class OnlineCommandProcessor {
           resourceOperation.target(), ResourcePermission.ALL);
     }
 
-    // this command processor does not exeucte command that needs fileData passed from client
+    // this command processor does not execute commands that need fileData passed from client
     CliMetaData metaData = method.getAnnotation(CliMetaData.class);
     if (metaData != null && metaData.isFileUploaded() && stagedFilePaths == null) {
       return ResultBuilder
           .createUserErrorResult(command + " can not be executed only from server side");
     }
 
-    return (Result) commandExecutor.execute((GfshParseResult) parseResult);
+    return commandExecutor.execute((GfshParseResult) parseResult);
   }
 }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/CommandResult.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/CommandResult.java
index f68eac9..64d3788 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/CommandResult.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/CommandResult.java
@@ -16,597 +16,95 @@ package org.apache.geode.management.internal.cli.result;
 
 import java.io.IOException;
 import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Vector;
-import java.util.stream.Collectors;
-import java.util.zip.DataFormatException;
 
-import org.apache.logging.log4j.Logger;
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import org.apache.geode.internal.logging.LogService;
 import org.apache.geode.management.cli.Result;
-import org.apache.geode.management.internal.cli.GfshParser;
-import org.apache.geode.management.internal.cli.json.GfJsonArray;
-import org.apache.geode.management.internal.cli.json.GfJsonException;
 import org.apache.geode.management.internal.cli.json.GfJsonObject;
-import org.apache.geode.management.internal.cli.result.TableBuilder.Row;
-import org.apache.geode.management.internal.cli.result.TableBuilder.RowGroup;
-import org.apache.geode.management.internal.cli.result.TableBuilder.Table;
 
-/**
- * Wraps the Result of a command execution.
- *
- * @since GemFire 7.0
- */
+public interface CommandResult extends Result {
 
-public class CommandResult implements Result {
-  private static final Logger logger = LogService.getLogger();
+  Path getFileToDownload();
 
-  private GfJsonObject gfJsonObject;
-  private Status status;
-  private int index;
-  private boolean isDataBuilt;
+  boolean hasFileToDownload();
 
-  private ResultData resultData;
-  private List<String> resultLines;
-  private boolean failedToPersist = false;
-  private Object configObject;
+  Status getStatus();
 
-  private transient int numTimesSaved;
+  void setStatus(Status status);
 
-  public Path getFileToDownload() {
-    return fileToDownload;
-  }
+  Object getResultData();
 
-  private Path fileToDownload;
+  Object getConfigObject();
 
+  void setConfigObject(Object configObject);
 
-  public CommandResult(ResultData resultData) {
-    this.resultData = resultData;
-    this.gfJsonObject = this.resultData.getGfJsonObject();
-    this.status = this.resultData.getStatus();
-    this.resultLines = new Vector<>();
-  }
+  void resetToFirstLine();
 
-  public CommandResult(Path fileToDownload) {
-    this(new InfoResultData(fileToDownload.toString()));
-    this.fileToDownload = fileToDownload.toAbsolutePath();
-  }
+  boolean hasIncomingFiles();
 
-  public boolean hasFileToDownload() {
-    return fileToDownload != null;
-  }
+  int getNumTimesSaved();
 
-  @Override
-  public Status getStatus() {
-    return this.status;
-  }
+  void saveIncomingFiles(String directory) throws IOException;
 
-  public void setStatus(Status status) {
-    this.status = status;
-  }
-
-  public ResultData getResultData() {
-    return ResultBuilder.getReadOnlyResultData(resultData);
-  }
+  boolean hasNextLine();
 
-  private GfJsonObject getGfJsonObject() {
-    return gfJsonObject;
-  }
+  String nextLine();
 
-  public Object getConfigObject() {
-    return configObject;
-  }
+  String toJson();
 
-  public void setConfigObject(Object configObject) {
-    this.configObject = configObject;
-  }
+  String getType();
 
-  @Override
-  public void resetToFirstLine() {
-    index = 0;
-  }
+  String getHeader();
 
-  private void buildData() {
-    try {
-      if (ResultData.TYPE_COMPOSITE.equals(resultData.getType())) {
-        buildComposite();
-      } else {
-        GfJsonObject content = getContent();
-        if (content != null) {
-          Table resultTable = TableBuilder.newTable();
-
-          addHeaderInTable(resultTable, getGfJsonObject());
-
-          RowGroup rowGroup = resultTable.newRowGroup();
-
-          if (ResultData.TYPE_TABULAR.equals(resultData.getType())) {
-            resultTable.setColumnSeparator("   ");
-            resultTable.setTabularResult(true);
-            buildTable(rowGroup, content);
-          } else {
-            buildInfoErrorData(rowGroup, content);
-          }
-
-          addFooterInTable(resultTable, getGfJsonObject());
-
-          resultLines.addAll(resultTable.buildTableList());
-        }
-      }
-    } catch (GfJsonException e) {
-      resultLines
-          .add("Error occurred while processing Command Result. Internal Error - Invalid Result.");
-    } finally {
-      isDataBuilt = true;
-    }
-  }
+  String getHeader(GfJsonObject gfJsonObject);
 
-  private void addHeaderInTable(Table resultTable, GfJsonObject fromJsonObject) {
-    String header = getHeader(fromJsonObject);
-    if (header != null && !header.isEmpty()) {
-      resultTable.newRow().newLeftCol(header);
-    }
-  }
+  GfJsonObject getContent();
 
-  private void addHeaderInRowGroup(RowGroup rowGroup, GfJsonObject fromJsonObject) {
-    String header = getHeader(fromJsonObject);
-    if (header != null && !header.isEmpty()) {
-      rowGroup.newRow().newLeftCol(header);
-    }
-  }
+  String getMessageFromContent();
 
-  private void addFooterInTable(Table resultTable, GfJsonObject fromJsonObject) {
-    String footer = getFooter(fromJsonObject);
-    if (footer != null && !footer.isEmpty()) {
-      resultTable.newRow().newLeftCol(footer);
-    }
+  default String getErrorMessage() {
+    throw new UnsupportedOperationException("This should never be called from LegacyCommandResult");
   }
 
-  private void addFooterInRowGroup(RowGroup rowGroup, GfJsonObject fromJsonObject) {
-    String footer = getFooter(fromJsonObject);
-    if (footer != null && !footer.isEmpty()) {
-      rowGroup.newRow().newLeftCol(footer);
-    }
-  }
+  String getValueFromContent(String key);
 
-  private void buildInfoErrorData(RowGroup rowGroup, GfJsonObject content) throws GfJsonException {
-    GfJsonArray accumulatedData = content.getJSONArray(InfoResultData.RESULT_CONTENT_MESSAGE);
-    if (accumulatedData != null) {
-      buildRows(rowGroup, null, accumulatedData);
-    }
-  }
+  List<String> getListFromContent(String key);
 
-  private boolean isPrimitiveOrStringOrWrapper(Object object) {
-    boolean isPrimitive = false;
-    if (String.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (byte.class.isInstance(object) || Byte.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (short.class.isInstance(object) || Short.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (int.class.isInstance(object) || Integer.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (long.class.isInstance(object) || Long.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (float.class.isInstance(object) || Float.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (double.class.isInstance(object) || Double.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (boolean.class.isInstance(object) || Boolean.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (char.class.isInstance(object) || Character.class.isInstance(object)) {
-      isPrimitive = true;
-    }
-
-    return isPrimitive;
+  default List<String> getColumnFromTableContent(String column, String sectionId, String tableId) {
+    throw new UnsupportedOperationException("This should never be called from ModelCommandResult");
   }
 
-  private boolean isPrimitiveOrStringOrWrapperArray(Object object) {
-    boolean isPrimitive = false;
-    if (String[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (byte[].class.isInstance(object) || Byte[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (short[].class.isInstance(object) || Short[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (int[].class.isInstance(object) || Integer[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (long[].class.isInstance(object) || Long[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (float[].class.isInstance(object) || Float[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (double[].class.isInstance(object) || Double[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (boolean[].class.isInstance(object) || Boolean[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (char[].class.isInstance(object) || Character[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (GfJsonArray.class.isInstance(object)) {
-      GfJsonArray jsonArr = (GfJsonArray) object;
-      try {
-        isPrimitive = isPrimitiveOrStringOrWrapper(jsonArr.get(0));
-      } catch (GfJsonException ignored) {
-      }
-    }
-
-    return isPrimitive;
+  default List<String> getColumnFromTableContent(String column, String tableId) {
+    throw new UnsupportedOperationException("This should never be called from LegacyCommandResult");
   }
 
-  private void buildComposite() {
-    try {
-      GfJsonObject content = getContent();
-      if (content != null) {
-        Table resultTable = TableBuilder.newTable();
-        resultTable.setColumnSeparator(" : ");
-
-        addHeaderInTable(resultTable, getGfJsonObject());
-
-        for (Iterator<String> it = content.keys(); it.hasNext();) {
-          String key = it.next();
-          if (key.startsWith(CompositeResultData.SECTION_DATA_ACCESSOR)) {
-            GfJsonObject subSection = content.getJSONObject(key);
-            buildSection(resultTable, null, subSection, 0);
-          } else if (key.equals(CompositeResultData.SEPARATOR)) {
-            String separatorString = content.getString(key);
-            resultTable.newRowGroup().newRowSeparator(separatorString.charAt(0), true);
-          }
-        }
-
-        addFooterInTable(resultTable, getGfJsonObject());
-        resultLines.addAll(resultTable.buildTableList());
-      }
-    } catch (GfJsonException e) {
-      resultLines
-          .add("Error occurred while processing Command Result. Internal Error - Invalid Result.");
-      logger.info(
-          "Error occurred while processing Command Result. Internal Error - Invalid Result.", e);
-    } finally {
-      isDataBuilt = true;
-    }
+  default Map<String, List<String>> getMapFromTableContent(String sectionId, String tableId) {
+    throw new UnsupportedOperationException("This should never be called from ModelCommandResult");
   }
 
-  private void buildSection(Table table, RowGroup parentRowGroup, GfJsonObject section, int depth)
-      throws GfJsonException {
-    Iterator<String> keys = section.keys();
-    RowGroup rowGroup;
-    if (parentRowGroup != null) {
-      rowGroup = parentRowGroup;
-    } else {
-      rowGroup = table.newRowGroup();
-    }
-    addHeaderInRowGroup(rowGroup, section);
-    while (keys.hasNext()) {
-      String key = keys.next();
-      Object object = section.get(key);
-      if (key.startsWith(CompositeResultData.TABLE_DATA_ACCESSOR)) {
-        GfJsonObject tableObject = section.getJSONObject(key);
-
-        addHeaderInTable(table, tableObject);
-
-        RowGroup rowGroupForTable = table.newRowGroup();
-        buildTable(rowGroupForTable, tableObject.getJSONObject(ResultData.RESULT_CONTENT));
-
-        addFooterInTable(table, tableObject);
-      } else if (key.startsWith(CompositeResultData.SECTION_DATA_ACCESSOR)) {
-        GfJsonObject subSection = section.getJSONObject(key);
-        buildSection(table, rowGroup, subSection, depth + 1);
-      } else if (key.equals(CompositeResultData.SEPARATOR)) {
-        String separatorString = section.getString(key);
-        rowGroup.newRowSeparator(separatorString.charAt(0), true);
-      } else if (key.equals(ResultData.RESULT_HEADER) || key.equals(ResultData.RESULT_FOOTER)) {
-        // skip header & footer
-      } else {
-        Row newRow = rowGroup.newRow();
-        String prefix = "";
-        for (int i = 0; i < depth; i++) {
-          prefix += " . ";
-        }
-        String[] value = getValuesSeparatedByLines(object);
-        if (value.length == 1) {
-          newRow.newLeftCol(prefix + key).newLeftCol(value[0]);
-        } else {
-          if (value.length != 0) { // possible when object == CliConstants.LINE_SEPARATOR
-            newRow.newLeftCol(prefix + key).newLeftCol(value[0]);
-            for (int i = 1; i < value.length; i++) {
-              newRow = rowGroup.newRow();
-              newRow.setColumnSeparator("   ");
-              newRow.newLeftCol("").newLeftCol(value[i]);
-            }
-          } else {
-            newRow.newLeftCol(prefix + key).newLeftCol("");
-          }
-        }
-      }
-    }
-    addFooterInRowGroup(rowGroup, section);
+  default Map<String, List<String>> getMapFromTableContent(String tableId) {
+    throw new UnsupportedOperationException("This should never be called from LegacyCommandResult");
   }
 
-  private static String[] getValuesSeparatedByLines(Object object) {
-    String valueString = String.valueOf(object);
-    return valueString.split(GfshParser.LINE_SEPARATOR);
-  }
+  Map<String, String> getMapFromSection(String sectionID);
 
-  private void buildTable(RowGroup rowGroup, GfJsonObject content) throws GfJsonException {
-    GfJsonArray columnNames = content.names();
-    int numOfColumns = columnNames.size();
-    Row headerRow = rowGroup.newRow();
-    rowGroup.setColumnSeparator(" | ");
-    rowGroup.newRowSeparator('-', false);
-
-    // build Table Header first
-    for (int i = 0; i < numOfColumns; i++) {
-      Object object = columnNames.get(i);
-      if (AbstractResultData.BYTE_DATA_ACCESSOR.equals(object)) {
-        // skip file data if any
-        continue;
-      }
-      headerRow.newCenterCol(object);
-    }
-
-    // Build remaining rows by extracting data column-wise from JSON object
-    Row[] dataRows = null;
-    for (int i = 0; i < numOfColumns; i++) {
-      Object object = columnNames.get(i);
-      if (AbstractResultData.BYTE_DATA_ACCESSOR.equals(object)) {
-        // skip file data if any
-        continue;
-      }
-      GfJsonArray accumulatedData = content.getJSONArray((String) object);
-
-      dataRows = buildRows(rowGroup, dataRows, accumulatedData);
-    }
-  }
+  String getFooter();
 
-  private Row[] buildRows(RowGroup rowGroup, Row[] dataRows, GfJsonArray accumulatedData)
-      throws GfJsonException {
-    int size = accumulatedData.size();
+  boolean equals(Object obj);
 
-    // Initialize rows' array as required
-    if (dataRows == null) {
-      dataRows = new Row[size];
+  int hashCode();
 
-      for (int j = 0; j < dataRows.length; j++) {
-        dataRows[j] = rowGroup.newRow();
-      }
-    }
+  String toString();
 
-    // Add data column-wise
-    for (int j = 0; j < size; j++) {
-      dataRows[j].newLeftCol(accumulatedData.get(j));
-    }
-    return dataRows;
-  }
-
-  @Override
-  public boolean hasIncomingFiles() {
-    GfJsonArray fileDataArray = null;
-    try {
-      GfJsonObject content = getContent();
-      if (content != null) {
-        fileDataArray = content.getJSONArray(CompositeResultData.BYTE_DATA_ACCESSOR);
-      }
-    } catch (GfJsonException e) {
-      e.printStackTrace();
-    }
-    return fileDataArray != null;
-  }
-
-  public int getNumTimesSaved() {
-    return numTimesSaved;
-  }
-
-  @Override
-  public void saveIncomingFiles(String directory) throws IOException {
-    // dump file data if any
-    try {
-      GfJsonObject content = getContent();
-      if (content != null) {
-        GfJsonArray bytesArray = content.getJSONArray(CompositeResultData.BYTE_DATA_ACCESSOR);
-        AbstractResultData.readFileDataAndDump(bytesArray, directory);
-      } else {
-        throw new RuntimeException("No associated files to save .. ");
-      }
-      numTimesSaved = numTimesSaved + 1;
-    } catch (DataFormatException | GfJsonException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  @Override
-  public boolean hasNextLine() {
-    if (!isDataBuilt) {
-      buildData();
-    }
-    return index < resultLines.size();
-  }
-
-  /**
-   * @throws ArrayIndexOutOfBoundsException if this method is called more number of times than the
-   *         data items it contains
-   */
-  @Override
-  public String nextLine() {
-    if (!isDataBuilt) {
-      buildData();
-    }
-    return resultLines.get(index++);
-  }
-
-  public String toJson() {
-    return gfJsonObject.toString();
-  }
-
-  public String getType() {
-    return resultData.getType();
-  }
-
-  public String getHeader() {
-    return getHeader(gfJsonObject);
-  }
-
-  public String getHeader(GfJsonObject gfJsonObject) {
-    return gfJsonObject.getString(ResultData.RESULT_HEADER);
-  }
-
-  public GfJsonObject getContent() {
-    return gfJsonObject.getJSONObject(ResultData.RESULT_CONTENT);
-  }
+  boolean failedToPersist();
 
-  public String getMessageFromContent() {
-    return getContent().getString("message");
-  }
-
-  public String getValueFromContent(String key) {
-    return getContent().get(key).toString();
-  }
-
-  public List<String> getListFromContent(String key) {
-    return getContent().getArrayValues(key);
-  }
-
-  public List<String> getColumnFromTableContent(String column, int... sectionAndTableIDs) {
-    List<String> ids =
-        Arrays.stream(sectionAndTableIDs).mapToObj(Integer::toString).collect(Collectors.toList());
-    return CommandResult.toList(
-        getTableContent(ids.toArray(new String[0])).getInternalJsonObject().getJSONArray(column));
-  }
-
-  public Map<String, List<String>> getMapFromTableContent(int... sectionAndTableIDs) {
-    Map<String, List<String>> result = new LinkedHashMap<>();
-
-    List<String> ids =
-        Arrays.stream(sectionAndTableIDs).mapToObj(Integer::toString).collect(Collectors.toList());
-    JSONObject table = getTableContent(ids.toArray(new String[0])).getInternalJsonObject();
-    for (String column : table.keySet()) {
-      result.put(column, CommandResult.toList(table.getJSONArray(column)));
-    }
-
-    return result;
-  }
-
-  public Map<String, List<String>> getMapFromTableContent(String... sectionAndTableIDs) {
-    Map<String, List<String>> result = new LinkedHashMap<>();
-
-    JSONObject table = getTableContent(sectionAndTableIDs).getInternalJsonObject();
-    for (String column : table.keySet()) {
-      result.put(column, CommandResult.toList(table.getJSONArray(column)));
-    }
-
-    return result;
-  }
-
-  public Map<String, String> getMapFromSection(String sectionID) {
-    Map<String, String> result = new LinkedHashMap<>();
-    GfJsonObject obj = getContent().getJSONObject("__sections__-" + sectionID);
-
-    Iterator<String> iter = obj.keys();
-    while (iter.hasNext()) {
-      String key = iter.next();
-      result.put(key, obj.getString(key));
-    }
-
-    return result;
-  }
-
-  /**
-   * The intent is that this method should be able to handle both ResultData as well as
-   * CompositeResultData
-   *
-   * @return the extracted GfJsonObject table
-   */
-  private GfJsonObject getTableContent() {
-    return getTableContent("0", "0");
-  }
-
-  /**
-   * Most frequently, only two index values are required: a section index followed by a table index.
-   * Some commands, such as 'describe region', may return command results with subsections, however.
-   * Include these in order, e.g., getTableContent(sectionIndex, subsectionIndex, tableIndex);
-   */
-  private GfJsonObject getTableContent(String... sectionAndTableIDs) {
-    GfJsonObject topLevelContent = getContent();
-    // Most common is receiving exactly one section index and one table index.
-    // Some results, however, will have subsections before the table listings.
-    assert (sectionAndTableIDs.length >= 2);
-
-    GfJsonObject sectionObject = topLevelContent;
-    for (int i = 0; i < sectionAndTableIDs.length - 1; i++) {
-      String idx = sectionAndTableIDs[i];
-      sectionObject = sectionObject.getJSONObject("__sections__-" + idx);
-      if (sectionObject == null) {
-        return topLevelContent;
-      }
-    }
-
-    String tableId = sectionAndTableIDs[sectionAndTableIDs.length - 1];
-    GfJsonObject tableContent = sectionObject.getJSONObject("__tables__-" + tableId);
-    if (tableContent == null) {
-      return topLevelContent;
-    }
-
-    return tableContent.getJSONObject("content");
-  }
-
-  public String getFooter() {
-    return getFooter(gfJsonObject);
-  }
-
-  private String getFooter(GfJsonObject gfJsonObject) {
-    return gfJsonObject.getString(ResultData.RESULT_FOOTER);
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (!(obj instanceof CommandResult)) {
-      return false;
-    }
-    CommandResult other = (CommandResult) obj;
-
-    return this.gfJsonObject.toString().equals(other.gfJsonObject.toString());
-  }
-
-  public int hashCode() {
-    return this.gfJsonObject.hashCode(); // any arbitrary constant will do
-  }
-
-  @Override
-  public String toString() {
-    return "CommandResult [gfJsonObject=" + gfJsonObject + ", status=" + status + ", index=" + index
-        + ", isDataBuilt=" + isDataBuilt + ", resultData=" + resultData + ", resultLines="
-        + resultLines + ", failedToPersist=" + failedToPersist + "]";
-  }
-
-  @Override
-  public boolean failedToPersist() {
-    return this.failedToPersist;
-  }
-
-  @Override
-  public void setCommandPersisted(boolean commandPersisted) {
-    this.failedToPersist = !commandPersisted;
-  }
-
-  public void setFileToDownload(Path fileToDownload) {
-    this.fileToDownload = fileToDownload;
-  }
-
-  public List<String> getColumnValues(String columnName) {
-    return toList(getTableContent().getInternalJsonObject().getJSONArray(columnName));
-  }
+  void setCommandPersisted(boolean commandPersisted);
 
-  public static List<String> toList(JSONArray array) {
-    Object[] values = new Object[array.length()];
+  void setFileToDownload(Path fileToDownload);
 
-    for (int i = 0; i < array.length(); i++) {
-      values[i] = array.get(i);
-    }
+  List<String> getTableColumnValues(String columnName);
 
-    return Arrays.stream(values).map(Object::toString).collect(Collectors.toList());
+  default List<String> getTableColumnValues(String sectionId, String columnName) {
+    throw new UnsupportedOperationException("This should never be called from LegacyCommandResult");
   }
 }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/CommandResult.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/LegacyCommandResult.java
similarity index 73%
copy from geode-core/src/main/java/org/apache/geode/management/internal/cli/result/CommandResult.java
copy to geode-core/src/main/java/org/apache/geode/management/internal/cli/result/LegacyCommandResult.java
index f68eac9..19a597e 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/CommandResult.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/LegacyCommandResult.java
@@ -30,7 +30,6 @@ import org.json.JSONArray;
 import org.json.JSONObject;
 
 import org.apache.geode.internal.logging.LogService;
-import org.apache.geode.management.cli.Result;
 import org.apache.geode.management.internal.cli.GfshParser;
 import org.apache.geode.management.internal.cli.json.GfJsonArray;
 import org.apache.geode.management.internal.cli.json.GfJsonException;
@@ -45,7 +44,7 @@ import org.apache.geode.management.internal.cli.result.TableBuilder.Table;
  * @since GemFire 7.0
  */
 
-public class CommandResult implements Result {
+public class LegacyCommandResult implements CommandResult {
   private static final Logger logger = LogService.getLogger();
 
   private GfJsonObject gfJsonObject;
@@ -56,7 +55,6 @@ public class CommandResult implements Result {
   private ResultData resultData;
   private List<String> resultLines;
   private boolean failedToPersist = false;
-  private Object configObject;
 
   private transient int numTimesSaved;
 
@@ -65,20 +63,30 @@ public class CommandResult implements Result {
   }
 
   private Path fileToDownload;
+  private Object configObject;
 
-
-  public CommandResult(ResultData resultData) {
+  public LegacyCommandResult(ResultData resultData) {
     this.resultData = resultData;
     this.gfJsonObject = this.resultData.getGfJsonObject();
     this.status = this.resultData.getStatus();
     this.resultLines = new Vector<>();
   }
 
-  public CommandResult(Path fileToDownload) {
+  public LegacyCommandResult(Path fileToDownload) {
     this(new InfoResultData(fileToDownload.toString()));
     this.fileToDownload = fileToDownload.toAbsolutePath();
   }
 
+  @Override
+  public Object getConfigObject() {
+    return configObject;
+  }
+
+  @Override
+  public void setConfigObject(Object configObject) {
+    this.configObject = configObject;
+  }
+
   public boolean hasFileToDownload() {
     return fileToDownload != null;
   }
@@ -88,6 +96,7 @@ public class CommandResult implements Result {
     return this.status;
   }
 
+  @Override
   public void setStatus(Status status) {
     this.status = status;
   }
@@ -100,14 +109,6 @@ public class CommandResult implements Result {
     return gfJsonObject;
   }
 
-  public Object getConfigObject() {
-    return configObject;
-  }
-
-  public void setConfigObject(Object configObject) {
-    this.configObject = configObject;
-  }
-
   @Override
   public void resetToFirstLine() {
     index = 0;
@@ -182,62 +183,6 @@ public class CommandResult implements Result {
     }
   }
 
-  private boolean isPrimitiveOrStringOrWrapper(Object object) {
-    boolean isPrimitive = false;
-    if (String.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (byte.class.isInstance(object) || Byte.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (short.class.isInstance(object) || Short.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (int.class.isInstance(object) || Integer.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (long.class.isInstance(object) || Long.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (float.class.isInstance(object) || Float.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (double.class.isInstance(object) || Double.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (boolean.class.isInstance(object) || Boolean.class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (char.class.isInstance(object) || Character.class.isInstance(object)) {
-      isPrimitive = true;
-    }
-
-    return isPrimitive;
-  }
-
-  private boolean isPrimitiveOrStringOrWrapperArray(Object object) {
-    boolean isPrimitive = false;
-    if (String[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (byte[].class.isInstance(object) || Byte[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (short[].class.isInstance(object) || Short[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (int[].class.isInstance(object) || Integer[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (long[].class.isInstance(object) || Long[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (float[].class.isInstance(object) || Float[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (double[].class.isInstance(object) || Double[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (boolean[].class.isInstance(object) || Boolean[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (char[].class.isInstance(object) || Character[].class.isInstance(object)) {
-      isPrimitive = true;
-    } else if (GfJsonArray.class.isInstance(object)) {
-      GfJsonArray jsonArr = (GfJsonArray) object;
-      try {
-        isPrimitive = isPrimitiveOrStringOrWrapper(jsonArr.get(0));
-      } catch (GfJsonException ignored) {
-      }
-    }
-
-    return isPrimitive;
-  }
-
   private void buildComposite() {
     try {
       GfJsonObject content = getContent();
@@ -293,9 +238,6 @@ public class CommandResult implements Result {
         buildTable(rowGroupForTable, tableObject.getJSONObject(ResultData.RESULT_CONTENT));
 
         addFooterInTable(table, tableObject);
-      } else if (key.startsWith(CompositeResultData.SECTION_DATA_ACCESSOR)) {
-        GfJsonObject subSection = section.getJSONObject(key);
-        buildSection(table, rowGroup, subSection, depth + 1);
       } else if (key.equals(CompositeResultData.SEPARATOR)) {
         String separatorString = section.getString(key);
         rowGroup.newRowSeparator(separatorString.charAt(0), true);
@@ -397,6 +339,7 @@ public class CommandResult implements Result {
     return fileDataArray != null;
   }
 
+  @Override
   public int getNumTimesSaved() {
     return numTimesSaved;
   }
@@ -438,69 +381,75 @@ public class CommandResult implements Result {
     return resultLines.get(index++);
   }
 
+  @Override
   public String toJson() {
     return gfJsonObject.toString();
   }
 
+  @Override
   public String getType() {
     return resultData.getType();
   }
 
+  @Override
   public String getHeader() {
     return getHeader(gfJsonObject);
   }
 
+  @Override
   public String getHeader(GfJsonObject gfJsonObject) {
     return gfJsonObject.getString(ResultData.RESULT_HEADER);
   }
 
+  @Override
   public GfJsonObject getContent() {
     return gfJsonObject.getJSONObject(ResultData.RESULT_CONTENT);
   }
 
+  @Override
   public String getMessageFromContent() {
-    return getContent().getString("message");
+    List<String> messages;
+    try {
+      GfJsonArray jsonArray = getContent().getJSONArray("message");
+      if (jsonArray == null) {
+        return "";
+      }
+      messages = toList(jsonArray.getInternalJsonArray());
+    } catch (GfJsonException jex) {
+      return "";
+    }
+
+    return messages.stream().collect(Collectors.joining(". "));
   }
 
+  @Override
   public String getValueFromContent(String key) {
     return getContent().get(key).toString();
   }
 
+  @Override
   public List<String> getListFromContent(String key) {
     return getContent().getArrayValues(key);
   }
 
-  public List<String> getColumnFromTableContent(String column, int... sectionAndTableIDs) {
-    List<String> ids =
-        Arrays.stream(sectionAndTableIDs).mapToObj(Integer::toString).collect(Collectors.toList());
-    return CommandResult.toList(
-        getTableContent(ids.toArray(new String[0])).getInternalJsonObject().getJSONArray(column));
-  }
-
-  public Map<String, List<String>> getMapFromTableContent(int... sectionAndTableIDs) {
-    Map<String, List<String>> result = new LinkedHashMap<>();
-
-    List<String> ids =
-        Arrays.stream(sectionAndTableIDs).mapToObj(Integer::toString).collect(Collectors.toList());
-    JSONObject table = getTableContent(ids.toArray(new String[0])).getInternalJsonObject();
-    for (String column : table.keySet()) {
-      result.put(column, CommandResult.toList(table.getJSONArray(column)));
-    }
-
-    return result;
+  @Override
+  public List<String> getColumnFromTableContent(String column, String sectionId, String tableId) {
+    return toList(getTableContent(sectionId, tableId).getInternalJsonObject().getJSONArray(column));
   }
 
-  public Map<String, List<String>> getMapFromTableContent(String... sectionAndTableIDs) {
+  @Override
+  public Map<String, List<String>> getMapFromTableContent(String sectionId, String tableId) {
     Map<String, List<String>> result = new LinkedHashMap<>();
 
-    JSONObject table = getTableContent(sectionAndTableIDs).getInternalJsonObject();
+    JSONObject table = getTableContent(sectionId, tableId).getInternalJsonObject();
     for (String column : table.keySet()) {
-      result.put(column, CommandResult.toList(table.getJSONArray(column)));
+      result.put(column, toList(table.getJSONArray(column)));
     }
 
     return result;
   }
 
+  @Override
   public Map<String, String> getMapFromSection(String sectionID) {
     Map<String, String> result = new LinkedHashMap<>();
     GfJsonObject obj = getContent().getJSONObject("__sections__-" + sectionID);
@@ -515,36 +464,17 @@ public class CommandResult implements Result {
   }
 
   /**
-   * The intent is that this method should be able to handle both ResultData as well as
-   * CompositeResultData
-   *
-   * @return the extracted GfJsonObject table
+   * Tables can only occur within a section. Sections cannot be nested.
    */
-  private GfJsonObject getTableContent() {
-    return getTableContent("0", "0");
-  }
-
-  /**
-   * Most frequently, only two index values are required: a section index followed by a table index.
-   * Some commands, such as 'describe region', may return command results with subsections, however.
-   * Include these in order, e.g., getTableContent(sectionIndex, subsectionIndex, tableIndex);
-   */
-  private GfJsonObject getTableContent(String... sectionAndTableIDs) {
+  private GfJsonObject getTableContent(String sectionId, String tableId) {
     GfJsonObject topLevelContent = getContent();
-    // Most common is receiving exactly one section index and one table index.
-    // Some results, however, will have subsections before the table listings.
-    assert (sectionAndTableIDs.length >= 2);
 
     GfJsonObject sectionObject = topLevelContent;
-    for (int i = 0; i < sectionAndTableIDs.length - 1; i++) {
-      String idx = sectionAndTableIDs[i];
-      sectionObject = sectionObject.getJSONObject("__sections__-" + idx);
-      if (sectionObject == null) {
-        return topLevelContent;
-      }
+    sectionObject = sectionObject.getJSONObject("__sections__-" + sectionId);
+    if (sectionObject == null) {
+      return topLevelContent;
     }
 
-    String tableId = sectionAndTableIDs[sectionAndTableIDs.length - 1];
     GfJsonObject tableContent = sectionObject.getJSONObject("__tables__-" + tableId);
     if (tableContent == null) {
       return topLevelContent;
@@ -553,6 +483,7 @@ public class CommandResult implements Result {
     return tableContent.getJSONObject("content");
   }
 
+  @Override
   public String getFooter() {
     return getFooter(gfJsonObject);
   }
@@ -563,14 +494,15 @@ public class CommandResult implements Result {
 
   @Override
   public boolean equals(Object obj) {
-    if (!(obj instanceof CommandResult)) {
+    if (!(obj instanceof LegacyCommandResult)) {
       return false;
     }
-    CommandResult other = (CommandResult) obj;
+    LegacyCommandResult other = (LegacyCommandResult) obj;
 
     return this.gfJsonObject.toString().equals(other.gfJsonObject.toString());
   }
 
+  @Override
   public int hashCode() {
     return this.gfJsonObject.hashCode(); // any arbitrary constant will do
   }
@@ -592,15 +524,17 @@ public class CommandResult implements Result {
     this.failedToPersist = !commandPersisted;
   }
 
+  @Override
   public void setFileToDownload(Path fileToDownload) {
     this.fileToDownload = fileToDownload;
   }
 
-  public List<String> getColumnValues(String columnName) {
-    return toList(getTableContent().getInternalJsonObject().getJSONArray(columnName));
+  @Override
+  public List<String> getTableColumnValues(String columnName) {
+    return getTableContent("0", "0").getArrayValues(columnName);
   }
 
-  public static List<String> toList(JSONArray array) {
+  private List<String> toList(JSONArray array) {
     Object[] values = new Object[array.length()];
 
     for (int i = 0; i < array.length(); i++) {
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ModelCommandResult.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ModelCommandResult.java
new file mode 100644
index 0000000..bdd9b42
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ModelCommandResult.java
@@ -0,0 +1,357 @@
+/*
+ * 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.geode.management.internal.cli.result;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.apache.geode.management.internal.cli.GfshParser;
+import org.apache.geode.management.internal.cli.json.GfJsonObject;
+import org.apache.geode.management.internal.cli.result.model.AbstractResultModel;
+import org.apache.geode.management.internal.cli.result.model.DataResultModel;
+import org.apache.geode.management.internal.cli.result.model.InfoResultModel;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.cli.result.model.TabularResultModel;
+
+public class ModelCommandResult implements CommandResult {
+
+  private ResultModel result;
+  private List<String> commandOutput;
+  private int commandOutputIndex;
+  private Object configObject;
+  private static final Map<String, List<String>> EMPTY_TABLE_MAP = new LinkedHashMap<>();
+  private static final List<String> EMPTY_LIST = new ArrayList<>();
+
+  public ModelCommandResult(ResultModel result) {
+    this.result = result;
+  }
+
+  @Override
+  public Path getFileToDownload() {
+    return null;
+  }
+
+  @Override
+  public boolean hasFileToDownload() {
+    return false;
+  }
+
+  @Override
+  public Status getStatus() {
+    return result.getStatus();
+  }
+
+  public void setStatus(Status status) {}
+
+  @Override
+  public ResultModel getResultData() {
+    return result;
+  }
+
+  @Override
+  public void resetToFirstLine() {
+    commandOutputIndex = 0;
+  }
+
+  @Override
+  public boolean hasIncomingFiles() {
+    return false;
+  }
+
+  @Override
+  public int getNumTimesSaved() {
+    return 0;
+  }
+
+  @Override
+  public void saveIncomingFiles(String directory) throws IOException {
+
+  }
+
+  @JsonIgnore
+  @Override
+  public Object getConfigObject() {
+    return configObject;
+  }
+
+  @JsonIgnore
+  @Override
+  public void setConfigObject(Object configObject) {
+    this.configObject = configObject;
+  }
+
+  @Override
+  public boolean hasNextLine() {
+    if (commandOutput == null) {
+      buildCommandOutput();
+    }
+    return commandOutputIndex < commandOutput.size();
+  }
+
+  @Override
+  public String nextLine() {
+    if (commandOutput == null) {
+      buildCommandOutput();
+    }
+    return commandOutput.get(commandOutputIndex++);
+  }
+
+  @Override
+  public String toJson() {
+    ObjectMapper mapper = new ObjectMapper();
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    try {
+      mapper.writeValue(baos, getResultData());
+    } catch (IOException e) {
+      return e.getMessage();
+    }
+
+    return baos.toString();
+  }
+
+  @Override
+  public String getType() {
+    return "model";
+  }
+
+  @Override
+  public String getHeader() {
+    return result.getHeader();
+  }
+
+  @Override
+  public String getHeader(GfJsonObject gfJsonObject) {
+    throw new IllegalArgumentException("Cannot pass GfJsonObject to ModelCommandResult");
+  }
+
+  @Override
+  public GfJsonObject getContent() {
+    throw new IllegalArgumentException("Cannot use GfJsonObject from ModelCommandResult");
+  }
+
+  @Override
+  public String getMessageFromContent() {
+    List<InfoResultModel> infos = result.getInfoSections();
+    if (infos.size() == 0) {
+      return "";
+    }
+
+    List<String> messages = infos.get(0).getContent();
+    return messages.stream().collect(Collectors.joining(". "));
+  }
+
+  @Override
+  public String getErrorMessage() {
+    return getMessageFromContent();
+  }
+
+  @Override
+  public String getValueFromContent(String key) {
+    return null;
+  }
+
+  @Override
+  public List<String> getListFromContent(String key) {
+    return null;
+  }
+
+  @Override
+  public List<String> getColumnFromTableContent(String column, String tableId) {
+    TabularResultModel table = result.getTableSection(tableId);
+    if (table == null) {
+      return EMPTY_LIST;
+    }
+
+    return table.getContent().get(column);
+  }
+
+  @Override
+  public Map<String, List<String>> getMapFromTableContent(String tableId) {
+    TabularResultModel table = result.getTableSection(tableId);
+    if (table == null) {
+      return EMPTY_TABLE_MAP;
+    }
+
+    return table.getContent();
+  }
+
+  @Override
+  public Map<String, String> getMapFromSection(String sectionID) {
+    return result.getDataSection(sectionID).getContent();
+  }
+
+  @Override
+  public String getFooter() {
+    return result.getFooter();
+  }
+
+  @Override
+  public boolean failedToPersist() {
+    return false;
+  }
+
+  @Override
+  public void setCommandPersisted(boolean commandPersisted) {
+
+  }
+
+  @Override
+  public void setFileToDownload(Path fileToDownload) {
+
+  }
+
+  // Convenience implementation using the first table found
+  @Override
+  public List<String> getTableColumnValues(String columnName) {
+    List<TabularResultModel> tables = result.getTableSections();
+    if (tables.size() == 0) {
+      return EMPTY_LIST;
+    }
+
+    return tables.get(0).getContent().get(columnName);
+  }
+
+  @Override
+  public List<String> getTableColumnValues(String sectionId, String columnName) {
+    return result.getTableSection(sectionId).getContent().get(columnName);
+  }
+
+  // same as legacy buildData()
+  private void buildCommandOutput() {
+    commandOutputIndex = 0;
+    commandOutput = new ArrayList<>();
+    TableBuilder.Table resultTable = TableBuilder.newTable();
+
+    addSpacedRowInTable(resultTable, result.getHeader());
+
+    for (AbstractResultModel section : result.getContent().values()) {
+      if (section instanceof DataResultModel) {
+        buildData(resultTable, (DataResultModel) section);
+      } else if (section instanceof TabularResultModel) {
+        buildTabularCommandOutput(resultTable, (TabularResultModel) section);
+      } else if (section instanceof InfoResultModel) {
+        buildInfoOrErrorCommandOutput(resultTable, (InfoResultModel) section);
+      } else {
+        throw new IllegalArgumentException(
+            "Unable to process output for " + section.getClass().getName());
+      }
+    }
+
+    addSpacedRowInTable(resultTable, result.getFooter());
+
+    commandOutput.addAll(resultTable.buildTableList());
+  }
+
+  private void addHeaderInTable(TableBuilder.Table resultTable, ResultModel model) {
+    String header = model.getHeader();
+    if (header != null && !header.isEmpty()) {
+      resultTable.newRow().newLeftCol(header);
+      resultTable.newRow().newLeftCol("");
+    }
+  }
+
+  private void addSpacedRowInTable(TableBuilder.Table resultTable, String row) {
+    if (row != null && !row.isEmpty()) {
+      resultTable.newRow().newLeftCol(row);
+      resultTable.newRow().newLeftCol("");
+    }
+  }
+
+  private void addRowInRowGroup(TableBuilder.RowGroup rowGroup, String row) {
+    if (row != null && !row.isEmpty()) {
+      rowGroup.newRow().newLeftCol(row);
+    }
+  }
+
+  private void buildTabularCommandOutput(TableBuilder.Table resultTable, TabularResultModel model) {
+    addSpacedRowInTable(resultTable, model.getHeader());
+
+    resultTable.setColumnSeparator("   ");
+    resultTable.setTabularResult(true);
+
+    TableBuilder.RowGroup rowGroup = resultTable.newRowGroup();
+    buildTable(rowGroup, model);
+
+    addSpacedRowInTable(resultTable, model.getFooter());
+  }
+
+  private void buildTable(TableBuilder.RowGroup rowGroup, TabularResultModel model) {
+    TableBuilder.Row headerRow = rowGroup.newRow();
+    rowGroup.setColumnSeparator(" | ");
+    rowGroup.newRowSeparator('-', false);
+
+    Map<String, List<String>> rows = model.getContent();
+    if (!rows.isEmpty()) {
+      // build table header first
+      rows.keySet().forEach(c -> headerRow.newCenterCol(c));
+
+      // each row should have the same number of entries, so just look at the first one
+      int rowCount = rows.values().iterator().next().size();
+      for (int i = 0; i < rowCount; i++) {
+        TableBuilder.Row oneRow = rowGroup.newRow();
+        for (String column : rows.keySet()) {
+          oneRow.newLeftCol(rows.get(column).get(i));
+        }
+      }
+    }
+  }
+
+  private void buildData(TableBuilder.Table resultTable, DataResultModel section) {
+    TableBuilder.RowGroup rowGroup = resultTable.newRowGroup();
+    rowGroup.setColumnSeparator(" : ");
+
+    addRowInRowGroup(rowGroup, section.getHeader());
+
+    // finally process map values
+    for (Map.Entry<String, String> entry : section.getContent().entrySet()) {
+      TableBuilder.Row newRow = rowGroup.newRow();
+      String key = entry.getKey();
+      String value = entry.getValue();
+      String[] values = entry.getValue().split(GfshParser.LINE_SEPARATOR);
+      if (values.length == 1) {
+        newRow.newLeftCol(key).newLeftCol(values[0]);
+      } else {
+        if (values.length != 0) { // possible when object == CliConstants.LINE_SEPARATOR
+          newRow.newLeftCol(key).newLeftCol(values[0]);
+          for (int i = 1; i < values.length; i++) {
+            newRow = rowGroup.newRow();
+            newRow.setColumnSeparator("   ");
+            newRow.newLeftCol("").newLeftCol(values[i]);
+          }
+        } else {
+          newRow.newLeftCol(key).newLeftCol("");
+        }
+      }
+    }
+    addRowInRowGroup(rowGroup, section.getFooter());
+  }
+
+  private void buildInfoOrErrorCommandOutput(TableBuilder.Table resultTable,
+      InfoResultModel model) {
+    TableBuilder.RowGroup rowGroup = resultTable.newRowGroup();
+
+    model.getContent().forEach(c -> rowGroup.newRow().newLeftCol(c));
+  }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultBuilder.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultBuilder.java
index ae98985..b677dab 100755
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultBuilder.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultBuilder.java
@@ -14,15 +14,18 @@
  */
 package org.apache.geode.management.internal.cli.result;
 
+import java.io.IOException;
 import java.nio.file.Paths;
 import java.util.List;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.commons.lang.StringUtils;
 
 import org.apache.geode.management.cli.Result;
 import org.apache.geode.management.internal.cli.functions.CliFunctionResult;
 import org.apache.geode.management.internal.cli.json.GfJsonException;
 import org.apache.geode.management.internal.cli.json.GfJsonObject;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
 
 /**
  * Provides methods for creating {@link Result} objects to return from Gfsh command functions
@@ -181,7 +184,7 @@ public class ResultBuilder {
    * @param resultData data to use to build Result
    */
   public static CommandResult buildResult(ResultData resultData) {
-    return new CommandResult(resultData);
+    return new LegacyCommandResult(resultData);
   }
 
   public static CommandResult buildResult(List<CliFunctionResult> functionResults) {
@@ -235,8 +238,13 @@ public class ResultBuilder {
     CommandResult result;
     try {
       GfJsonObject jsonObject = new GfJsonObject(json);
-      String contentType = jsonObject.getString("contentType");
+
+      if (jsonObject.has("legacy") && !jsonObject.getBoolean("legacy")) {
+        return createModelBasedCommandResult(json);
+      }
+
       GfJsonObject data = jsonObject.getJSONObject("data");
+      String contentType = jsonObject.getString("contentType");
 
       AbstractResultData resultData;
       if (ResultData.TYPE_TABULAR.equals(contentType)) {
@@ -274,6 +282,19 @@ public class ResultBuilder {
     return result;
   }
 
+  public static CommandResult createModelBasedCommandResult(String json) {
+    ObjectMapper mapper = new ObjectMapper();
+
+    ResultModel response;
+    try {
+      response = mapper.readValue(json, ResultModel.class);
+    } catch (IOException iox) {
+      throw new RuntimeException(iox);
+    }
+
+    return new ModelCommandResult(response);
+  }
+
   public static String resultAsString(Result result) {
     StringBuilder builder = new StringBuilder();
 
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java
index cc0b5ab..a4ca7eb 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java
@@ -18,8 +18,6 @@ import org.apache.geode.management.cli.Result.Status;
 import org.apache.geode.management.internal.cli.json.GfJsonObject;
 
 /**
- *
- *
  * @since GemFire 7.0
  */
 public interface ResultData {
@@ -37,11 +35,28 @@ public interface ResultData {
 
   String getFooter();
 
-  GfJsonObject getGfJsonObject();
-
-  String getType();
-
-  Status getStatus();
-
-  void setStatus(final Status status);
+  default GfJsonObject getGfJsonObject() {
+    throw new UnsupportedOperationException(
+        "This should never be called and only exists during migration from GfJsonObject to POJOs - use getContent() instead");
+  }
+
+  default String getType() {
+    throw new UnsupportedOperationException(
+        "This should never be called and only exists during migration from GfJsonObject to POJOs");
+  }
+
+  default Status getStatus() {
+    throw new UnsupportedOperationException(
+        "This should never be called and only exists during migration from GfJsonObject to POJOs");
+  }
+
+  default void setStatus(final Status status) {
+    throw new UnsupportedOperationException(
+        "This should never be called and only exists during migration from GfJsonObject to POJOs");
+  }
+
+  default Object getContent() {
+    throw new UnsupportedOperationException(
+        "This should never be called from a legacy ResultData object");
+  }
 }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/TabularResultData.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/TabularResultData.java
index 92e3261..a1ece11 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/TabularResultData.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/TabularResultData.java
@@ -66,7 +66,7 @@ public class TabularResultData extends AbstractResultData {
       return 0;
     }
 
-    return jsonArray.getInternalJsonArray().length();
+    return jsonArray.size();
   }
 
   /**
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/AbstractResultModel.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/AbstractResultModel.java
new file mode 100644
index 0000000..aee54aa
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/AbstractResultModel.java
@@ -0,0 +1,53 @@
+/*
+ * 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.geode.management.internal.cli.result.model;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import org.apache.geode.management.cli.Result;
+
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY,
+    property = "modelClass")
+@JsonSubTypes({@JsonSubTypes.Type(value = TabularResultModel.class),
+    @JsonSubTypes.Type(value = DataResultModel.class),
+    @JsonSubTypes.Type(value = InfoResultModel.class)})
+public abstract class AbstractResultModel {
+
+  private String header = "";
+
+  private String footer = "";
+
+  private Result.Status status = Result.Status.OK;
+
+  public abstract Object getContent();
+
+  public String getHeader() {
+    return header;
+  }
+
+  public void setHeader(String header) {
+    this.header = header;
+  }
+
+  public String getFooter() {
+    return footer;
+  }
+
+  public void setFooter(String footer) {
+    this.footer = footer;
+  }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/DataResultModel.java
similarity index 55%
copy from geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java
copy to geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/DataResultModel.java
index cc0b5ab..e3a9f0a 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/DataResultModel.java
@@ -12,36 +12,29 @@
  * or implied. See the License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.geode.management.internal.cli.result;
 
-import org.apache.geode.management.cli.Result.Status;
-import org.apache.geode.management.internal.cli.json.GfJsonObject;
-
-/**
- *
- *
- * @since GemFire 7.0
- */
-public interface ResultData {
-  String RESULT_HEADER = "header";
-  String RESULT_CONTENT = "content";
-  String RESULT_FOOTER = "footer";
+package org.apache.geode.management.internal.cli.result.model;
 
+import java.util.LinkedHashMap;
+import java.util.Map;
 
-  String TYPE_COMPOSITE = "composite";
-  String TYPE_ERROR = "error";
-  String TYPE_INFO = "info";
-  String TYPE_TABULAR = "table";
+public class DataResultModel extends AbstractResultModel {
 
-  String getHeader();
+  private Map<String, String> data = new LinkedHashMap<>();
 
-  String getFooter();
+  DataResultModel() {}
 
-  GfJsonObject getGfJsonObject();
+  @Override
+  public Map<String, String> getContent() {
+    return data;
+  }
 
-  String getType();
+  public void setContent(Map<String, String> content) {
+    this.data = content;
+  }
 
-  Status getStatus();
+  public void addData(String key, Object value) {
+    data.put(key, value != null ? value.toString() : "");
+  }
 
-  void setStatus(final Status status);
 }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/InfoResultModel.java
similarity index 55%
copy from geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java
copy to geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/InfoResultModel.java
index cc0b5ab..09fb6dd 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/InfoResultModel.java
@@ -12,36 +12,32 @@
  * or implied. See the License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.geode.management.internal.cli.result;
 
-import org.apache.geode.management.cli.Result.Status;
-import org.apache.geode.management.internal.cli.json.GfJsonObject;
+package org.apache.geode.management.internal.cli.result.model;
+
+import java.util.ArrayList;
+import java.util.List;
 
-/**
- *
- *
- * @since GemFire 7.0
- */
-public interface ResultData {
-  String RESULT_HEADER = "header";
-  String RESULT_CONTENT = "content";
-  String RESULT_FOOTER = "footer";
 
 
-  String TYPE_COMPOSITE = "composite";
-  String TYPE_ERROR = "error";
-  String TYPE_INFO = "info";
-  String TYPE_TABULAR = "table";
+public class InfoResultModel extends AbstractResultModel {
 
-  String getHeader();
+  private List<String> messages = new ArrayList<>();
 
-  String getFooter();
+  InfoResultModel() {}
 
-  GfJsonObject getGfJsonObject();
+  @Override
+  public List<String> getContent() {
+    return messages;
+  }
 
-  String getType();
+  public void setContent(List<String> messages) {
+    this.messages = messages;
+  }
 
-  Status getStatus();
+  public InfoResultModel addLine(String line) {
+    messages.add(line);
+    return this;
+  }
 
-  void setStatus(final Status status);
 }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/ResultModel.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/ResultModel.java
new file mode 100644
index 0000000..be698f7
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/ResultModel.java
@@ -0,0 +1,184 @@
+/*
+ * 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.geode.management.internal.cli.result.model;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import org.apache.geode.management.cli.GfshCommand;
+import org.apache.geode.management.cli.Result;
+
+/**
+ * This class is the primary container for results returned from a {@link GfshCommand}.
+ * <br/>
+ * The following different types of 'models' can be added to an instance of {@code ResultModel}.
+ * <ol>
+ * <li>{@code InfoResultModel}</li>
+ * This model holds multiple lines of text.
+ * <li>{@code TabularResultModel}</li>
+ * This model holds a table of named columns and associated row values.
+ * <li>{@code DataResultModel}</li>
+ * This model holds a map of key/value pairs
+ * </ol>
+ * The order in which models are added is maintained and will be presented to the user in the same
+ * order.
+ * <br/>
+ * Errors should just be added as {@code InfoResultModel}s and then the status should be set
+ * appropriately to indicate an error.
+ *
+ */
+public class ResultModel {
+
+  private String header;
+  private String footer;
+  private Map<String, AbstractResultModel> sections = new LinkedHashMap<>();
+  private int sectionCount = 0;
+  private Result.Status status = Result.Status.OK;
+  private Object configObject;
+
+  @JsonIgnore
+  public Object getConfigObject() {
+    return configObject;
+  }
+
+  public boolean getLegacy() {
+    return false;
+  }
+
+  public void setLegacy(boolean legacy) {
+    // no-op
+  }
+
+  public void setConfigObject(Object configObject) {
+    this.configObject = configObject;
+  }
+
+  @JsonIgnore
+  public boolean isSuccessful() {
+    return status == Result.Status.OK;
+  }
+
+  public void setStatus(Result.Status status) {
+    this.status = status;
+  }
+
+  public Result.Status getStatus() {
+    return status;
+  }
+
+  public String getHeader() {
+    return header;
+  }
+
+  public void setHeader(String header) {
+    this.header = header;
+  }
+
+  public String getFooter() {
+    return footer;
+  }
+
+  public void setFooter(String footer) {
+    this.footer = footer;
+  }
+
+  public Map<String, AbstractResultModel> getContent() {
+    return sections;
+  }
+
+  public void setContent(Map<String, AbstractResultModel> content) {
+    this.sections = content;
+  }
+
+  public InfoResultModel addInfo() {
+    return addInfo(Integer.toString(sectionCount++));
+  }
+
+  public InfoResultModel addInfo(String namedSection) {
+    InfoResultModel section = new InfoResultModel();
+    sections.put(namedSection, section);
+
+    return section;
+  }
+
+  @JsonIgnore
+  public List<InfoResultModel> getInfoSections() {
+    return sections.values().stream().filter(InfoResultModel.class::isInstance)
+        .map(InfoResultModel.class::cast).collect(Collectors.toList());
+  }
+
+  public TabularResultModel addTable() {
+    return addTable(Integer.toString(sectionCount++));
+  }
+
+  public TabularResultModel addTable(String namedSection) {
+    TabularResultModel section = new TabularResultModel();
+    sections.put(namedSection, section);
+
+    return section;
+  }
+
+  @JsonIgnore
+  public List<TabularResultModel> getTableSections() {
+    return sections.values().stream().filter(TabularResultModel.class::isInstance)
+        .map(TabularResultModel.class::cast).collect(Collectors.toList());
+  }
+
+  public TabularResultModel getTableSection(String name) {
+    return (TabularResultModel) sections.get(name);
+  }
+
+  public DataResultModel addData() {
+    return addData(Integer.toString(sectionCount++));
+  }
+
+  public DataResultModel addData(String namedSection) {
+    DataResultModel section = new DataResultModel();
+    sections.put(namedSection, section);
+
+    return section;
+  }
+
+  @JsonIgnore
+  public List<DataResultModel> getDataSections() {
+    return sections.values().stream().filter(DataResultModel.class::isInstance)
+        .map(DataResultModel.class::cast).collect(Collectors.toList());
+  }
+
+  public DataResultModel getDataSection(String name) {
+    return (DataResultModel) sections.get(name);
+  }
+
+  /**
+   * Convenience method which creates an {@code InfoResultModel} section. The provided message is
+   * prepended with the string "Error processing command:". The status will be set to
+   * {@code Result.Status.ERROR}
+   */
+  public ResultModel createCommandProcessingError(String message) {
+    return createError("Error processing command: " + message);
+  }
+
+  public ResultModel createError(String message) {
+    addInfo().addLine(message);
+    setStatus(Result.Status.ERROR);
+
+    return this;
+  }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/TabularResultModel.java
similarity index 50%
copy from geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java
copy to geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/TabularResultModel.java
index cc0b5ab..9833699 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/ResultData.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/result/model/TabularResultModel.java
@@ -12,36 +12,37 @@
  * or implied. See the License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.geode.management.internal.cli.result;
 
-import org.apache.geode.management.cli.Result.Status;
-import org.apache.geode.management.internal.cli.json.GfJsonObject;
+package org.apache.geode.management.internal.cli.result.model;
 
-/**
- *
- *
- * @since GemFire 7.0
- */
-public interface ResultData {
-  String RESULT_HEADER = "header";
-  String RESULT_CONTENT = "content";
-  String RESULT_FOOTER = "footer";
-
-
-  String TYPE_COMPOSITE = "composite";
-  String TYPE_ERROR = "error";
-  String TYPE_INFO = "info";
-  String TYPE_TABULAR = "table";
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 
-  String getHeader();
+public class TabularResultModel extends AbstractResultModel {
 
-  String getFooter();
+  /*
+   * Table data mapped by column name. The map needs to be able to maintain insertion order.
+   */
+  private Map<String, List<String>> table = new LinkedHashMap<>();
 
-  GfJsonObject getGfJsonObject();
+  TabularResultModel() {}
 
-  String getType();
+  public TabularResultModel accumulate(String column, String value) {
+    if (table.containsKey(column)) {
+      table.get(column).add(value);
+    } else {
+      List<String> list = new ArrayList<>();
+      list.add(value);
+      table.put(column, list);
+    }
 
-  Status getStatus();
+    return this;
+  }
 
-  void setStatus(final Status status);
+  @Override
+  public Map<String, List<String>> getContent() {
+    return table;
+  }
 }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/shell/GfshExecutionStrategy.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/shell/GfshExecutionStrategy.java
index 13ee449..6fd6143 100755
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/shell/GfshExecutionStrategy.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/shell/GfshExecutionStrategy.java
@@ -39,7 +39,9 @@ import org.apache.geode.management.internal.cli.LogWrapper;
 import org.apache.geode.management.internal.cli.i18n.CliStrings;
 import org.apache.geode.management.internal.cli.remote.CommandExecutor;
 import org.apache.geode.management.internal.cli.result.FileResult;
+import org.apache.geode.management.internal.cli.result.ModelCommandResult;
 import org.apache.geode.management.internal.cli.result.ResultBuilder;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
 import org.apache.geode.security.NotAuthorizedException;
 
 /**
@@ -168,9 +170,17 @@ public class GfshExecutionStrategy implements ExecutionStrategy {
         return ResultBuilder.createBadConfigurationErrorResult("Interceptor Configuration Error");
       }
 
-      Result preExecResult = interceptor.preExecution(parseResult);
-      if (Status.ERROR.equals(preExecResult.getStatus())) {
-        return preExecResult;
+      Object preExecResult = interceptor.preExecution(parseResult);
+      if (preExecResult instanceof ResultModel) {
+        if (((ResultModel) preExecResult).getStatus() != Status.OK) {
+          return new ModelCommandResult((ResultModel) preExecResult);
+        }
+      }
+
+      if (preExecResult instanceof Result) {
+        if (Status.ERROR.equals(((Result) preExecResult).getStatus())) {
+          return (Result) preExecResult;
+        }
       }
 
       // when the preExecution yields a FileResult, we will get the fileData out of it
@@ -205,24 +215,30 @@ public class GfshExecutionStrategy implements ExecutionStrategy {
     // the response could be a string which is a json representation of the CommandResult object
     // it can also be a Path to a temp file downloaded from the rest http request
     if (response instanceof String) {
-      CommandResponse commandResponse =
-          CommandResponseBuilder.prepareCommandResponseFromJson((String) response);
+      try {
+        // TODO: stuff when failedToPersist...
+        // TODO: stuff for debug info...
+        commandResult = ResultBuilder.createModelBasedCommandResult((String) response);
+      } catch (Exception ex) {
+        CommandResponse commandResponse =
+            CommandResponseBuilder.prepareCommandResponseFromJson((String) response);
 
-      if (commandResponse.isFailedToPersist()) {
-        shell.printAsSevere(CliStrings.SHARED_CONFIGURATION_FAILED_TO_PERSIST_COMMAND_CHANGES);
-        logWrapper.severe(CliStrings.SHARED_CONFIGURATION_FAILED_TO_PERSIST_COMMAND_CHANGES);
-      }
+        if (commandResponse.isFailedToPersist()) {
+          shell.printAsSevere(CliStrings.SHARED_CONFIGURATION_FAILED_TO_PERSIST_COMMAND_CHANGES);
+          logWrapper.severe(CliStrings.SHARED_CONFIGURATION_FAILED_TO_PERSIST_COMMAND_CHANGES);
+        }
 
-      String debugInfo = commandResponse.getDebugInfo();
-      if (StringUtils.isNotBlank(debugInfo)) {
-        debugInfo = debugInfo.replaceAll("\n\n\n", "\n");
-        debugInfo = debugInfo.replaceAll("\n\n", "\n");
-        debugInfo =
-            debugInfo.replaceAll("\n", "\n[From Manager : " + commandResponse.getSender() + "]");
-        debugInfo = "[From Manager : " + commandResponse.getSender() + "]" + debugInfo;
-        this.logWrapper.info(debugInfo);
+        String debugInfo = commandResponse.getDebugInfo();
+        if (StringUtils.isNotBlank(debugInfo)) {
+          debugInfo = debugInfo.replaceAll("\n\n\n", "\n");
+          debugInfo = debugInfo.replaceAll("\n\n", "\n");
+          debugInfo =
+              debugInfo.replaceAll("\n", "\n[From Manager : " + commandResponse.getSender() + "]");
+          debugInfo = "[From Manager : " + commandResponse.getSender() + "]" + debugInfo;
+          this.logWrapper.info(debugInfo);
+        }
+        commandResult = ResultBuilder.fromJson((String) response);
       }
-      commandResult = ResultBuilder.fromJson((String) response);
     } else if (response instanceof Path) {
       tempFile = (Path) response;
     }
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommandTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommandTest.java
index e20fdba..9b8e6a9 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommandTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommandTest.java
@@ -91,7 +91,7 @@ public class CreateRegionCommandTest {
         parser.executeCommandWithInstance(command, "create region --name=region");
     assertThat(result.getStatus()).isEqualTo(Result.Status.ERROR);
     assertThat(result.getMessageFromContent())
-        .contains("One of \\\"type\\\" or \\\"template-region\\\" is required.");
+        .contains("One of \"type\" or \"template-region\" is required.");
   }
 
   @Test
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeRegionDUnitTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeRegionDUnitTest.java
index e7234fb..e926f40 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeRegionDUnitTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeRegionDUnitTest.java
@@ -170,16 +170,16 @@ public class DescribeRegionDUnitTest {
     CommandResult result = gfsh.executeAndAssertThat("describe region --name=" + PR1)
         .statusIsSuccess().getCommandResult();
 
-    List<String> names = result.getColumnFromTableContent("Name", 0, 0);
+    List<String> names = result.getColumnFromTableContent("Name", "0", "0");
     assertThat(names).containsOnlyOnce(RegionAttributesNames.ENTRY_IDLE_TIME_CUSTOM_EXPIRY);
 
-    List<String> values = result.getColumnFromTableContent("Value", 0, 0);
+    List<String> values = result.getColumnFromTableContent("Value", "0", "0");
     assertThat(values).containsOnlyOnce(TestCustomIdleExpiry.class.getName());
 
-    names = result.getColumnFromTableContent("Name", 0, 1);
+    names = result.getColumnFromTableContent("Name", "0", "1");
     assertThat(names).containsOnlyOnce(RegionAttributesNames.ENTRY_TIME_TO_LIVE_CUSTOM_EXPIRY);
 
-    values = result.getColumnFromTableContent("Value", 0, 1);
+    values = result.getColumnFromTableContent("Value", "0", "1");
     assertThat(values).containsOnlyOnce(TestCustomTTLExpiry.class.getName());
   }
 
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeRegionJUnitTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeRegionJUnitTest.java
index af19532..c185335 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeRegionJUnitTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeRegionJUnitTest.java
@@ -103,9 +103,9 @@ public class DescribeRegionJUnitTest {
             .doesNotContainOutput("Non-Default Attributes Specific To");
 
     Map<String, List<String>> shared =
-        commandAssert.getCommandResult().getMapFromTableContent(0, 0);
+        commandAssert.getCommandResult().getMapFromTableContent("0", "0");
     Map<String, List<String>> memberSpecific =
-        commandAssert.getCommandResult().getMapFromTableContent(0, 1);
+        commandAssert.getCommandResult().getMapFromTableContent("0", "1");
 
     assertThat(shared.get("Name")).contains("regKey", "evictKey", "partKey");
     assertThat(shared.get("Value")).contains("regVal", "evictVal", "partVal");
@@ -134,9 +134,9 @@ public class DescribeRegionJUnitTest {
             .doesNotContainOutput("Non-Default Attributes Specific To");
 
     Map<String, List<String>> shared =
-        commandAssert.getCommandResult().getMapFromTableContent(0, 0);
+        commandAssert.getCommandResult().getMapFromTableContent("0", "0");
     Map<String, List<String>> memberSpecific =
-        commandAssert.getCommandResult().getMapFromTableContent(0, 1);
+        commandAssert.getCommandResult().getMapFromTableContent("0", "1");
 
     assertThat(shared.get("Name")).contains("regKey", "evictKey", "partKey");
     assertThat(shared.get("Value")).contains("regVal", "evictVal", "partVal");
@@ -172,9 +172,9 @@ public class DescribeRegionJUnitTest {
         gfsh.executeAndAssertThat(command, COMMAND + " --name=" + regionName).statusIsSuccess();
 
     Map<String, List<String>> shared =
-        commandAssert.getCommandResult().getMapFromTableContent(0, 0);
+        commandAssert.getCommandResult().getMapFromTableContent("0", "0");
     Map<String, List<String>> memberSpecific =
-        commandAssert.getCommandResult().getMapFromTableContent(0, 1);
+        commandAssert.getCommandResult().getMapFromTableContent("0", "1");
 
     assertThat(shared.get("Type")).containsExactly("Eviction");
     assertThat(shared.get("Name")).containsExactly("sharedEvictionKey");
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DiskStoreCommandsDUnitTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DiskStoreCommandsDUnitTest.java
index 45f3aa6..65a8154 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DiskStoreCommandsDUnitTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/DiskStoreCommandsDUnitTest.java
@@ -124,7 +124,7 @@ public class DiskStoreCommandsDUnitTest {
     gfsh.executeAndAssertThat("show missing-disk-stores").statusIsSuccess()
         .containsOutput("Missing Disk Stores", "No missing colocated region found");
 
-    List<String> diskstoreIDs = gfsh.getCommandResult().getColumnValues("Disk Store ID");
+    List<String> diskstoreIDs = gfsh.getCommandResult().getTableColumnValues("Disk Store ID");
     assertThat(diskstoreIDs.size()).isEqualTo(1);
 
     gfsh.executeAndAssertThat("revoke missing-disk-store --id=" + diskstoreIDs.get(0))
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ExportLogsTestSuite.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ExportLogsTestSuite.java
deleted file mode 100644
index 94fb7f0..0000000
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ExportLogsTestSuite.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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.geode.management.internal.cli.commands;
-
-import org.junit.Ignore;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
-
-import org.apache.geode.management.internal.cli.functions.SizeExportLogsFunctionFileTest;
-import org.apache.geode.management.internal.cli.functions.SizeExportLogsFunctionTest;
-import org.apache.geode.management.internal.cli.util.LogExporterIntegrationTest;
-import org.apache.geode.management.internal.cli.util.LogExporterTest;
-import org.apache.geode.management.internal.cli.util.LogSizerTest;
-
-/**
- * All of the JUnit, DUnit and Integration tests for gfsh command export logs.
- */
-
-@Ignore
-@Suite.SuiteClasses({ExportLogsCommandTest.class, ExportLogsDUnitTest.class,
-    SizeExportLogsFunctionTest.class, SizeExportLogsFunctionFileTest.class, LogSizerTest.class,
-    LogExporterTest.class, LogExporterIntegrationTest.class, ExportLogsIntegrationTest.class})
-@RunWith(Suite.class)
-public class ExportLogsTestSuite {
-}
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListJndiBindingCommandDUnitTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListJndiBindingCommandDUnitTest.java
index 2022b70..7fa50ea 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListJndiBindingCommandDUnitTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListJndiBindingCommandDUnitTest.java
@@ -73,7 +73,7 @@ public class ListJndiBindingCommandDUnitTest {
 
     // member table
     List<String> jndiNames =
-        commandResultAssert.getCommandResult().getColumnFromTableContent("JNDI Name", 0, 1);
+        commandResultAssert.getCommandResult().getColumnFromTableContent("JNDI Name", "0", "1");
     assertThat(jndiNames.size()).isEqualTo(3);
     assertThat(jndiNames).containsExactlyInAnyOrder("java:jndi1", "java:UserTransaction",
         "java:TransactionManager");
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListMembersCommandDUnitTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListMembersCommandDUnitTest.java
index 034927c..923502d 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListMembersCommandDUnitTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListMembersCommandDUnitTest.java
@@ -19,6 +19,8 @@ import static org.apache.geode.management.internal.cli.i18n.CliStrings.LIST_MEMB
 import static org.apache.geode.test.junit.rules.GfshCommandRule.PortType.jmxManager;
 import static org.assertj.core.api.Assertions.assertThat;
 
+import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 
 import org.junit.BeforeClass;
@@ -27,6 +29,8 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
 
+import org.apache.geode.management.cli.Result;
+import org.apache.geode.management.internal.cli.result.CommandResult;
 import org.apache.geode.test.dunit.rules.ClusterStartupRule;
 import org.apache.geode.test.dunit.rules.MemberVM;
 import org.apache.geode.test.junit.categories.DistributedTest;
@@ -60,10 +64,14 @@ public class ListMembersCommandDUnitTest {
 
   @Test
   public void listAllMembers() throws Exception {
-    gfsh.executeAndAssertThat(LIST_MEMBER).statusIsSuccess().tableHasRowCount("Name", 4)
-        .tableHasColumnWithExactValuesInAnyOrder("Name", "locator-0", "server-1", "server-2",
-            "server-3")
-        .containsOutput("[Coordinator]");
+    CommandResult result = gfsh.executeCommand(LIST_MEMBER);
+
+    assertThat(result.getStatus()).isEqualTo(Result.Status.OK);
+
+    Map<String, List<String>> table = result.getMapFromTableContent("0");
+
+    assertThat(table.get("Name").size()).isEqualTo(4);
+    assertThat(table.get("Name")).contains("locator-0", "server-1", "server-2", "server-3");
   }
 
   @Test
@@ -98,8 +106,11 @@ public class ListMembersCommandDUnitTest {
 
   @Test
   public void listMembersInNonExistentGroup() throws Exception {
-    gfsh.executeAndAssertThat(LIST_MEMBER + " --group=foo").statusIsSuccess();
-    String output = gfsh.getGfshOutput();
+    CommandResult result = gfsh.executeCommand(LIST_MEMBER + " --group=foo");
+
+    assertThat(result.getStatus()).isEqualTo(Result.Status.OK);
+
+    String output = result.getMessageFromContent();
 
     assertThat(output).doesNotContain("locator-0");
     assertThat(output).doesNotContain("server-1");
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListMembersCommandTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListMembersCommandTest.java
index 6ea66fa..39cbb69 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListMembersCommandTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListMembersCommandTest.java
@@ -15,6 +15,7 @@
 
 package org.apache.geode.management.internal.cli.commands;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -22,6 +23,8 @@ import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import org.junit.Before;
@@ -30,6 +33,7 @@ import org.junit.Test;
 import org.junit.experimental.categories.Category;
 
 import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.management.internal.cli.result.CommandResult;
 import org.apache.geode.test.junit.categories.UnitTest;
 import org.apache.geode.test.junit.rules.GfshParserRule;
 
@@ -74,8 +78,11 @@ public class ListMembersCommandTest {
   public void basicListMembers() {
     members.add(member1);
 
-    gfsh.executeAndAssertThat(command, "list members").tableHasRowCount("Name", 1)
-        .tableHasRowWithValues("Name", "Id", "name", "id [Coordinator]").statusIsSuccess();
+    CommandResult result = gfsh.executeCommandWithInstance(command, "list members");
+    Map<String, List<String>> table = result.getMapFromTableContent("0");
+
+    assertThat(table.get("Name")).contains("name");
+    assertThat(table.get("Id")).contains("id [Coordinator]");
   }
 
   @Test
@@ -83,8 +90,11 @@ public class ListMembersCommandTest {
     members.add(member1);
     doReturn(null).when(command).getCoordinator();
 
-    gfsh.executeAndAssertThat(command, "list members").tableHasRowCount("Name", 1)
-        .tableHasRowWithValues("Name", "Id", "name", "id").statusIsSuccess();
+    CommandResult result = gfsh.executeCommandWithInstance(command, "list members");
+    Map<String, List<String>> table = result.getMapFromTableContent("0");
+
+    assertThat(table.get("Name")).contains("name");
+    assertThat(table.get("Id")).contains("id");
   }
 
   @Test
@@ -92,10 +102,10 @@ public class ListMembersCommandTest {
     members.add(member1);
     members.add(member2);
 
-    gfsh.executeAndAssertThat(command, "list members").tableHasRowCount("Name", 2)
-        .tableHasRowWithValues("Name", "Id", "name", "id [Coordinator]")
-        .tableHasRowWithValues("Name", "Id", "name2", "id2")
-        .tableHasColumnWithExactValuesInExactOrder("Name", member1.getName(), member2.getName())
-        .statusIsSuccess();
+    CommandResult result = gfsh.executeCommandWithInstance(command, "list members");
+    Map<String, List<String>> table = result.getMapFromTableContent("0");
+
+    assertThat(table.get("Name")).contains("name", "name2");
+    assertThat(table.get("Id")).contains("id [Coordinator]", "id2");
   }
 }
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/LogLevelInterceptorTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/LogLevelInterceptorTest.java
index ac04a61..988beae 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/LogLevelInterceptorTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/LogLevelInterceptorTest.java
@@ -53,7 +53,7 @@ public class LogLevelInterceptorTest {
     when(parseResult.getParamValueAsString("log-level")).thenReturn("test");
     when(parseResult.getParamValueAsString("loglevel")).thenReturn("test");
     for (AbstractCliAroundInterceptor interceptor : interceptors) {
-      result = interceptor.preExecution(parseResult);
+      result = (Result) interceptor.preExecution(parseResult);
       assertThat(result.nextLine()).contains("Invalid log level: test");
     }
   }
@@ -63,7 +63,7 @@ public class LogLevelInterceptorTest {
     when(parseResult.getParamValueAsString("log-level")).thenReturn("fine");
     when(parseResult.getParamValueAsString("loglevel")).thenReturn("fine");
     for (AbstractCliAroundInterceptor interceptor : interceptors) {
-      result = interceptor.preExecution(parseResult);
+      result = (Result) interceptor.preExecution(parseResult);
       assertThat(result.nextLine()).isEmpty();
     }
   }
@@ -73,7 +73,7 @@ public class LogLevelInterceptorTest {
     when(parseResult.getParamValueAsString("log-level")).thenReturn("trace");
     when(parseResult.getParamValueAsString("loglevel")).thenReturn("trace");
     for (AbstractCliAroundInterceptor interceptor : interceptors) {
-      result = interceptor.preExecution(parseResult);
+      result = (Result) interceptor.preExecution(parseResult);
       assertThat(result.nextLine()).isEmpty();
     }
   }
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/remote/OnlineCommandProcessorTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/remote/OnlineCommandProcessorTest.java
index 89a3e6c..8bd941e 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/remote/OnlineCommandProcessorTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/remote/OnlineCommandProcessorTest.java
@@ -59,7 +59,6 @@ public class OnlineCommandProcessorTest {
         new OnlineCommandProcessor(properties, securityService, executor, null);
   }
 
-
   @Test
   public void executeWithNullThrowsNPE() throws Exception {
     assertThatThrownBy(() -> onlineCommandProcessor.executeCommand(null))
@@ -73,13 +72,13 @@ public class OnlineCommandProcessorTest {
 
   @Test
   public void executeStripsComments() throws Exception {
-    Result commandResult = onlineCommandProcessor.executeCommand("/*comment*/");
+    Object commandResult = onlineCommandProcessor.executeCommand("/*comment*/");
     assertThat(commandResult).isNull();
   }
 
   @Test
   public void executeReturnsExecutorResult() throws Exception {
-    Result commandResult = onlineCommandProcessor.executeCommand("start locator");
+    Object commandResult = onlineCommandProcessor.executeCommand("start locator");
     assertThat(commandResult).isSameAs(result);
   }
 
@@ -92,7 +91,7 @@ public class OnlineCommandProcessorTest {
 
   @Test
   public void handlesParsingError() throws Exception {
-    Result commandResult = onlineCommandProcessor.executeCommand("foo --bar");
+    Object commandResult = onlineCommandProcessor.executeCommand("foo --bar");
     assertThat(commandResult).isInstanceOf(CommandResult.class);
     assertThat(commandResult.toString()).contains("Could not parse command string. foo --bar");
   }
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/result/CommandResultTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/result/CommandResultTest.java
index f5202a6..026b290 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/result/CommandResultTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/result/CommandResultTest.java
@@ -31,14 +31,14 @@ public class CommandResultTest {
 
   @Test
   public void emptyResultHasOneEmptyLine() {
-    CommandResult commandResult = new CommandResult(new InfoResultData());
+    CommandResult commandResult = new LegacyCommandResult(new InfoResultData());
     Assertions.assertThat(commandResult.nextLine()).isEqualTo("");
     Assertions.assertThat(commandResult.hasNextLine()).isFalse();
   }
 
   @Test
   public void resultWithOneLineHasOneLine() {
-    CommandResult commandResult = new CommandResult(new InfoResultData("oneLine"));
+    CommandResult commandResult = new LegacyCommandResult(new InfoResultData("oneLine"));
 
     assertThat(commandResult.nextLine()).isEqualTo("oneLine" + LINE_SEPARATOR);
     assertThat(commandResult.hasNextLine()).isFalse();
@@ -49,7 +49,7 @@ public class CommandResultTest {
     InfoResultData resultData = new InfoResultData();
     resultData.addLine("lineOne");
     resultData.addLine("lineTwo");
-    CommandResult commandResult = new CommandResult(resultData);
+    CommandResult commandResult = new LegacyCommandResult(resultData);
 
     assertThat(commandResult.nextLine())
         .isEqualTo("lineOne" + LINE_SEPARATOR + "lineTwo" + LINE_SEPARATOR);
@@ -58,14 +58,14 @@ public class CommandResultTest {
 
   @Test
   public void emptyResultDoesNotHaveFileToDownload() {
-    CommandResult commandResult = new CommandResult(new InfoResultData());
+    CommandResult commandResult = new LegacyCommandResult(new InfoResultData());
     Assertions.assertThat(commandResult.hasFileToDownload()).isFalse();
   }
 
   @Test
   public void resultWithFileDoesHaveFileToDownload() {
     Path fileToDownload = Paths.get(".").toAbsolutePath();
-    CommandResult commandResult = new CommandResult(fileToDownload);
+    CommandResult commandResult = new LegacyCommandResult(fileToDownload);
 
     assertThat(commandResult.hasFileToDownload()).isTrue();
     assertThat(commandResult.nextLine()).isEqualTo(fileToDownload.toString() + LINE_SEPARATOR);
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/result/model/LegacyVsResultModelComparisonTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/result/model/LegacyVsResultModelComparisonTest.java
new file mode 100644
index 0000000..81613ba
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/result/model/LegacyVsResultModelComparisonTest.java
@@ -0,0 +1,204 @@
+/*
+ * 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.geode.management.internal.cli.result.model;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.geode.management.cli.Result;
+import org.apache.geode.management.internal.cli.CommandResponseBuilder;
+import org.apache.geode.management.internal.cli.result.CommandResult;
+import org.apache.geode.management.internal.cli.result.CompositeResultData;
+import org.apache.geode.management.internal.cli.result.ErrorResultData;
+import org.apache.geode.management.internal.cli.result.ResultBuilder;
+import org.apache.geode.management.internal.cli.result.TabularResultData;
+import org.apache.geode.test.junit.categories.UnitTest;
+
+@Category(UnitTest.class)
+public class LegacyVsResultModelComparisonTest {
+
+  @Test
+  public void legacyTableComparison() {
+    // Create the legacy results
+    TabularResultData legacyTable = ResultBuilder.createTabularResultData();
+    legacyTable.setHeader("Heads");
+    legacyTable.accumulate("Name", "server1");
+    legacyTable.accumulate("Name", "server2");
+    legacyTable.accumulate("Id", "member name for server1");
+    legacyTable.accumulate("Id", "member name for server2");
+    legacyTable.setFooter("Tails");
+
+    Result legacyResult = ResultBuilder.buildResult(legacyTable);
+    String legacyString =
+        CommandResponseBuilder.createCommandResponseJson("server1", (CommandResult) legacyResult);
+
+    CommandResult clientLegacyResult = ResultBuilder.fromJson(legacyString);
+
+    // Create the new model results
+    ResultModel newCrm = new ResultModel();
+    TabularResultModel modelTable = newCrm.addTable();
+    modelTable.setHeader("Heads");
+    modelTable.accumulate("Name", "server1");
+    modelTable.accumulate("Name", "server2");
+    modelTable.accumulate("Id", "member name for server1");
+    modelTable.accumulate("Id", "member name for server2");
+    modelTable.setFooter("Tails");
+
+    String newModelString = CommandResponseBuilder.createCommandResponseJson("server1", newCrm);
+    CommandResult clientNewModelResult = ResultBuilder.fromJson(newModelString);
+
+    assertThat(clientNewModelResult.getTableColumnValues("0", "Name"))
+        .isEqualTo(clientLegacyResult.getTableColumnValues("Name"));
+
+    assertThat(readCommandOutput(clientNewModelResult))
+        .isEqualTo(readCommandOutput(clientLegacyResult));
+  }
+
+  @Test
+  public void legacyCompositeComparison() {
+    // Create the legacy results
+    CompositeResultData legacyCrd = ResultBuilder.createCompositeResultData();
+    legacyCrd.setHeader("Heads");
+    legacyCrd.setFooter("Tails");
+
+    // section-0 table-0
+    TabularResultData table1 = legacyCrd.addSection().addTable();
+    table1.setHeader("section-0 table-1 header");
+    table1.accumulate("Parameter", "param1");
+    table1.accumulate("Value", "value1");
+    table1.setFooter("section-0 table-1 footer");
+
+    // section-1
+    CompositeResultData.SectionResultData section1 = legacyCrd.addSection();
+    section1.setHeader("section 0 header");
+    section1.addSeparator('-');
+    section1.addData("param-1", "value-1");
+    section1.addData("param-3", "value-3");
+    section1.addData("param-2", "value-2");
+    section1.setFooter("section 0 footer");
+
+    // section-2
+    CompositeResultData.SectionResultData section2 = legacyCrd.addSection("named-section");
+    section2.setHeader("named section header");
+    section2.addSeparator('-');
+    section2.addData("param-A", "value-B");
+
+    Result legacyResult = ResultBuilder.buildResult(legacyCrd);
+    String legacyString =
+        CommandResponseBuilder.createCommandResponseJson("server1", (CommandResult) legacyResult);
+
+    CommandResult clientLegacyResult = ResultBuilder.fromJson(legacyString);
+
+    // Create the new model results
+    ResultModel newCrm = new ResultModel();
+    newCrm.setHeader("Heads");
+    newCrm.setFooter("Tails");
+
+    TabularResultModel newTable1 = newCrm.addTable();
+    newTable1.setHeader("section-0 table-1 header");
+    newTable1.accumulate("Parameter", "param1");
+    newTable1.accumulate("Value", "value1");
+    newTable1.setFooter("section-0 table-1 footer");
+
+    DataResultModel newSection1 = newCrm.addData();
+    newSection1.setHeader("section 0 header");
+    newSection1.addData("param-1", "value-1");
+    newSection1.addData("param-3", "value-3");
+    newSection1.addData("param-2", "value-2");
+    newSection1.setFooter("section 0 footer");
+
+    DataResultModel newSection2 = newCrm.addData("named-section");
+    newSection2.setHeader("named section header");
+    newSection2.addData("param-A", "value-B");
+
+    String newModelString = CommandResponseBuilder.createCommandResponseJson("server1", newCrm);
+    CommandResult clientNewModelResult = ResultBuilder.fromJson(newModelString);
+
+    assertThat(clientLegacyResult.getMapFromTableContent("0", "0"))
+        .containsAllEntriesOf(clientNewModelResult.getMapFromTableContent("0"));
+
+    assertThat(clientLegacyResult.getMapFromSection("1"))
+        .containsAllEntriesOf(clientNewModelResult.getMapFromSection("1"));
+  }
+
+  @Test
+  public void legacyErrorComparison() {
+    // Create the legacy results
+    ErrorResultData legacyError = ResultBuilder.createErrorResultData();
+    legacyError.addLine("This is a bad line");
+    legacyError.addLine("This is another bad line");
+
+    Result legacyResult = ResultBuilder.buildResult(legacyError);
+    String legacyString =
+        CommandResponseBuilder.createCommandResponseJson("server1", (CommandResult) legacyResult);
+
+    CommandResult legacyErrorResult = ResultBuilder.fromJson(legacyString);
+
+    // Create the new model results
+    ResultModel newCrm = new ResultModel();
+    newCrm.createError("This is a bad line");
+    newCrm.getInfoSections().get(0).addLine("This is another bad line");
+
+    String newModelString = CommandResponseBuilder.createCommandResponseJson("server1", newCrm);
+    CommandResult newErrorModelResult = ResultBuilder.fromJson(newModelString);
+
+    assertThat(legacyErrorResult.getMessageFromContent())
+        .isEqualTo(newErrorModelResult.getErrorMessage());
+
+    assertThat(readCommandOutput(newErrorModelResult))
+        .isEqualTo(readCommandOutput(legacyErrorResult));
+  }
+
+  @Test
+  public void legacyUserErrorComparison() {
+    // Create the legacy results
+    Result legacyResult = ResultBuilder.createUserErrorResult("This is an error message");
+    String legacyString =
+        CommandResponseBuilder.createCommandResponseJson("server1", (CommandResult) legacyResult);
+
+    CommandResult legacyErrorResult = ResultBuilder.fromJson(legacyString);
+
+    // Create the new model results
+    ResultModel newCrm = new ResultModel();
+    newCrm.createError("This is an error message");
+
+    String newModelString = CommandResponseBuilder.createCommandResponseJson("server1", newCrm);
+    CommandResult newErrorModelResult = ResultBuilder.fromJson(newModelString);
+
+    assertThat(legacyErrorResult.getMessageFromContent())
+        .isEqualTo(newErrorModelResult.getErrorMessage());
+
+    assertThat(readCommandOutput(newErrorModelResult))
+        .isEqualTo(readCommandOutput(legacyErrorResult));
+  }
+
+  private List<String> readCommandOutput(CommandResult cmd) {
+    List<String> result = new ArrayList<>();
+    while (cmd.hasNextLine()) {
+      String line = cmd.nextLine();
+      if (!line.isEmpty()) {
+        result.add(line);
+      }
+    }
+
+    return result;
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/security/MultiGfshDUnitTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/security/MultiGfshDUnitTest.java
index d846197..3a38031 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/security/MultiGfshDUnitTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/security/MultiGfshDUnitTest.java
@@ -21,7 +21,6 @@ import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 import org.awaitility.Awaitility;
-import org.json.JSONException;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -38,7 +37,6 @@ import org.apache.geode.test.dunit.IgnoredException;
 import org.apache.geode.test.dunit.VM;
 import org.apache.geode.test.dunit.rules.ClusterStartupRule;
 import org.apache.geode.test.junit.categories.DistributedTest;
-import org.apache.geode.test.junit.categories.FlakyTest;
 import org.apache.geode.test.junit.categories.SecurityTest;
 import org.apache.geode.test.junit.rules.GfshCommandRule;
 import org.apache.geode.test.junit.rules.GfshCommandRule.PortType;
@@ -58,9 +56,8 @@ public class MultiGfshDUnitTest {
         x -> x.withJMXManager().withSecurityManager(SimpleTestSecurityManager.class));
   }
 
-  @Category(FlakyTest.class) // GEODE-1579
   @Test
-  public void testMultiUser() throws JSONException, InterruptedException {
+  public void testMultiUser() throws Exception {
 
     IgnoredException.addIgnoredException("java.util.zip.ZipException: zip file is empty");
     IgnoredException
@@ -127,12 +124,18 @@ public class MultiGfshDUnitTest {
         LogService.getLogger().info("executing: " + command.getCommand());
 
         CommandResult result = gfsh.executeCommand(command.getCommand());
-        if (result.getResultData().getStatus() == Status.OK) {
+        if (result.getStatus() == Status.OK) {
           continue;
         }
+
+        int errorResultCode;
+        if (result.getResultData() instanceof ErrorResultData) {
+          errorResultCode = ((ErrorResultData) result.getResultData()).getErrorCode();
+        } else {
+          errorResultCode = 9999; // ((ResultModel) result.getResultData()).getErrorCode();
+        }
         assertNotEquals("Did not expect an Unauthorized exception: " + result.toString(),
-            ResultBuilder.ERRORCODE_UNAUTHORIZED,
-            ((ErrorResultData) result.getResultData()).getErrorCode());
+            ResultBuilder.ERRORCODE_UNAUTHORIZED, errorResultCode);
       }
       gfsh.close();
       LogService.getLogger().info("vm 3 done!");
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/web/controllers/ShellCommandsControllerProcessCommandTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/web/controllers/ShellCommandsControllerProcessCommandTest.java
index a0139dd..6c40bec 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/web/controllers/ShellCommandsControllerProcessCommandTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/web/controllers/ShellCommandsControllerProcessCommandTest.java
@@ -37,6 +37,7 @@ import org.apache.geode.management.internal.cli.CommandResponseBuilder;
 import org.apache.geode.management.internal.cli.result.CommandResult;
 import org.apache.geode.management.internal.cli.result.ErrorResultData;
 import org.apache.geode.management.internal.cli.result.InfoResultData;
+import org.apache.geode.management.internal.cli.result.LegacyCommandResult;
 import org.apache.geode.management.internal.cli.result.ResultBuilder;
 import org.apache.geode.test.junit.categories.IntegrationTest;
 
@@ -62,7 +63,7 @@ public class ShellCommandsControllerProcessCommandTest {
 
   @Test
   public void infoOkResult() throws IOException {
-    fakeResult = new CommandResult(new InfoResultData("Some info message"));
+    fakeResult = new LegacyCommandResult(new InfoResultData("Some info message"));
 
     ResponseEntity<InputStreamResource> responseJsonStream = controller.command("xyz", null);
     assertThatContentTypeEquals(responseJsonStream, MediaType.APPLICATION_JSON);
@@ -76,7 +77,7 @@ public class ShellCommandsControllerProcessCommandTest {
   @Test
   public void errorResult() throws IOException {
     ErrorResultData errorResultData = new ErrorResultData("Some error message");
-    fakeResult = new CommandResult(errorResultData);
+    fakeResult = new LegacyCommandResult(errorResultData);
 
     ResponseEntity<InputStreamResource> responseJsonStream = controller.command("xyz", null);
     assertThatContentTypeEquals(responseJsonStream, MediaType.APPLICATION_JSON);
@@ -92,7 +93,7 @@ public class ShellCommandsControllerProcessCommandTest {
     File tempFile = temporaryFolder.newFile();
     FileUtils.writeStringToFile(tempFile, "some file contents", "UTF-8");
 
-    fakeResult = new CommandResult(tempFile.toPath());
+    fakeResult = new LegacyCommandResult(tempFile.toPath());
 
     ResponseEntity<InputStreamResource> responseFileStream = controller.command("xyz", null);
 
diff --git a/geode-core/src/test/java/org/apache/geode/test/junit/assertions/CommandResultAssert.java b/geode-core/src/test/java/org/apache/geode/test/junit/assertions/CommandResultAssert.java
index 506eefb..7528c0b 100644
--- a/geode-core/src/test/java/org/apache/geode/test/junit/assertions/CommandResultAssert.java
+++ b/geode-core/src/test/java/org/apache/geode/test/junit/assertions/CommandResultAssert.java
@@ -147,7 +147,7 @@ public class CommandResultAssert
    */
   public CommandResultAssert tableHasColumnWithExactValuesInExactOrder(String header,
       String... expectedValues) {
-    List<String> actualValues = actual.getCommandResult().getColumnValues(header);
+    List<String> actualValues = actual.getCommandResult().getTableColumnValues(header);
     assertThat(actualValues).containsExactly(expectedValues);
 
     return this;
@@ -175,7 +175,7 @@ public class CommandResultAssert
    */
   public CommandResultAssert tableHasColumnWithExactValuesInAnyOrder(String header,
       String... expectedValues) {
-    List<String> actualValues = actual.getCommandResult().getColumnValues(header);
+    List<String> actualValues = actual.getCommandResult().getTableColumnValues(header);
     assertThat(actualValues).containsExactlyInAnyOrder(expectedValues);
 
     return this;
@@ -196,7 +196,7 @@ public class CommandResultAssert
     Map<String, List<String>> allValues = new HashMap<>();
     int numberOfRows = -1;
     for (String header : headers) {
-      List<String> columnValues = actual.getCommandResult().getColumnValues(header);
+      List<String> columnValues = actual.getCommandResult().getTableColumnValues(header);
       if (numberOfRows > 0) {
         assertThat(columnValues.size()).isEqualTo(numberOfRows);
       }
@@ -222,7 +222,7 @@ public class CommandResultAssert
   }
 
   public CommandResultAssert tableHasRowCount(String anyColumnHeader, int rowSize) {
-    assertThat(actual.getCommandResult().getColumnValues(anyColumnHeader).size())
+    assertThat(actual.getCommandResult().getTableColumnValues(anyColumnHeader).size())
         .isEqualTo(rowSize);
     return this;
   }
@@ -233,7 +233,7 @@ public class CommandResultAssert
    */
   public CommandResultAssert tableHasColumnWithValuesContaining(String header,
       String... expectedValues) {
-    List<String> actualValues = actual.getCommandResult().getColumnValues(header);
+    List<String> actualValues = actual.getCommandResult().getTableColumnValues(header);
 
     for (Object actualValue : actualValues) {
       String actualValueString = (String) actualValue;
@@ -254,7 +254,7 @@ public class CommandResultAssert
    * one of the expectedValues.
    */
   public CommandResultAssert tableHasColumnOnlyWithValues(String header, String... expectedValues) {
-    List<String> actualValues = actual.getCommandResult().getColumnValues(header);
+    List<String> actualValues = actual.getCommandResult().getTableColumnValues(header);
     assertThat(actualValues).containsOnly(expectedValues);
 
     return this;
diff --git a/geode-core/src/test/java/org/apache/geode/test/junit/rules/GfshCommandRule.java b/geode-core/src/test/java/org/apache/geode/test/junit/rules/GfshCommandRule.java
index 527662b..71fd317 100644
--- a/geode-core/src/test/java/org/apache/geode/test/junit/rules/GfshCommandRule.java
+++ b/geode-core/src/test/java/org/apache/geode/test/junit/rules/GfshCommandRule.java
@@ -249,7 +249,6 @@ public class GfshCommandRule extends DescribedExternalResource {
     return new CommandResultAssert(gfsh.outputString, commandResult);
   }
 
-
   public String getGfshOutput() {
     return gfsh.outputString;
   }
diff --git a/geode-core/src/test/java/org/apache/geode/test/junit/rules/GfshParserRule.java b/geode-core/src/test/java/org/apache/geode/test/junit/rules/GfshParserRule.java
index 3507479..4085639 100644
--- a/geode-core/src/test/java/org/apache/geode/test/junit/rules/GfshParserRule.java
+++ b/geode-core/src/test/java/org/apache/geode/test/junit/rules/GfshParserRule.java
@@ -33,7 +33,9 @@ import org.apache.geode.management.internal.cli.GfshParseResult;
 import org.apache.geode.management.internal.cli.GfshParser;
 import org.apache.geode.management.internal.cli.remote.CommandExecutor;
 import org.apache.geode.management.internal.cli.result.CommandResult;
+import org.apache.geode.management.internal.cli.result.ModelCommandResult;
 import org.apache.geode.management.internal.cli.result.ResultBuilder;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
 import org.apache.geode.test.junit.assertions.CommandResultAssert;
 
 public class GfshParserRule extends ExternalResource {
@@ -76,14 +78,25 @@ public class GfshParserRule extends ExternalResource {
           throw new RuntimeException(e);
         }
 
-        Result preExecResult = interceptor.preExecution(parseResult);
-        if (Result.Status.ERROR.equals(preExecResult.getStatus())) {
-          return (CommandResult) preExecResult;
+        Object preExecResult = interceptor.preExecution(parseResult);
+        if (preExecResult instanceof ResultModel) {
+          if (((ResultModel) preExecResult).getStatus() != Result.Status.OK) {
+            return new ModelCommandResult((ResultModel) preExecResult);
+          }
+        } else {
+          if (Result.Status.ERROR.equals(((Result) preExecResult).getStatus())) {
+            return (CommandResult) preExecResult;
+          }
         }
       }
     }
 
-    return (CommandResult) commandExecutor.execute(instance, parseResult);
+    Object exeResult = commandExecutor.execute(instance, parseResult);
+    if (exeResult instanceof ResultModel) {
+      return new ModelCommandResult((ResultModel) exeResult);
+    }
+
+    return (CommandResult) exeResult;
   }
 
   public <T> CommandResultAssert executeAndAssertThat(T instance, String command) {
diff --git a/geode-cq/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeClientCommandDUnitTest.java b/geode-cq/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeClientCommandDUnitTest.java
index 7834312..4805973 100644
--- a/geode-cq/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeClientCommandDUnitTest.java
+++ b/geode-cq/src/test/java/org/apache/geode/management/internal/cli/commands/DescribeClientCommandDUnitTest.java
@@ -15,769 +15,224 @@
 
 package org.apache.geode.management.internal.cli.commands;
 
-import static org.apache.geode.distributed.ConfigurationProperties.LOG_FILE;
-import static org.apache.geode.distributed.ConfigurationProperties.LOG_LEVEL;
-import static org.apache.geode.distributed.ConfigurationProperties.STATISTIC_ARCHIVE_FILE;
-import static org.apache.geode.distributed.ConfigurationProperties.STATISTIC_SAMPLING_ENABLED;
-import static org.apache.geode.management.internal.cli.commands.ClientCommandsTestUtils.getMember;
-import static org.apache.geode.management.internal.cli.commands.ClientCommandsTestUtils.getNonDurableClientProps;
-import static org.apache.geode.management.internal.cli.commands.ClientCommandsTestUtils.getServerProperties;
-import static org.apache.geode.management.internal.cli.commands.ClientCommandsTestUtils.setupCqsOnVM;
-import static org.apache.geode.test.dunit.LogWriterUtils.getLogWriter;
-import static org.apache.geode.test.dunit.NetworkUtils.getServerHostName;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.assertj.core.api.Assertions.assertThat;
 
+import java.io.Serializable;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.TimeUnit;
-
-import javax.management.ObjectName;
+import java.util.function.Consumer;
 
 import org.apache.commons.lang.exception.ExceptionUtils;
 import org.awaitility.Awaitility;
-import org.junit.Ignore;
+import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
 
-import org.apache.geode.cache.AttributesFactory;
 import org.apache.geode.cache.Cache;
-import org.apache.geode.cache.DataPolicy;
 import org.apache.geode.cache.Region;
-import org.apache.geode.cache.Scope;
+import org.apache.geode.cache.RegionFactory;
+import org.apache.geode.cache.RegionShortcut;
 import org.apache.geode.cache.client.ClientCache;
 import org.apache.geode.cache.client.ClientCacheFactory;
 import org.apache.geode.cache.client.ClientRegionFactory;
 import org.apache.geode.cache.client.ClientRegionShortcut;
-import org.apache.geode.cache.client.PoolManager;
-import org.apache.geode.cache.client.internal.PoolImpl;
-import org.apache.geode.cache.server.CacheServer;
-import org.apache.geode.distributed.DistributedMember;
-import org.apache.geode.internal.OSProcess;
-import org.apache.geode.internal.cache.DistributedRegion;
+import org.apache.geode.cache.query.CqAttributesFactory;
+import org.apache.geode.cache.query.QueryService;
 import org.apache.geode.internal.cache.GemFireCacheImpl;
-import org.apache.geode.internal.cache.tier.sockets.CacheServerTestUtil;
-import org.apache.geode.management.CacheServerMXBean;
-import org.apache.geode.management.ClientHealthStatus;
-import org.apache.geode.management.ManagementService;
+import org.apache.geode.internal.net.SocketCreator;
 import org.apache.geode.management.cli.Result;
-import org.apache.geode.management.internal.SystemManagementService;
-import org.apache.geode.management.internal.cli.LogWrapper;
 import org.apache.geode.management.internal.cli.i18n.CliStrings;
 import org.apache.geode.management.internal.cli.result.CommandResult;
-import org.apache.geode.management.internal.cli.result.CompositeResultData;
-import org.apache.geode.management.internal.cli.result.TabularResultData;
-import org.apache.geode.test.dunit.Host;
-import org.apache.geode.test.dunit.SerializableCallable;
-import org.apache.geode.test.dunit.SerializableRunnableIF;
-import org.apache.geode.test.dunit.VM;
+import org.apache.geode.test.dunit.rules.ClientVM;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
 import org.apache.geode.test.junit.categories.DistributedTest;
-import org.apache.geode.test.junit.categories.FlakyTest;
+import org.apache.geode.test.junit.rules.GfshCommandRule;
 
-@Category({DistributedTest.class, FlakyTest.class}) // GEODE-910 GEODE-3530
+@Category(DistributedTest.class)
 @SuppressWarnings("serial")
-public class DescribeClientCommandDUnitTest extends CliCommandTestBase {
+public class DescribeClientCommandDUnitTest {
 
-  private final String regionName = "stocks";
-  private final String cq1 = "cq1";
-  private final String cq2 = "cq2";
-  private final String cq3 = "cq3";
-  private String clientId = "";
-  private int port0 = 0;
-  private int port1 = 0;
+  private MemberVM locatorVm0;
+  private MemberVM server1Vm1;
+  private MemberVM server2Vm2;
+  private ClientVM client1Vm3;
+  private ClientVM client2Vm4;
 
-  @Ignore("disabled for unknown reason")
-  @Test
-  public void testDescribeClientWithServers3() throws Exception {
-    setupSystem3();
-    String commandString;
-
-    final VM server1 = Host.getHost(0).getVM(1);
-    final VM server2 = Host.getHost(0).getVM(3);
-    final VM manager = Host.getHost(0).getVM(0);
-
-    String serverName1 =
-        server1.invoke("get DistributedMemberID ", ClientCommandsTestUtils::getDistributedMemberId);
-    String serverName2 =
-        server2.invoke("get DistributedMemberID ", ClientCommandsTestUtils::getDistributedMemberId);
-
-    final DistributedMember serverMember1 = getMember(server1);
-
-    String[] clientIds = manager.invoke("get Client Ids", () -> {
-      final SystemManagementService service =
-          (SystemManagementService) ManagementService.getManagementService(getCache());
-      final ObjectName cacheServerMBeanName = service.getCacheServerMBeanName(port0, serverMember1);
-      CacheServerMXBean bean = service.getMBeanProxy(cacheServerMBeanName, CacheServerMXBean.class);
-      return bean.getClientIds();
-    });
+  private static final String STOCKS_REGION = "stocks";
 
-    String clientId1 = "";
+  @Rule
+  public ClusterStartupRule rule = new ClusterStartupRule(5);
 
-    for (String str : clientIds) {
-      clientId1 = str;
-      getLogWriter().info("testDescribeClientWithServers clientIds for server1 =" + str);
-    }
+  @Rule
+  public GfshCommandRule gfsh = new GfshCommandRule();
 
-    final DistributedMember serverMember2 = getMember(server2);
+  @Before
+  public void setup() throws Exception {
+    locatorVm0 = rule.startLocatorVM(0);
+    server1Vm1 = rule.startServerVM(1, locatorVm0.getPort());
+    server2Vm2 = rule.startServerVM(2, locatorVm0.getPort());
 
-    String[] clientIds2 = manager.invoke("get Client Ids", () -> {
-      final SystemManagementService service =
-          (SystemManagementService) ManagementService.getManagementService(getCache());
-      final ObjectName cacheServerMBeanName = service.getCacheServerMBeanName(port1, serverMember2);
-      CacheServerMXBean bean = service.getMBeanProxy(cacheServerMBeanName, CacheServerMXBean.class);
-      return bean.getClientIds();
+    server1Vm1.invoke(() -> {
+      RegionFactory factory =
+          ClusterStartupRule.getCache().createRegionFactory(RegionShortcut.REPLICATE);
+      factory.create(STOCKS_REGION);
     });
 
-    String clientId2 = "";
-
-    for (String str : clientIds2) {
-      clientId2 = str;
-      getLogWriter().info("testDescribeClientWithServers clientIds for server2 =" + str);
-    }
-
-    commandString = CliStrings.DESCRIBE_CLIENT + " --" + CliStrings.DESCRIBE_CLIENT__ID + "=\""
-        + clientId1 + "\"";
-    getLogWriter().info("testDescribeClientWithServers commandStr clientId1 =" + commandString);
-    CommandResult commandResultForClient1 = executeCommand(commandString);
-    getLogWriter()
-        .info("testDescribeClientWithServers commandStr clientId1=" + commandResultForClient1);
-    String resultAsString = commandResultToString(commandResultForClient1);
-    getLogWriter().info("testDescribeClientWithServers commandStr clientId1 =" + resultAsString);
-    assertTrue(Result.Status.OK.equals(commandResultForClient1.getStatus()));
-    ClientCommandsTestUtils.verifyClientStats(commandResultForClient1, serverName1);
-    commandString = CliStrings.DESCRIBE_CLIENT + " --" + CliStrings.DESCRIBE_CLIENT__ID + "=\""
-        + clientId2 + "\"";
-    getLogWriter().info("testDescribeClientWithServers commandStr1=" + commandString);
-    CommandResult commandResultForClient2 = executeCommand(commandString);
-    getLogWriter().info("testDescribeClientWithServers commandResult1=" + commandResultForClient2);
-    resultAsString = commandResultToString(commandResultForClient2);
-    getLogWriter().info("testDescribeClientWithServers resultAsString1=" + resultAsString);
-    assertTrue(Result.Status.OK.equals(commandResultForClient2.getStatus()));
-    ClientCommandsTestUtils.verifyClientStats(commandResultForClient2, serverName2);
-    ClientCommandsTestUtils.closeNonDurableClient(Host.getHost(0).getVM(2));
-    ClientCommandsTestUtils.closeCacheServer(Host.getHost(0).getVM(3));
-    ClientCommandsTestUtils.closeCacheServer(Host.getHost(0).getVM(1));
-  }
-
-  @Ignore("disabled for unknown reason")
-  @Test
-  public void testDescribeClient() throws Exception {
-    setupSystem1();
-
-    getLogWriter().info("testDescribeClient clientId=" + clientId);
-    assertNotNull(clientId);
-
-    String commandString = CliStrings.DESCRIBE_CLIENT + " --" + CliStrings.DESCRIBE_CLIENT__ID
-        + "=\"" + clientId + "\"";
-    getLogWriter().info("testDescribeClient commandStr=" + commandString);
-
-    final VM server1 = Host.getHost(0).getVM(1);
-
-    String serverName = server1.invoke("get distributed member Id",
-        ClientCommandsTestUtils::getDistributedMemberId);
-
-    CommandResult commandResult = executeCommand(commandString);
-    getLogWriter().info("testDescribeClient commandResult=" + commandResult);
-    String resultAsString = commandResultToString(commandResult);
-    getLogWriter().info("testDescribeClient resultAsString=" + resultAsString);
-    assertTrue(Result.Status.OK.equals(commandResult.getStatus()));
-
-    CompositeResultData resultData = (CompositeResultData) commandResult.getResultData();
-    CompositeResultData.SectionResultData section = resultData.retrieveSection("InfoSection");
-    assertNotNull(section);
-    TabularResultData tableResultData = section.retrieveTable("Pool Stats For Pool Name = DEFAULT");
-    assertNotNull(tableResultData);
-
-    List<String> minConn = tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_MIN_CONN);
-    List<String> maxConn = tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_MAX_CONN);
-    List<String> redundancy =
-        tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_REDUNDANCY);
-    List<String> numCqs = tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_CQs);
-
-    assertTrue(minConn.contains("1"));
-    assertTrue(maxConn.contains("-1"));
-    assertTrue(redundancy.contains("1"));
-    assertTrue(numCqs.contains("3"));
-    String puts = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PUTS);
-    assertTrue(puts.equals("2"));
-    String queue = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_QUEUE_SIZE);
-    assertTrue(queue.equals("1"));
-    String calls = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_LISTENER_CALLS);
-    assertTrue(calls.equals("1"));
-    String primServer = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PRIMARY_SERVERS);
-    assertTrue(primServer.equals(serverName));
-    String durable = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_DURABLE);
-    assertTrue(durable.equals("No"));
-    String threads = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_THREADS);
-    assertTrue(Integer.parseInt(threads) > 0);
-    String cpu = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_CPU);
-    assertTrue(Integer.parseInt(cpu) > 0);
-    String upTime = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_UP_TIME);
-    assertTrue(Integer.parseInt(upTime) >= 0);
-    String prcTime = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PROCESS_CPU_TIME);
-    assertTrue(Long.parseLong(prcTime) > 0);
-
-    ClientCommandsTestUtils.closeNonDurableClient(Host.getHost(0).getVM(2));
-    ClientCommandsTestUtils.closeCacheServer(Host.getHost(0).getVM(1));
-    ClientCommandsTestUtils.closeCacheServer(Host.getHost(0).getVM(3));
-  }
-
-  @Test
-  public void testDescribeClientWithServers() throws Exception {
-    setupSystem2();
-
-    String commandString = CliStrings.DESCRIBE_CLIENT + " --" + CliStrings.DESCRIBE_CLIENT__ID
-        + "=\"" + clientId + "\"";
-    getLogWriter().info("testDescribeClientWithServers commandStr=" + commandString);
-
-    final VM server1 = Host.getHost(0).getVM(1);
-
-    String serverName = server1.invoke("get Distributed Member Id",
-        ClientCommandsTestUtils::getDistributedMemberId);
-    CommandResult commandResult = executeCommand(commandString);
-    getLogWriter().info("testDescribeClientWithServers commandResult=" + commandResult);
-
-    String resultAsString = commandResultToString(commandResult);
-    getLogWriter().info("testDescribeClientWithServers resultAsString=" + resultAsString);
-    assertTrue(Result.Status.OK.equals(commandResult.getStatus()));
-
-    CompositeResultData resultData = (CompositeResultData) commandResult.getResultData();
-    CompositeResultData.SectionResultData section = resultData.retrieveSection("InfoSection");
-    assertNotNull(section);
-
-    TabularResultData tableResultData = section.retrieveTable("Pool Stats For Pool Name = DEFAULT");
-    assertNotNull(tableResultData);
-
-    List<String> minConn = tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_MIN_CONN);
-    List<String> maxConn = tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_MAX_CONN);
-    List<String> redundancy =
-        tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_REDUNDANCY);
-    List<String> numCqs = tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_CQs);
-
-    assertTrue(minConn.contains("1"));
-    assertTrue(maxConn.contains("-1"));
-    assertTrue(redundancy.contains("1"));
-    assertTrue(numCqs.contains("3"));
-    String puts = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PUTS);
-    assertTrue(puts.equals("2"));
-    String queue = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_QUEUE_SIZE);
-    assertTrue(queue.equals("1"));
-    String calls = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_LISTENER_CALLS);
-    assertTrue(calls.equals("1"));
-    String primServer = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PRIMARY_SERVERS);
-    assertTrue(primServer.equals(serverName));
-    String durable = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_DURABLE);
-    assertTrue(durable.equals("No"));
-    String threads = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_THREADS);
-    assertTrue(Integer.parseInt(threads) > 0);
-    String cpu = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_CPU);
-    assertTrue(Integer.parseInt(cpu) > 0);
-    String upTime = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_UP_TIME);
-    assertTrue(Integer.parseInt(upTime) >= 0);
-    String prcTime = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PROCESS_CPU_TIME);
-    assertTrue(Long.parseLong(prcTime) > 0);
-
-    ClientCommandsTestUtils.closeNonDurableClient(Host.getHost(0).getVM(2));
-    ClientCommandsTestUtils.closeNonDurableClient(Host.getHost(0).getVM(3));
-    ClientCommandsTestUtils.closeCacheServer(Host.getHost(0).getVM(1));
-  }
+    server2Vm2.invoke(() -> {
+      RegionFactory factory =
+          ClusterStartupRule.getCache().createRegionFactory(RegionShortcut.REPLICATE);
+      factory.create(STOCKS_REGION);
+    });
 
-  @Test // FlakyTest: GEODE-910
-  public void testDescribeClientForNonSubscribedClient() throws Exception {
-    setUpNonSubscribedClient();
-
-    getLogWriter().info("testDescribeClientForNonSubscribedClient clientId=" + clientId);
-    assertNotNull(clientId);
-
-    String commandString = CliStrings.DESCRIBE_CLIENT + " --" + CliStrings.DESCRIBE_CLIENT__ID
-        + "=\"" + clientId + "\"";
-    getLogWriter().info("testDescribeClientForNonSubscribedClient commandStr=" + commandString);
-    CommandResult commandResult = executeCommand(commandString);
-    getLogWriter().info("testDescribeClientForNonSubscribedClient commandResult=" + commandResult);
-    String resultAsString = commandResultToString(commandResult);
-    getLogWriter()
-        .info("testDescribeClientForNonSubscribedClient resultAsString=" + resultAsString);
-    assertTrue(Result.Status.OK.equals(commandResult.getStatus()));
-
-    CompositeResultData resultData = (CompositeResultData) commandResult.getResultData();
-    CompositeResultData.SectionResultData section = resultData.retrieveSection("InfoSection");
-    assertNotNull(section);
-
-    TabularResultData tableResultData = section.retrieveTable("Pool Stats For Pool Name = DEFAULT");
-    assertNotNull(tableResultData);
-
-    List<String> minConn = tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_MIN_CONN);
-    List<String> maxConn = tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_MAX_CONN);
-    List<String> redundancy =
-        tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_REDUNDANCY);
-
-    assertTrue(minConn.contains("1"));
-    assertTrue(maxConn.contains("-1"));
-    assertTrue(redundancy.contains("1"));
-    String puts = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PUTS);
-    assertTrue(puts.equals("2"));
-    String calls = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_LISTENER_CALLS);
-    assertTrue(calls.equals("1"));
-    String primServer = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PRIMARY_SERVERS);
-    assertTrue(primServer.equals("N.A."));
-    String durable = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_DURABLE);
-    assertTrue(durable.equals("No"));
-    String threads = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_THREADS);
-    assertTrue(Integer.parseInt(threads) > 0);
-    String cpu = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_CPU);
-    assertTrue(Integer.parseInt(cpu) > 0);
-    String upTime = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_UP_TIME);
-    assertTrue(Integer.parseInt(upTime) == 0);
-    String prcTime = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PROCESS_CPU_TIME);
-    assertTrue(Long.parseLong(prcTime) > 0);
-
-    ClientCommandsTestUtils.closeNonDurableClient(Host.getHost(0).getVM(2));
-    ClientCommandsTestUtils.closeCacheServer(Host.getHost(0).getVM(1));
-    ClientCommandsTestUtils.closeCacheServer(Host.getHost(0).getVM(3));
+    gfsh.connectAndVerify(locatorVm0);
   }
 
   @Test
-  public void testDescribeMixClientWithServers() throws Exception {
-    String[] clientIds = setupSystemWithSubAndNonSubClient();
-    final VM server1 = Host.getHost(0).getVM(1);
-    String serverName =
-        server1.invoke("Get DistributedMember Id", ClientCommandsTestUtils::getDistributedMemberId);
-    String commandString = CliStrings.DESCRIBE_CLIENT + " --" + CliStrings.DESCRIBE_CLIENT__ID
-        + "=\"" + clientIds[0] + "\"";
-    getLogWriter().info("testDescribeMixClientWithServers commandStr=" + commandString);
-    executeAndVerifyResultsForMixedClients(commandString, serverName);
-    String commandString2 = CliStrings.DESCRIBE_CLIENT + " --" + CliStrings.DESCRIBE_CLIENT__ID
-        + "=\"" + clientIds[1] + "\"";
-    getLogWriter().info("testDescribeMixClientWithServers commandString2=" + commandString2);
-    executeAndVerifyResultsForMixedClients(commandString2, serverName);
-
-    ClientCommandsTestUtils.closeNonDurableClient(Host.getHost(0).getVM(2));
-    ClientCommandsTestUtils.closeNonDurableClient(Host.getHost(0).getVM(3));
-    ClientCommandsTestUtils.closeCacheServer(Host.getHost(0).getVM(1));
-  }
+  public void describeClient() throws Exception {
+    boolean subscriptionEnabled = true;
+    client1Vm3 = createClient(3, subscriptionEnabled);
+    setupCqsOnVM(client1Vm3, STOCKS_REGION, "cq1", "cq2", "cq3");
 
-  private void executeAndVerifyResultsForMixedClients(String commandString, String serverName) {
-    CommandResult commandResult = executeCommand(commandString);
-    getLogWriter().info("testDescribeMixClientWithServers commandResult=" + commandResult);
-    String resultAsString = commandResultToString(commandResult);
-    getLogWriter().info("testDescribeMixClientWithServers resultAsString=" + resultAsString);
-    assertTrue(Result.Status.OK.equals(commandResult.getStatus()));
-
-    CompositeResultData resultData = (CompositeResultData) commandResult.getResultData();
-    CompositeResultData.SectionResultData section = resultData.retrieveSection("InfoSection");
-    assertNotNull(section);
-    TabularResultData tableResultData = section.retrieveTable("Pool Stats For Pool Name = DEFAULT");
-    assertNotNull(tableResultData);
-
-    List<String> minConn = tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_MIN_CONN);
-    List<String> maxConn = tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_MAX_CONN);
-    List<String> redundancy =
-        tableResultData.retrieveAllValues(CliStrings.DESCRIBE_CLIENT_REDUNDANCY);
-
-    assertTrue(minConn.contains("1"));
-    assertTrue(maxConn.contains("-1"));
-    assertTrue(redundancy.contains("1"));
-    String puts = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PUTS);
-    assertTrue(puts.equals("2"));
-    String calls = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_LISTENER_CALLS);
-    assertTrue(calls.equals("1"));
-    String primServer = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PRIMARY_SERVERS);
-    assertTrue(primServer.equals(serverName) || primServer.equals("N.A."));
-    String durable = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_DURABLE);
-    assertTrue(durable.equals("No"));
-    String threads = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_THREADS);
-    assertTrue(Integer.parseInt(threads) > 0);
-    String cpu = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_CPU);
-    assertTrue(Integer.parseInt(cpu) > 0);
-    String upTime = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_UP_TIME);
-    assertTrue(Integer.parseInt(upTime) >= 0);
-    String prcTime = section.retrieveString(CliStrings.DESCRIBE_CLIENT_COLUMN_PROCESS_CPU_TIME);
-    assertTrue(Long.parseLong(prcTime) > 0);
-  }
+    client2Vm4 = createClient(4, subscriptionEnabled);
+    setupCqsOnVM(client2Vm4, STOCKS_REGION, "cq1", "cq2", "cq3");
 
-  private String[] setupSystemWithSubAndNonSubClient() throws Exception {
-    disconnectAllFromDS();
-    setUpJmxManagerOnVm0ThenConnect(getServerProperties());
-    final VM manager = Host.getHost(0).getVM(0);
-    final VM server1 = Host.getHost(0).getVM(1);
-    final VM client1 = Host.getHost(0).getVM(2);
-    final VM client2 = Host.getHost(0).getVM(3);
-    port0 = startCacheServer(server1, regionName);
-    startNonDurableClient(client1, server1, port0);
-    startNonSubscribedClient(client2, server1, port0);
-    waitForMixedClients();
-    return manager.invoke("get client Ids", () -> {
-      Cache cache = GemFireCacheImpl.getInstance();
-      SystemManagementService service =
-          (SystemManagementService) ManagementService.getExistingManagementService(cache);
-      DistributedMember serverMember = getMember(server1);
-      final ObjectName cacheServerMBeanName = service.getCacheServerMBeanName(port0, serverMember);
-      CacheServerMXBean bean = service.getMBeanProxy(cacheServerMBeanName, CacheServerMXBean.class);
-      return bean.getClientIds();
-    });
-  }
-
-  private int startCacheServer(VM server, final String regionName) {
-    return server.invoke("setup CacheServer", () -> {
-      getSystem(getServerProperties());
-      GemFireCacheImpl cache = (GemFireCacheImpl) getCache();
-      AttributesFactory factory = new AttributesFactory();
-      factory.setScope(Scope.DISTRIBUTED_ACK);
-      factory.setDataPolicy(DataPolicy.REPLICATE);
-      Region region = createRootRegion(regionName, factory.create());
-      assertTrue(region instanceof DistributedRegion);
-      CacheServer cacheServer = cache.addCacheServer();
-      cacheServer.setPort(0);
-      cacheServer.start();
-      return cacheServer.getPort();
-    });
-  }
+    waitForClientReady(3);
 
-  private void waitForMixedClients() {
-    final VM manager = Host.getHost(0).getVM(0);
-    final VM server1 = Host.getHost(0).getVM(1);
-    final DistributedMember serverMember = getMember(server1);
-    assertNotNull(serverMember);
-    manager.invoke(() -> Awaitility.waitAtMost(5 * 60, TimeUnit.SECONDS)
-        .pollDelay(2, TimeUnit.SECONDS).until(() -> {
-          try {
-            final SystemManagementService service =
-                (SystemManagementService) ManagementService.getManagementService(getCache());
-            if (service == null) {
-              getLogWriter().info("waitForMixedClients Still probing for service");
-              return false;
-            } else {
-              getLogWriter().info("waitForMixedClients 1");
-              final ObjectName cacheServerMBeanName =
-                  service.getCacheServerMBeanName(port0, serverMember);
-              getLogWriter()
-                  .info("waitForMixedClients 2 cacheServerMBeanName " + cacheServerMBeanName);
-              CacheServerMXBean bean =
-                  service.getMBeanProxy(cacheServerMBeanName, CacheServerMXBean.class);
-              getLogWriter().info("waitForMixedClients 2 bean " + bean);
-              if (bean.getClientIds().length > 1) {
-                return true;
-              }
-            }
-          } catch (Exception e) {
-            LogWrapper.getInstance(cache)
-                .warning("waitForMixedClients Exception in waitForMBean ::: "
-                    + ExceptionUtils.getStackTrace(e));
-          }
-          return false;
-        }));
+    validateResults(subscriptionEnabled);
   }
 
+  @Test
+  public void describeClientWithoutSubscription() throws Exception {
+    boolean subscriptionEnabled = false;
+    client1Vm3 = createClient(3, subscriptionEnabled);
+    setupCqsOnVM(client1Vm3, STOCKS_REGION, "cq1", "cq2", "cq3");
 
-  private void setupSystem1() throws Exception {
-    disconnectAllFromDS();
-    setUpJmxManagerOnVm0ThenConnect(getServerProperties());
-
-    final VM manager = Host.getHost(0).getVM(0);
-    final VM server1 = Host.getHost(0).getVM(1);
-    final VM client1 = Host.getHost(0).getVM(2);
-    final VM server2 = Host.getHost(0).getVM(3);
-
-    port0 = startCacheServer(server1, regionName);
-    startCacheServer(server2, regionName);
+    client2Vm4 = createClient(4, subscriptionEnabled);
+    setupCqsOnVM(client2Vm4, STOCKS_REGION, "cq1", "cq2", "cq3");
 
-    startNonDurableClient(client1, server1, port0);
-    setupCqsOnVM(cq1, cq2, cq3, regionName, client1);
-    waitForMBean();
+    waitForClientReady(1);
 
-    clientId = manager.invoke("get client Id", () -> getClientIdString(server1));
+    validateResults(subscriptionEnabled);
   }
 
-  private void setupSystem2() throws Exception {
-    disconnectAllFromDS();
-    setUpJmxManagerOnVm0ThenConnect(getServerProperties());
+  private void validateResults(boolean subscriptionEnabled) {
+    CommandResult result = gfsh.executeCommand("list members");
+    // list is always locator-0, server-1, server-2
+    String server1 = result.getTableColumnValues("0", "Id").get(1);
 
-    final VM manager = Host.getHost(0).getVM(0);
-    final VM server1 = Host.getHost(0).getVM(1);
-    final VM client1 = Host.getHost(0).getVM(2);
-    final VM client2 = Host.getHost(0).getVM(3);
+    result = gfsh.executeCommand("list clients");
+    String clientId = result.getColumnFromTableContent(CliStrings.LIST_CLIENT_COLUMN_Clients,
+        "section1", "TableForClientList").get(0);
 
-    port0 = startCacheServer(server1, regionName);
-    startNonDurableClient(client1, server1, port0);
-    startNonDurableClient(client2, server1, port0);
-
-    setupCqsOnVM(cq1, cq2, cq3, regionName, client1);
-    setupCqsOnVM(cq1, cq2, cq3, regionName, client2);
-    waitForMBean();
-
-    clientId = manager.invoke("get client Id", () -> getClientIdString(server1));
-  }
+    result = gfsh.executeCommand("describe client --clientID=" + clientId);
+    assertThat(result.getStatus()).isEqualTo(Result.Status.OK);
 
-  private void setupSystem3() throws Exception {
-    disconnectAllFromDS();
-    setUpJmxManagerOnVm0ThenConnect(getServerProperties());
+    Map<String, List<String>> table =
+        result.getMapFromTableContent("Pool Stats For Pool Name = DEFAULT");
+    Map<String, String> data = result.getMapFromSection("InfoSection");
 
-    final VM manager = Host.getHost(0).getVM(0);
-    final VM server1 = Host.getHost(0).getVM(1);
-    final VM client1 = Host.getHost(0).getVM(2);
-    final VM server2 = Host.getHost(0).getVM(3);
+    assertThat(table.get(CliStrings.DESCRIBE_CLIENT_MIN_CONN).get(0)).isEqualTo("1");
+    assertThat(table.get(CliStrings.DESCRIBE_CLIENT_MAX_CONN).get(0)).isEqualTo("-1");
+    assertThat(table.get(CliStrings.DESCRIBE_CLIENT_REDUNDANCY).get(0)).isEqualTo("1");
 
-    port0 = startCacheServer(server1, regionName);
-    port1 = startCacheServer(server2, regionName);
-    startNonDurableClient(client1, server1, port0);
-    startNonDurableClient(client1, server2, port1);
+    if (subscriptionEnabled) {
+      assertThat(table.get(CliStrings.DESCRIBE_CLIENT_CQs).get(0)).isEqualTo("3");
+      assertThat(Integer.parseInt(data.get(CliStrings.DESCRIBE_CLIENT_COLUMN_QUEUE_SIZE)))
+          .isGreaterThanOrEqualTo(1);
+      assertThat(data.get(CliStrings.DESCRIBE_CLIENT_COLUMN_PRIMARY_SERVERS)).isEqualTo(server1);
+    } else {
+      assertThat(table.get(CliStrings.DESCRIBE_CLIENT_CQs).get(0)).isEqualTo("1");
+      assertThat(Integer.parseInt(data.get(CliStrings.DESCRIBE_CLIENT_COLUMN_QUEUE_SIZE)))
+          .isEqualTo(0);
+      assertThat(data.get(CliStrings.DESCRIBE_CLIENT_COLUMN_PRIMARY_SERVERS)).isEqualTo("N.A.");
+    }
 
-    setupCqsOnVM(cq1, cq2, cq3, regionName, client1);
-    waitForListClientMBean3();
+    assertThat(data.get(CliStrings.DESCRIBE_CLIENT_COLUMN_PUTS)).isEqualTo("2");
+    assertThat(data.get(CliStrings.DESCRIBE_CLIENT_COLUMN_LISTENER_CALLS)).isEqualTo("1");
+    assertThat(data.get(CliStrings.DESCRIBE_CLIENT_COLUMN_DURABLE)).isEqualTo("No");
+    assertThat(Integer.parseInt(data.get(CliStrings.DESCRIBE_CLIENT_COLUMN_THREADS)))
+        .isGreaterThan(0);
+    assertThat(Integer.parseInt(data.get(CliStrings.DESCRIBE_CLIENT_COLUMN_CPU))).isGreaterThan(0);
+    assertThat(Integer.parseInt(data.get(CliStrings.DESCRIBE_CLIENT_COLUMN_UP_TIME)))
+        .isGreaterThanOrEqualTo(0);
+    assertThat(Long.parseLong(data.get(CliStrings.DESCRIBE_CLIENT_COLUMN_PROCESS_CPU_TIME)))
+        .isGreaterThan(0);
+  }
+
+  void waitForClientReady(int cqsToWaitFor) {
+    // Wait until all CQs are ready
+    Awaitility.waitAtMost(20, TimeUnit.SECONDS).until(() -> {
+      CommandResult r = gfsh.executeCommand("list clients");
+      if (r.getStatus() != Result.Status.OK) {
+        return false;
+      }
 
-    clientId = manager.invoke("get client Id", () -> getClientIdString(server1));
-  }
+      String clientId = r.getColumnFromTableContent(CliStrings.LIST_CLIENT_COLUMN_Clients,
+          "section1", "TableForClientList").get(0);
+      r = gfsh.executeCommand("describe client --clientID=" + clientId);
+      Map<String, List<String>> table =
+          r.getMapFromTableContent("Pool Stats For Pool Name = DEFAULT");
 
-  private void startNonDurableClient(VM client, final VM server, final int port) {
-    client.invoke("start non-durable client", () -> {
-      Cache cache = GemFireCacheImpl.getInstance();
-      if (cache == null) {
-
-        Properties props = getNonDurableClientProps();
-        props.setProperty(LOG_FILE, "client_" + OSProcess.getId() + ".log");
-        props.setProperty(LOG_LEVEL, "fine");
-        props.setProperty(STATISTIC_ARCHIVE_FILE, "client_" + OSProcess.getId() + ".gfs");
-        props.setProperty(STATISTIC_SAMPLING_ENABLED, "true");
-
-        getSystem(props);
-
-        final ClientCacheFactory ccf = new ClientCacheFactory(props);
-        ccf.addPoolServer(getServerHostName(server.getHost()), port);
-        ccf.setPoolSubscriptionEnabled(true);
-        ccf.setPoolPingInterval(1);
-        ccf.setPoolStatisticInterval(1);
-        ccf.setPoolSubscriptionRedundancy(1);
-        ccf.setPoolMinConnections(1);
-
-        ClientCache clientCache = getClientCache(ccf);
-        // Create region
-        if (clientCache.getRegion(Region.SEPARATOR + regionName) == null
-            && clientCache.getRegion(regionName) == null) {
-          ClientRegionFactory<Object, Object> regionFactory =
-              clientCache.createClientRegionFactory(ClientRegionShortcut.LOCAL)
-                  .setPoolName(clientCache.getDefaultPool().getName());
-          Region<Object, Object> dataRegion = regionFactory.create(regionName);
-          assertNotNull(dataRegion);
-          dataRegion.put("k1", "v1");
-          dataRegion.put("k2", "v2");
-        }
-      } else {
-        String poolName = "new_pool_" + System.currentTimeMillis();
-        try {
-          PoolImpl p = (PoolImpl) PoolManager.createFactory()
-              .addServer(getServerHostName(server.getHost()), port).setThreadLocalConnections(true)
-              .setMinConnections(1).setSubscriptionEnabled(true).setPingInterval(1)
-              .setStatisticInterval(1).setMinConnections(1).setSubscriptionRedundancy(1)
-              .create(poolName);
-          System.out.println("Created new pool pool " + poolName);
-          assertNotNull(p);
-        } catch (Exception eee) {
-          System.err.println("Exception in creating pool " + poolName + "    Exception =="
-              + ExceptionUtils.getStackTrace(eee));
-        }
+      if (table.size() == 0 || table.get(CliStrings.DESCRIBE_CLIENT_CQs).size() == 0) {
+        return false;
       }
+
+      return table.get(CliStrings.DESCRIBE_CLIENT_CQs).get(0).equals(cqsToWaitFor + "");
     });
   }
 
-  private void waitForListClientMBean3() {
-    final VM manager = Host.getHost(0).getVM(0);
-    final VM server1 = Host.getHost(0).getVM(1);
-    final VM server2 = Host.getHost(0).getVM(3);
-    final DistributedMember serverMember1 = getMember(server1);
-    final DistributedMember serverMember2 = getMember(server2);
-    assertNotNull(serverMember1);
-    manager.invoke(() -> Awaitility.waitAtMost(2 * 60, TimeUnit.SECONDS)
-        .pollDelay(2, TimeUnit.SECONDS).until(() -> {
-          final SystemManagementService service =
-              (SystemManagementService) ManagementService.getManagementService(getCache());
-          if (service == null) {
-            getLogWriter().info("waitForListClientMBean3 Still probing for service");
-            return false;
-          } else {
-            final ObjectName cacheServerMBeanName1 =
-                service.getCacheServerMBeanName(port0, serverMember1);
-            final ObjectName cacheServerMBeanName2 =
-                service.getCacheServerMBeanName(port1, serverMember2);
-            CacheServerMXBean bean1 =
-                service.getMBeanProxy(cacheServerMBeanName1, CacheServerMXBean.class);
-            CacheServerMXBean bean2 =
-                service.getMBeanProxy(cacheServerMBeanName2, CacheServerMXBean.class);
-            try {
-              if (bean1 != null && bean2 != null) {
-                if (bean1.getClientIds().length > 0 && bean2.getClientIds().length > 0) {
-                  return true;
-                }
-              }
-              return false;
-
-            } catch (Exception e) {
-              LogWrapper.getInstance(cache)
-                  .warning("waitForListClientMBean3 Exception in waitForListClientMbean ::: "
-                      + ExceptionUtils.getStackTrace(e));
-            }
-            return false;
-          }
-        }));
-  }
+  private ClientVM createClient(int vmId, boolean subscriptionEnabled) throws Exception {
+    int server1Port = server1Vm1.getPort();
+    Consumer<ClientCacheFactory> cacheSetup = (Serializable & Consumer<ClientCacheFactory>) cf -> {
+      cf.addPoolServer("localhost", server1Port);
+      cf.setPoolSubscriptionEnabled(subscriptionEnabled);
+      cf.setPoolPingInterval(100);
+      cf.setPoolStatisticInterval(100);
+      cf.setPoolSubscriptionRedundancy(1);
+      cf.setPoolMinConnections(1);
+      // TODO: Remove this once GEODE-5157 is fixed
+      SocketCreator.use_client_host_name = false;
+    };
+
+    Properties clientProps = new Properties();
+    clientProps.setProperty("statistic-archive-file", "client.gfs");
+    clientProps.setProperty("statistic-sampling-enabled", "true");
+    ClientVM vm = rule.startClientVM(vmId, clientProps, cacheSetup);
+
+    vm.invoke(() -> {
+      ClientCache cache = ClusterStartupRule.getClientCache();
+      ClientRegionFactory crf = cache.createClientRegionFactory(ClientRegionShortcut.PROXY);
+      crf.setPoolName(cache.getDefaultPool().getName());
+
+      Region region = crf.create(STOCKS_REGION);
+      region.put("k1", "v1");
+      region.put("k2", "v2");
+    });
 
-  private void waitForMBean() {
-    final VM manager = Host.getHost(0).getVM(0);
-    final VM server1 = Host.getHost(0).getVM(1);
-    final DistributedMember serverMember = getMember(server1);
-    assertNotNull(serverMember);
-    manager.invoke(() -> Awaitility.waitAtMost(2 * 60, TimeUnit.SECONDS)
-        .pollDelay(2, TimeUnit.SECONDS).until(() -> {
-          final SystemManagementService service =
-              (SystemManagementService) ManagementService.getManagementService(getCache());
-          if (service == null) {
-            getLogWriter().info("waitForMBean Still probing for service");
-            return false;
-          } else {
-            final ObjectName cacheServerMBeanName =
-                service.getCacheServerMBeanName(port0, serverMember);
-            CacheServerMXBean bean =
-                service.getMBeanProxy(cacheServerMBeanName, CacheServerMXBean.class);
-            try {
-              ClientHealthStatus stats = bean.showClientStats(bean.getClientIds()[0]);
-              Map<String, String> poolStats = stats.getPoolStats();
-              if (poolStats.size() > 0) {
-                for (Map.Entry<String, String> entry : poolStats.entrySet()) {
-                  String poolStatsStr = entry.getValue();
-                  String str[] = poolStatsStr.split(";");
-                  int numCqs = Integer.parseInt(str[3].substring(str[3].indexOf("=") + 1));
-                  if (numCqs == 3) {
-                    return true;
-                  }
-                }
-              }
-              return false;
-            } catch (Exception e) {
-              LogWrapper.getInstance(cache).warning(
-                  "waitForMBean Exception in waitForMBean ::: " + ExceptionUtils.getStackTrace(e));
-            }
-            return false;
-          }
-        }));
+    return vm;
   }
 
-  private void startNonSubscribedClient(VM client, final VM server, final int port) {
-    client.invoke("Start client", () -> {
+  private void setupCqsOnVM(ClientVM vm, String regionName, String cq1, String cq2, String cq3) {
+    vm.invoke(() -> {
       Cache cache = GemFireCacheImpl.getInstance();
-      if (cache == null) {
-        Properties props = getNonDurableClientProps();
-        props.setProperty(LOG_FILE, "client_" + OSProcess.getId() + ".log");
-        props.setProperty(LOG_LEVEL, "fine");
-        props.setProperty(STATISTIC_ARCHIVE_FILE, "client_" + OSProcess.getId() + ".gfs");
-        props.setProperty(STATISTIC_SAMPLING_ENABLED, "true");
-        getSystem(props);
-        final ClientCacheFactory ccf = new ClientCacheFactory(props);
-        ccf.addPoolServer(getServerHostName(server.getHost()), port);
-        ccf.setPoolSubscriptionEnabled(false);
-        ccf.setPoolPingInterval(1);
-        ccf.setPoolStatisticInterval(1);
-        ccf.setPoolSubscriptionRedundancy(1);
-        ccf.setPoolMinConnections(1);
-        ClientCache clientCache = getClientCache(ccf);
-        // Create region
-        if (clientCache.getRegion(Region.SEPARATOR + regionName) == null
-            && clientCache.getRegion(regionName) == null) {
-          ClientRegionFactory<Object, Object> regionFactory =
-              clientCache.createClientRegionFactory(ClientRegionShortcut.LOCAL)
-                  .setPoolName(clientCache.getDefaultPool().getName());
-          Region<Object, Object> dataRegion = regionFactory.create(regionName);
-          assertNotNull(dataRegion);
-          dataRegion.put("k1", "v1");
-          dataRegion.put("k2", "v2");
-        }
-      } else {
-        String poolName = "new_pool_" + System.currentTimeMillis();
-        try {
-          PoolImpl p = (PoolImpl) PoolManager.createFactory()
-              .addServer(getServerHostName(server.getHost()), port).setThreadLocalConnections(true)
-              .setMinConnections(1).setSubscriptionEnabled(false).setPingInterval(1)
-              .setStatisticInterval(1).setMinConnections(1).setSubscriptionRedundancy(1)
-              .create(poolName);
-          cache.getLogger().info("Created new pool pool " + poolName);
-          assertNotNull(p);
-        } catch (Exception eee) {
-          cache.getLogger().info("Exception in creating pool " + poolName + "    Exception =="
-              + ExceptionUtils.getStackTrace(eee));
-        }
-      }
-    });
-  }
-
-  private void setUpNonSubscribedClient() throws Exception {
-    disconnectAllFromDS();
-    setUpJmxManagerOnVm0ThenConnect(getServerProperties());
-    final VM manager = Host.getHost(0).getVM(0);
-    final VM server1 = Host.getHost(0).getVM(1);
-    final VM client1 = Host.getHost(0).getVM(2);
-    final VM server2 = Host.getHost(0).getVM(3);
-    port0 = startCacheServer(server1, regionName);
-    startCacheServer(server2, regionName);
-    startNonSubscribedClient(client1, server1, port0);
-    setupCqsOnVM(cq1, cq2, cq3, regionName, client1);
-    waitForNonSubCliMBean();
-    clientId = (String) manager.invoke(new SerializableCallable() {
-      @Override
-      public Object call() throws Exception {
-        return getClientIdString(server1);
+      QueryService qs = cache.getQueryService();
+      CqAttributesFactory cqAf = new CqAttributesFactory();
+      try {
+        qs.newCq(cq1, "select * from /" + regionName, cqAf.create(), true).execute();
+        qs.newCq(cq2, "select * from /" + regionName + " where id = 1", cqAf.create(), true)
+            .execute();
+        qs.newCq(cq3, "select * from /" + regionName + " where id > 2", cqAf.create(), true)
+            .execute();
+        cache.getLogger()
+            .info("setupCqs on vm created cqs = " + cache.getQueryService().getCqs().length);
+      } catch (Exception e) {
+        cache.getLogger().info("setupCqs on vm Exception " + ExceptionUtils.getStackTrace(e));
       }
+      return true;
     });
   }
-
-  private void waitForNonSubCliMBean() {
-    final VM manager = Host.getHost(0).getVM(0);
-    final VM server1 = Host.getHost(0).getVM(1);
-    final DistributedMember serverMember = getMember(server1);
-    assertNotNull(serverMember);
-    manager.invoke(() -> Awaitility.waitAtMost(5 * 60, TimeUnit.SECONDS)
-        .pollDelay(2, TimeUnit.SECONDS).until(() -> {
-          try {
-            final SystemManagementService service =
-                (SystemManagementService) ManagementService.getManagementService(getCache());
-            if (service == null) {
-              getLogWriter().info("waitForNonSubScribedClientMBean Still probing for service");
-              return false;
-            } else {
-              getLogWriter().info("waitForNonSubScribedClientMBean 1");
-              final ObjectName cacheServerMBeanName =
-                  service.getCacheServerMBeanName(port0, serverMember);
-              getLogWriter().info(
-                  "waitForNonSubScribedClientMBean 2 cacheServerMBeanName " + cacheServerMBeanName);
-              CacheServerMXBean bean =
-                  service.getMBeanProxy(cacheServerMBeanName, CacheServerMXBean.class);
-              getLogWriter().info("waitForNonSubScribedClientMBean 2 bean " + bean);
-              if (bean.getClientIds().length > 0) {
-                return true;
-              }
-            }
-          } catch (Exception e) {
-            LogWrapper.getInstance(cache)
-                .warning("waitForNonSubScribedClientMBean Exception in waitForMBean ::: "
-                    + ExceptionUtils.getStackTrace(e));
-          }
-          return false;
-        }));
-  }
-
-  private String getClientIdString(VM server1) throws Exception {
-    Cache cache = GemFireCacheImpl.getInstance();
-    SystemManagementService service =
-        (SystemManagementService) ManagementService.getExistingManagementService(cache);
-    DistributedMember serverMember = getMember(server1);
-    final ObjectName cacheServerMBeanName = service.getCacheServerMBeanName(port0, serverMember);
-    CacheServerMXBean bean = service.getMBeanProxy(cacheServerMBeanName, CacheServerMXBean.class);
-    return bean.getClientIds()[0];
-  }
-
-  @Override
-  public final void postTearDownCacheTestCase() throws Exception {
-    Host.getHost(0).getVM(0).invoke((SerializableRunnableIF) CacheServerTestUtil::closeCache);
-    Host.getHost(0).getVM(1).invoke((SerializableRunnableIF) CacheServerTestUtil::closeCache);
-    Host.getHost(0).getVM(2).invoke((SerializableRunnableIF) CacheServerTestUtil::closeCache);
-    Host.getHost(0).getVM(3).invoke((SerializableRunnableIF) CacheServerTestUtil::closeCache);
-  }
 }
diff --git a/geode-web/src/test/java/org/apache/geode/management/internal/cli/commands/CommandOverHttpTest.java b/geode-web/src/test/java/org/apache/geode/management/internal/cli/commands/CommandOverHttpTest.java
index 6932124..9ed1321 100644
--- a/geode-web/src/test/java/org/apache/geode/management/internal/cli/commands/CommandOverHttpTest.java
+++ b/geode-web/src/test/java/org/apache/geode/management/internal/cli/commands/CommandOverHttpTest.java
@@ -62,7 +62,7 @@ public class CommandOverHttpTest {
   public void testDescribeClient() throws Exception {
     CommandResult result = gfshRule.executeCommand("describe client --clientID=xyz");
     assertThat(result.getStatus()).isEqualTo(Result.Status.ERROR);
-    assertThat(result.toString()).contains("Specified Client ID xyz not present");
+    assertThat(result.getErrorMessage()).contains("Specified Client ID xyz not present");
   }
 
   @Test

-- 
To stop receiving notification emails like this one, please contact
jensdeppe@apache.org.