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

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


The following commit(s) were added to refs/heads/main by this push:
     new 386cc6c89f3 SOLR-18113: Revamp ZookeeperInfoHandler (#4124)
386cc6c89f3 is described below

commit 386cc6c89f3837c640cd34187e23a7b482dabc1c
Author: Eric Pugh <[email protected]>
AuthorDate: Tue Feb 17 12:39:48 2026 -0500

    SOLR-18113: Revamp ZookeeperInfoHandler (#4124)
    
    Co-authored-by: Copilot <[email protected]>
---
 changelog/unreleased/SOLR-18113.yml                |   8 +
 .../solr/handler/admin/ZookeeperInfoHandler.java   | 614 ++++++++++-----------
 solr/core/src/resources/ImplicitPlugins.json       |   2 +-
 .../handler/admin/ZookeeperInfoHandlerTest.java    | 195 ++++++-
 solr/webapp/web/js/angular/controllers/cloud.js    |   5 +-
 solr/webapp/web/js/angular/services.js             |   2 +-
 solr/webapp/web/partials/cloud.html                |   6 +-
 7 files changed, 504 insertions(+), 328 deletions(-)

diff --git a/changelog/unreleased/SOLR-18113.yml 
b/changelog/unreleased/SOLR-18113.yml
new file mode 100644
index 00000000000..c8f80140c01
--- /dev/null
+++ b/changelog/unreleased/SOLR-18113.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Improve footer UI of Solr Cloud - Graph when you have large numbers of 
collections.  Plus code refactor.
+type: fixed # added, changed, fixed, deprecated, removed, dependency_update, 
security, other
+authors:
+  - name: Eric Pugh
+links:
+  - name: SOLR-18113
+    url: https://issues.apache.org/jira/browse/SOLR-18113
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java 
b/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java
index f7598af121f..f52a548a0ff 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java
@@ -18,13 +18,8 @@ package org.apache.solr.handler.admin;
 
 import static org.apache.solr.common.params.CommonParams.OMIT_HEADER;
 import static org.apache.solr.common.params.CommonParams.PATH;
-import static org.apache.solr.common.params.CommonParams.WT;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStreamWriter;
-import java.io.Reader;
-import java.io.Writer;
 import java.lang.invoke.MethodHandles;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
@@ -55,23 +50,17 @@ import org.apache.solr.common.cloud.SolrZkClient;
 import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.MapSolrParams;
 import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.common.util.SuppressForbidden;
-import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.JSONResponseWriter;
-import org.apache.solr.response.RawResponseWriter;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.security.AuthorizationContext;
 import org.apache.zookeeper.KeeperException;
 import org.apache.zookeeper.WatchedEvent;
 import org.apache.zookeeper.Watcher;
 import org.apache.zookeeper.data.Stat;
-import org.apache.zookeeper.server.ByteBufferInputStream;
-import org.noggit.CharArr;
-import org.noggit.JSONWriter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -117,7 +106,7 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
   }
 
   /** Enumeration of ways to filter collections on the graph panel. */
-  static enum FilterType {
+  enum FilterType {
     none,
     name,
     status
@@ -127,8 +116,8 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
   static final class PageOfCollections {
     List<String> selected;
     int numFound = 0; // total number of matches (across all pages)
-    int start = 0;
-    int rows = -1;
+    int start;
+    int rows;
     FilterType filterType;
     String filter;
 
@@ -169,7 +158,7 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
       if (!regexFilter.startsWith("(?i)")) regexFilter = "(?i)" + regexFilter;
 
       Pattern filterRegex = Pattern.compile(regexFilter);
-      List<String> filtered = new ArrayList<String>();
+      List<String> filtered = new ArrayList<>();
       for (String next : collections) {
         if (matches(filterRegex, next)) filtered.add(next);
       }
@@ -182,9 +171,9 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
      * user is filtering by.
      */
     @SuppressWarnings("unchecked")
-    final boolean matchesStatusFilter(Map<String, Object> collectionState, 
Set<String> liveNodes) {
+    boolean matchesStatusFilter(Map<String, Object> collectionState, 
Set<String> liveNodes) {
 
-      if (filterType != FilterType.status || filter == null || filter.length() 
== 0)
+      if (filterType != FilterType.status || filter == null || 
filter.isEmpty())
         return true; // no status filter, so all match
 
       boolean isHealthy = true; // means all replicas for all shards active
@@ -233,7 +222,7 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
       return true;
     }
 
-    final boolean matches(final Pattern filter, final String collName) {
+    boolean matches(final Pattern filter, final String collName) {
       return filter.matcher(collName).matches();
     }
 
@@ -282,7 +271,7 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
         throws KeeperException, InterruptedException {
       if (cachedCollections == null) {
         // cache is stale, rebuild the full list ...
-        cachedCollections = new ArrayList<String>();
+        cachedCollections = new ArrayList<>();
 
         List<String> fromZk = zkClient.getChildren("/collections", this);
         if (fromZk != null) cachedCollections.addAll(fromZk);
@@ -304,7 +293,6 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
       // activate paging (if disabled) for large collection sets
       if (page.start == 0 && page.rows == -1 && page.filter == null && 
children.size() > 10) {
         page.rows = 20;
-        page.start = 0;
       }
 
       // apply the name filter if supplied (we don't need to pull state
@@ -337,7 +325,7 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
             // using longs here as we don't know how long the 2nd group is
             int leftGroup2 = Integer.parseInt(leftMatcher.group(2));
             int rightGroup2 = Integer.parseInt(rightMatcher.group(2));
-            return (leftGroup2 > rightGroup2) ? 1 : ((leftGroup2 == 
rightGroup2) ? 0 : -1);
+            return Integer.compare(leftGroup2, rightGroup2);
           }
         }
       }
@@ -357,74 +345,160 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
   private PagedCollectionSupport pagingSupport;
 
   @Override
-  @SuppressWarnings({"unchecked"})
   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
     final SolrParams params = req.getParams();
-    Map<String, String> map = Map.of(WT, "raw", OMIT_HEADER, "true");
+
+    // Force JSON response and omit header for cleaner output
+    // Map<String, String> map = Map.of(WT, "json", OMIT_HEADER, "true");
+    Map<String, String> map = Map.of(OMIT_HEADER, "true");
     req.setParams(SolrParams.wrapDefaults(new MapSolrParams(map), params));
+
+    // Ensure paging support is initialized
+    ensurePagingSupportInitialized();
+
+    // Validate parameters
+    validateParameters(params);
+
+    // Determine request type and handle accordingly
+    boolean isGraphView = "graph".equals(params.get("view"));
+    ZkBaseResponseBuilder builder =
+        isGraphView ? handleGraphViewRequest(params) : 
handlePathViewRequest(params);
+
+    builder.build();
+
+    addMapToResponse(builder.getDataMap(), rsp);
+  }
+
+  /** Ensures the paging support is initialized (thread-safe lazy 
initialization). */
+  private void ensurePagingSupportInitialized() {
     synchronized (this) {
       if (pagingSupport == null) {
         pagingSupport = new PagedCollectionSupport();
         ZkController zkController = cores.getZkController();
         if (zkController != null) {
-          // get notified when the ZK session expires (so we can clear the 
cached collections and
-          // rebuild)
+          // Get notified when the ZK session expires (so we can clear cached 
collections)
           zkController.addOnReconnectListener(pagingSupport);
         }
       }
     }
+  }
 
-    String path = params.get(PATH);
-
+  /**
+   * Validates request parameters to prevent illegal operations.
+   *
+   * @param params Request parameters to validate
+   */
+  private void validateParameters(SolrParams params) {
     if (params.get("addr") != null) {
       throw new SolrException(ErrorCode.BAD_REQUEST, "Illegal parameter 
\"addr\"");
     }
+  }
+
+  /**
+   * Handles the graph view request with paginated collections.
+   *
+   * @param params Request parameters including pagination settings
+   * @return JSON string representing paginated collection data
+   */
+  private ZkBaseResponseBuilder handleGraphViewRequest(SolrParams params) {
+    // Extract pagination parameters
+    int start = params.getInt("start", 0);
+    int rows = params.getInt("rows", -1);
 
-    String detailS = params.get(PARAM_DETAIL);
-    boolean detail = detailS != null && detailS.equals("true");
+    // Extract filter parameters
+    FilterType filterType = extractFilterType(params);
+    String filter = extractFilter(params, filterType);
+
+    // Extract display options (applicable to graph view)
+    boolean detail = params.getBool(PARAM_DETAIL, false);
+    boolean dump = params.getBool("dump", false);
+
+    // Create response builder for paginated collections
+    return new ZkGraphResponseBuilder(
+        cores.getZkController(),
+        new PageOfCollections(start, rows, filterType, filter),
+        pagingSupport,
+        detail,
+        dump);
+  }
 
-    String dumpS = params.get("dump");
-    boolean dump = dumpS != null && dumpS.equals("true");
+  /**
+   * Handles the path view request for a specific ZooKeeper path.
+   *
+   * @param params Request parameters including the path to display
+   * @return JSON string representing the ZooKeeper path data
+   */
+  private ZkBaseResponseBuilder handlePathViewRequest(SolrParams params) {
+    // Extract path parameter
+    String path = params.get(PATH);
 
-    int start = params.getInt("start", 0); // Note start ignored if rows not 
specified
-    int rows = params.getInt("rows", -1);
+    // Extract display options
+    boolean detail = params.getBool(PARAM_DETAIL, false);
+    boolean dump = params.getBool("dump", false);
 
+    // Create response builder for specific path
+    return new ZkPathResponseBuilder(cores.getZkController(), path, detail, 
dump);
+  }
+
+  /**
+   * Extracts and normalizes the filter type from request parameters.
+   *
+   * @param params Request parameters
+   * @return The filter type (defaults to FilterType.none if not specified)
+   */
+  private FilterType extractFilterType(SolrParams params) {
     String filterType = params.get("filterType");
     if (filterType != null) {
       filterType = filterType.trim().toLowerCase(Locale.ROOT);
-      if (filterType.length() == 0) filterType = null;
+      if (filterType.isEmpty()) {
+        return FilterType.none;
+      }
+      return switch (filterType) {
+        case "none" -> FilterType.none;
+        case "name" -> FilterType.name;
+        case "status" -> FilterType.status;
+        default -> throw new SolrException(
+            ErrorCode.BAD_REQUEST,
+            "Invalid filterType '" + filterType + "'. Allowed values are: 
none, name, status");
+      };
+    }
+    return FilterType.none;
+  }
+
+  /**
+   * Extracts and normalizes the filter value from request parameters.
+   *
+   * @param params Request parameters
+   * @param filterType The filter type being used
+   * @return The filter string, or null if not applicable
+   */
+  private String extractFilter(SolrParams params, FilterType filterType) {
+    if (filterType == FilterType.none) {
+      return null;
     }
-    FilterType type = (filterType != null) ? FilterType.valueOf(filterType) : 
FilterType.none;
 
-    String filter = (type != FilterType.none) ? params.get("filter") : null;
+    String filter = params.get("filter");
     if (filter != null) {
       filter = filter.trim();
-      if (filter.length() == 0) filter = null;
+      if (!filter.isEmpty()) {
+        return filter;
+      }
     }
+    return null;
+  }
 
-    ZKPrinter printer = new ZKPrinter(cores.getZkController());
-    printer.detail = detail;
-    printer.dump = dump;
-    boolean isGraphView = "graph".equals(params.get("view"));
-    // There is no znode /clusterstate.json (removed in Solr 9), but we do as 
if there's one and
-    // return collection listing. Need to change services.js if cleaning up 
here, collection list is
-    // used from Admin UI Cloud - Graph
-    boolean paginateCollections = (isGraphView && 
"/clusterstate.json".equals(path));
-    printer.page = paginateCollections ? new PageOfCollections(start, rows, 
type, filter) : null;
-    printer.pagingSupport = pagingSupport;
-
-    try {
-      if (paginateCollections) {
-        // List collections and allow pagination, but no specific znode info 
like when looking at a
-        // normal ZK path
-        printer.printPaginatedCollections();
-      } else {
-        printer.print(path);
-      }
-    } finally {
-      printer.close();
+  /**
+   * Adds Map data to SolrQueryResponse.
+   *
+   * @param dataMap The data map to add
+   * @param rsp The response object to populate
+   */
+  private void addMapToResponse(Map<String, Object> dataMap, SolrQueryResponse 
rsp) {
+    // Add the structured data directly to the response
+    // This allows any response writer (json, xml, etc.) to serialize it 
properly
+    for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
+      rsp.add(entry.getKey(), entry.getValue());
     }
-    rsp.getValues().add(RawResponseWriter.CONTENT, printer);
   }
 
   @SuppressForbidden(reason = "JDK String class doesn't offer a stripEnd 
equivalent")
@@ -436,40 +510,59 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
   //
   // 
--------------------------------------------------------------------------------------
 
-  static class ZKPrinter implements ContentStream {
-    static boolean FULLPATH_DEFAULT = false;
+  /**
+   * Base class for ZooKeeper response builders. Provides common functionality 
for building
+   * structured response data from ZooKeeper.
+   */
+  abstract static class ZkBaseResponseBuilder {
+    protected boolean detail;
+    protected boolean dump;
 
-    boolean indent = true;
-    boolean fullpath = FULLPATH_DEFAULT;
-    boolean detail = false;
-    boolean dump = false;
+    protected final Map<String, Object> dataMap = new LinkedHashMap<>();
+    protected final SolrZkClient zkClient;
+    protected final ZkController zkController;
+    protected final String keeperAddr;
 
-    String keeperAddr; // the address we're connected to
+    public ZkBaseResponseBuilder(ZkController controller, boolean detail, 
boolean dump) {
+      this.zkController = controller;
+      this.detail = detail;
+      this.dump = dump;
+      this.keeperAddr = controller.getZkServerAddress();
+      this.zkClient = controller.getZkClient();
+    }
 
-    final Utils.BAOS baos = new Utils.BAOS();
-    final Writer out = new OutputStreamWriter(baos, StandardCharsets.UTF_8);
-    SolrZkClient zkClient;
+    public abstract void build() throws IOException;
 
-    PageOfCollections page;
-    PagedCollectionSupport pagingSupport;
-    ZkController zkController;
+    /** Returns the data as a Map for proper serialization by response 
writers. */
+    public Map<String, Object> getDataMap() {
+      return dataMap;
+    }
 
-    public ZKPrinter(ZkController controller) throws IOException {
-      this.zkController = controller;
-      keeperAddr = controller.getZkServerAddress();
-      zkClient = controller.getZkClient();
+    protected void writeError(int code, String msg) {
+      throw new SolrException(ErrorCode.getErrorCode(code), msg);
     }
 
-    public void close() {
-      try {
-        out.flush();
-      } catch (Exception e) {
-        throw new RuntimeException(e);
-      }
+    protected String time(long ms) {
+      return (new Date(ms)) + " (" + ms + ")";
+    }
+  }
+
+  /**
+   * Response builder implementation for a specific ZooKeeper path and its 
data. Used by Solr Admin
+   * UI.
+   */
+  static class ZkPathResponseBuilder extends ZkBaseResponseBuilder {
+
+    private String path;
+
+    public ZkPathResponseBuilder(
+        ZkController controller, String path, boolean detail, boolean dump) {
+      super(controller, detail, dump);
+      this.path = path;
     }
 
-    // main entry point for printing from path
-    void print(String path) throws IOException {
+    @Override
+    public void build() throws IOException {
       if (zkClient == null) {
         return;
       }
@@ -479,7 +572,7 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
         path = "/";
       } else {
         path = path.trim();
-        if (path.length() == 0) {
+        if (path.isEmpty()) {
           path = "/";
         }
       }
@@ -490,142 +583,37 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
 
       int idx = path.lastIndexOf('/');
       String parent = idx >= 0 ? path.substring(0, idx) : path;
-      if (parent.length() == 0) {
+      if (parent.isEmpty()) {
         parent = "/";
       }
 
-      CharArr chars = new CharArr();
-      JSONWriter json = new JSONWriter(chars, 2);
-      json.startObject();
-
       if (detail) {
-        if (!printZnode(json, path)) {
+        Map<String, Object> znodeData = buildZnodeData(path);
+        if (znodeData == null) {
           return;
         }
-        json.writeValueSeparator();
+        dataMap.putAll(znodeData);
       }
 
-      json.writeString("tree");
-      json.writeNameSeparator();
-      json.startArray();
-      if (!printTree(json, path)) {
+      List<Object> treeList = new ArrayList<>();
+      if (!buildTree(treeList, path)) {
         return; // there was an error
       }
-      json.endArray();
-      json.endObject();
-      out.write(chars.toString());
+      dataMap.put("tree", treeList);
     }
 
-    // main entry point for printing collections
-    @SuppressWarnings("unchecked")
-    void printPaginatedCollections() throws IOException {
-      SortedMap<String, Object> collectionStates;
-      try {
-        // support paging of the collections graph view (in case there are 
many collections)
-        // fetch the requested page of collections and then retrieve the state 
for each
-        pagingSupport.fetchPage(page, zkClient);
-        // keep track of how many collections match the filter
-        boolean applyStatusFilter = (page.filterType == FilterType.status && 
page.filter != null);
-        List<String> matchesStatusFilter = applyStatusFilter ? new 
ArrayList<>() : null;
-        ClusterState cs = zkController.getZkStateReader().getClusterState();
-        Set<String> liveNodes = applyStatusFilter ? cs.getLiveNodes() : null;
-
-        collectionStates = new TreeMap<>(pagingSupport);
-        for (String collection : page.selected) {
-          DocCollection dc = cs.getCollectionOrNull(collection);
-          if (dc != null) {
-            // TODO: for collections with perReplicaState, a ser/deser to JSON 
was needed to get the
-            // state to render correctly for the UI?
-            Map<String, Object> collectionState = dc.toMap(new 
LinkedHashMap<>());
-            if (applyStatusFilter) {
-              // verify this collection matches the filtered state
-              if (page.matchesStatusFilter(collectionState, liveNodes)) {
-                matchesStatusFilter.add(collection);
-                collectionStates.put(
-                    collection, 
ClusterStatus.postProcessCollectionJSON(collectionState));
-              }
-            } else {
-              collectionStates.put(
-                  collection, 
ClusterStatus.postProcessCollectionJSON(collectionState));
-            }
-          }
-        }
-
-        if (applyStatusFilter) {
-          // update the paged navigation info after applying the status filter
-          page.selectPage(matchesStatusFilter);
-
-          // rebuild the Map of state data
-          SortedMap<String, Object> map = new TreeMap<String, 
Object>(pagingSupport);
-          for (String next : page.selected) map.put(next, 
collectionStates.get(next));
-          collectionStates = map;
-        }
-      } catch (KeeperException | InterruptedException e) {
-        writeError(500, e.toString());
-        return;
-      }
-
-      CharArr chars = new CharArr();
-      JSONWriter json = new JSONWriter(chars, 2);
-      json.startObject();
-
-      json.writeString("znode");
-      json.writeNameSeparator();
-      json.startObject();
-
-      // For some reason, without this the Json is badly formed
-      writeKeyValue(json, PATH, "Undefined", true);
-
-      if (collectionStates != null) {
-        CharArr collectionOut = new CharArr();
-        new JSONWriter(collectionOut, 2).write(collectionStates);
-        writeKeyValue(json, "data", collectionOut.toString(), false);
-      }
-
-      writeKeyValue(json, "paging", page.getPagingHeader(), false);
-
-      json.endObject();
-      json.endObject();
-      out.write(chars.toString());
-    }
+    private boolean buildTree(List<Object> treeList, String path) {
+      int idx = path.lastIndexOf('/');
+      String label = idx > 0 ? path.substring(idx + 1) : path;
 
-    void writeError(int code, String msg) throws IOException {
-      throw new SolrException(ErrorCode.getErrorCode(code), msg);
-      /*response.setStatus(code);
-
-      CharArr chars = new CharArr();
-      JSONWriter w = new JSONWriter(chars, 2);
-      w.startObject();
-      w.indent();
-      w.writeString("status");
-      w.writeNameSeparator();
-      w.write(code);
-      w.writeValueSeparator();
-      w.indent();
-      w.writeString("error");
-      w.writeNameSeparator();
-      w.writeString(msg);
-      w.endObject();
-
-      out.write(chars.toString());*/
-    }
+      Map<String, Object> node = new LinkedHashMap<>();
+      node.put("text", label);
 
-    boolean printTree(JSONWriter json, String path) throws IOException {
-      String label = path;
-      if (!fullpath) {
-        int idx = path.lastIndexOf('/');
-        label = idx > 0 ? path.substring(idx + 1) : path;
-      }
-      json.startObject();
-      writeKeyValue(json, "text", label, true);
-      json.writeValueSeparator();
-      json.writeString("a_attr");
-      json.writeNameSeparator();
-      json.startObject();
+      Map<String, Object> aAttr = new LinkedHashMap<>();
       String href =
           "admin/zookeeper?detail=true&path=" + URLEncoder.encode(path, 
StandardCharsets.UTF_8);
-      writeKeyValue(json, "href", href, true);
-      json.endObject();
+      aAttr.put("href", href);
+      node.put("a_attr", aAttr);
 
       Stat stat = new Stat();
       try {
@@ -633,86 +621,57 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
         byte[] data = zkClient.getData(path, null, stat);
 
         if (stat.getEphemeralOwner() != 0) {
-          writeKeyValue(json, "ephemeral", true, false);
-          writeKeyValue(json, "version", stat.getVersion(), false);
+          node.put("ephemeral", true);
+          node.put("version", stat.getVersion());
         }
 
         if (dump) {
-          json.writeValueSeparator();
-          printZnode(json, path);
+          Map<String, Object> znodeData = buildZnodeData(path);
+          if (znodeData != null) {
+            node.putAll(znodeData);
+          }
         }
 
       } catch (IllegalArgumentException e) {
         // path doesn't exist (must have been removed)
-        writeKeyValue(json, "warning", "(path gone)", false);
+        node.put("warning", "(path gone)");
       } catch (KeeperException e) {
-        writeKeyValue(json, "warning", e.toString(), false);
+        node.put("warning", e.toString());
         log.warn("Keeper Exception", e);
       } catch (InterruptedException e) {
-        writeKeyValue(json, "warning", e.toString(), false);
+        node.put("warning", e.toString());
         log.warn("InterruptedException", e);
       }
 
       if (stat.getNumChildren() > 0) {
-        json.writeValueSeparator();
-        if (indent) {
-          json.indent();
-        }
-        json.writeString("children");
-        json.writeNameSeparator();
-        json.startArray();
+        List<Object> childrenList = new ArrayList<>();
 
         try {
           List<String> children = zkClient.getChildren(path, null);
           java.util.Collections.sort(children);
 
-          boolean first = true;
           for (String child : children) {
-            if (!first) {
-              json.writeValueSeparator();
-            }
-
             String childPath = path + (path.endsWith("/") ? "" : "/") + child;
-            if (!printTree(json, childPath)) {
+            if (!buildTree(childrenList, childPath)) {
               return false;
             }
-            first = false;
           }
-        } catch (KeeperException e) {
-          writeError(500, e.toString());
-          return false;
-        } catch (InterruptedException e) {
+        } catch (KeeperException | InterruptedException e) {
           writeError(500, e.toString());
           return false;
         } catch (IllegalArgumentException e) {
           // path doesn't exist (must have been removed)
-          json.writeString("(children gone)");
+          childrenList.add("(children gone)");
         }
 
-        json.endArray();
+        node.put("children", childrenList);
       }
 
-      json.endObject();
+      treeList.add(node);
       return true;
     }
 
-    String time(long ms) {
-      return (new Date(ms)).toString() + " (" + ms + ")";
-    }
-
-    public void writeKeyValue(JSONWriter json, String k, Object v, boolean 
isFirst) {
-      if (!isFirst) {
-        json.writeValueSeparator();
-      }
-      if (indent) {
-        json.indent();
-      }
-      json.writeString(k);
-      json.writeNameSeparator();
-      json.write(v);
-    }
-
-    boolean printZnode(JSONWriter json, String path) throws IOException {
+    private Map<String, Object> buildZnodeData(String path) {
       try {
         String dataStr = null;
         String dataStrErr = null;
@@ -723,89 +682,116 @@ public final class ZookeeperInfoHandler extends 
RequestHandlerBase {
           try {
             dataStr = (new BytesRef(data)).utf8ToString();
           } catch (Exception e) {
-            dataStrErr = "data is not parsable as a utf8 String: " + 
e.toString();
+            dataStrErr = "data is not parsable as a utf8 String: " + e;
           }
         }
 
-        json.writeString("znode");
-        json.writeNameSeparator();
-        json.startObject();
-
-        writeKeyValue(json, PATH, path, true);
-
-        json.writeValueSeparator();
-        json.writeString("prop");
-        json.writeNameSeparator();
-        json.startObject();
-        writeKeyValue(json, "version", stat.getVersion(), true);
-        writeKeyValue(json, "aversion", stat.getAversion(), false);
-        writeKeyValue(json, "children_count", stat.getNumChildren(), false);
-        writeKeyValue(json, "ctime", time(stat.getCtime()), false);
-        writeKeyValue(json, "cversion", stat.getCversion(), false);
-        writeKeyValue(json, "czxid", stat.getCzxid(), false);
-        writeKeyValue(json, "ephemeralOwner", stat.getEphemeralOwner(), false);
-        writeKeyValue(json, "mtime", time(stat.getMtime()), false);
-        writeKeyValue(json, "mzxid", stat.getMzxid(), false);
-        writeKeyValue(json, "pzxid", stat.getPzxid(), false);
-        writeKeyValue(json, "dataLength", stat.getDataLength(), false);
+        SimpleOrderedMap<Object> znodeMap = new SimpleOrderedMap<>();
+        SimpleOrderedMap<Object> znodeContent = new SimpleOrderedMap<>();
+
+        znodeContent.add(PATH, path);
+
+        SimpleOrderedMap<Object> prop = new SimpleOrderedMap<>();
+        prop.add("version", stat.getVersion());
+        prop.add("aversion", stat.getAversion());
+        prop.add("children_count", stat.getNumChildren());
+        prop.add("ctime", time(stat.getCtime()));
+        prop.add("cversion", stat.getCversion());
+        prop.add("czxid", stat.getCzxid());
+        prop.add("ephemeralOwner", stat.getEphemeralOwner());
+        prop.add("mtime", time(stat.getMtime()));
+        prop.add("mzxid", stat.getMzxid());
+        prop.add("pzxid", stat.getPzxid());
+        prop.add("dataLength", stat.getDataLength());
         if (null != dataStrErr) {
-          writeKeyValue(json, "dataNote", dataStrErr, false);
+          prop.add("dataNote", dataStrErr);
         }
-        json.endObject();
+        znodeContent.add("prop", prop);
 
         if (null != dataStr) {
-          writeKeyValue(json, "data", dataStr, false);
+          znodeContent.add("data", dataStr);
         }
 
-        if (page != null) {
-          writeKeyValue(json, "paging", page.getPagingHeader(), false);
-        }
-
-        json.endObject();
-      } catch (KeeperException e) {
-        writeError(500, e.toString());
-        return false;
-      } catch (InterruptedException e) {
+        znodeMap.add("znode", znodeContent);
+        return znodeMap;
+      } catch (KeeperException | InterruptedException e) {
         writeError(500, e.toString());
-        return false;
+        return null;
       }
-      return true;
     }
+  }
 
-    /* @Override
-        public void write(OutputStream os) throws IOException {
-          ByteBuffer bytes = baos.getByteBuffer();
-          os.write(bytes.array(),0,bytes.limit());
-        }
-    */
-    @Override
-    public String getName() {
-      return null;
-    }
+  /**
+   * Response builder implementation for a paginated, filtered view of 
collections. Supports
+   * pagination, and collection state retrieval.
+   */
+  static class ZkGraphResponseBuilder extends ZkBaseResponseBuilder {
+    private final PageOfCollections page;
+    private final PagedCollectionSupport pagingSupport;
 
-    @Override
-    public String getSourceInfo() {
-      return null;
+    public ZkGraphResponseBuilder(
+        ZkController controller,
+        PageOfCollections page,
+        PagedCollectionSupport pagingSupport,
+        boolean detail,
+        boolean dump) {
+      super(controller, detail, dump);
+      this.page = page;
+      this.pagingSupport = pagingSupport;
     }
 
     @Override
-    public String getContentType() {
-      return JSONResponseWriter.CONTENT_TYPE_JSON_UTF8;
-    }
+    public void build() throws IOException {
+      SortedMap<String, Object> collectionStates;
+      try {
+        // support paging of the collections graph view (in case there are 
many collections)
+        // fetch the requested page of collections and then retrieve the state 
for each
+        pagingSupport.fetchPage(page, zkClient);
+        // keep track of how many collections match the filter
+        boolean applyStatusFilter = (page.filterType == FilterType.status && 
page.filter != null);
+        List<String> matchesStatusFilter = applyStatusFilter ? new 
ArrayList<>() : null;
+        ClusterState cs = zkController.getZkStateReader().getClusterState();
+        Set<String> liveNodes = applyStatusFilter ? cs.getLiveNodes() : null;
 
-    @Override
-    public Long getSize() {
-      return null;
-    }
+        collectionStates = new TreeMap<>(pagingSupport);
+        for (String collection : page.selected) {
+          DocCollection dc = cs.getCollectionOrNull(collection);
+          if (dc != null) {
+            Map<String, Object> collectionState = dc.toMap(new 
LinkedHashMap<>());
+            if (applyStatusFilter) {
+              // verify this collection matches the filtered state
+              if (page.matchesStatusFilter(collectionState, liveNodes)) {
+                matchesStatusFilter.add(collection);
+                collectionStates.put(
+                    collection, 
ClusterStatus.postProcessCollectionJSON(collectionState));
+              }
+            } else {
+              collectionStates.put(
+                  collection, 
ClusterStatus.postProcessCollectionJSON(collectionState));
+            }
+          }
+        }
 
-    @Override
-    public InputStream getStream() throws IOException {
-      return new ByteBufferInputStream(baos.getByteBuffer());
-    }
+        if (applyStatusFilter) {
+          // update the paged navigation info after applying the status filter
+          page.selectPage(matchesStatusFilter);
 
-    @Override
-    public Reader getReader() throws IOException {
-      return null;
+          // rebuild the Map of state data
+          SortedMap<String, Object> map = new TreeMap<>(pagingSupport);
+          for (String next : page.selected) map.put(next, 
collectionStates.get(next));
+          collectionStates = map;
+        }
+      } catch (KeeperException | InterruptedException e) {
+        writeError(500, e.toString());
+        return;
+      }
+
+      Map<String, Object> znodeContent = new LinkedHashMap<>();
+      znodeContent.put(PATH, ZkStateReader.COLLECTIONS_ZKNODE);
+      znodeContent.put("data", collectionStates);
+      znodeContent.put("paging", page.getPagingHeader());
+
+      dataMap.put("znode", znodeContent);
     }
   }
 }
diff --git a/solr/core/src/resources/ImplicitPlugins.json 
b/solr/core/src/resources/ImplicitPlugins.json
index 83393194908..a9e8dd45ef4 100644
--- a/solr/core/src/resources/ImplicitPlugins.json
+++ b/solr/core/src/resources/ImplicitPlugins.json
@@ -158,7 +158,7 @@
       ]
     }
   },
-  "queryResponseWriter": {
+  "queryResponseWriter": {  
     "geojson": {
       "class": "solr.GeoJSONResponseWriter"
     },
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperInfoHandlerTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperInfoHandlerTest.java
index f6696b7bd2c..91bbaa18180 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperInfoHandlerTest.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperInfoHandlerTest.java
@@ -17,6 +17,7 @@
 package org.apache.solr.handler.admin;
 
 import java.io.IOException;
+import java.util.Map;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -40,7 +41,7 @@ public class ZookeeperInfoHandlerTest extends 
SolrCloudTestCase {
   }
 
   @Test
-  public void testZkInfoHandler() throws SolrServerException, IOException {
+  public void testZkInfoHandlerDetailView() throws SolrServerException, 
IOException {
     SolrClient client = cluster.getSolrClient();
 
     ModifiableSolrParams params = new ModifiableSolrParams();
@@ -58,7 +59,7 @@ public class ZookeeperInfoHandlerTest extends 
SolrCloudTestCase {
   }
 
   @Test
-  public void testZkInfoHandlerCollectionsView() throws Exception {
+  public void testZkInfoHandlerGraphView() throws Exception {
     // Create a test collection first
     String collectionName = "zkinfo_test_collection";
     CollectionAdminRequest.createCollection(collectionName, "conf", 1, 1)
@@ -66,9 +67,8 @@ public class ZookeeperInfoHandlerTest extends 
SolrCloudTestCase {
     cluster.waitForActiveCollection(collectionName, 1, 1);
 
     SolrClient client = cluster.getSolrClient();
-    // Test collections view (graph view with clusterstate.json)
+    // Return the data to power the Solr Admin UI - Graph.
     ModifiableSolrParams params = new ModifiableSolrParams();
-    params.set(CommonParams.PATH, "/clusterstate.json");
     params.set("view", "graph");
 
     GenericSolrRequest req =
@@ -79,10 +79,193 @@ public class ZookeeperInfoHandlerTest extends 
SolrCloudTestCase {
     SimpleSolrResponse response = req.process(client);
     NamedList<Object> responseData = response.getResponse();
 
-    // Collections view should return znode with collection data
     assertNotNull("Response should not be null", responseData);
-
     assertNotNull(
         "Response should contain 'znode' for collections view", 
responseData.get("znode"));
   }
+
+  @Test
+  public void testZkGraphResponseBuilderWithPagination() throws Exception {
+    // Create multiple test collections for pagination testing
+    String[] collectionNames = {
+      "zkgraph_collection_a", "zkgraph_collection_b", "zkgraph_collection_c", 
"zkgraph_collection_d"
+    };
+
+    for (String collectionName : collectionNames) {
+      CollectionAdminRequest.createCollection(collectionName, "conf", 1, 1)
+          .process(cluster.getSolrClient());
+      cluster.waitForActiveCollection(collectionName, 1, 1);
+    }
+
+    SolrClient client = cluster.getSolrClient();
+
+    // Test pagination with start=0, rows=2
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.set("view", "graph");
+    params.set("start", "0");
+    params.set("rows", "2");
+
+    GenericSolrRequest req =
+        new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/zookeeper", 
params);
+    req.setResponseParser(new JsonMapResponseParser());
+
+    SimpleSolrResponse response = req.process(client);
+    NamedList<Object> responseData = response.getResponse();
+
+    assertNotNull("Response should not be null", responseData);
+    @SuppressWarnings("unchecked")
+    Map<String, Object> znode = (Map<String, Object>) 
responseData.get("znode");
+    assertNotNull("Response should contain 'znode'", znode);
+
+    // Verify paging information is present
+    String paging = (String) znode.get("paging");
+    assertNotNull("Paging information should be present", paging);
+    assertTrue("Paging should include start position", paging.contains("0|"));
+    assertTrue("Paging should include rows", paging.contains("|2|"));
+
+    // Verify data field contains collection state (already parsed by 
JsonMapResponseParser)
+    Object dataObj = znode.get("data");
+    assertNotNull("Data field should be present", dataObj);
+
+    // Data should be a Map containing collection information
+    @SuppressWarnings("unchecked")
+    Map<String, Object> collectionData = (Map<String, Object>) dataObj;
+    assertNotNull("Collection data should not be null", collectionData);
+    assertFalse("Collection data should contain collections", 
collectionData.isEmpty());
+  }
+
+  @Test
+  public void testZkGraphResponseBuilderWithNameFilter() throws Exception {
+    // Create test collections with specific naming pattern
+    String[] collectionNames = {"filter_test_alpha", "filter_test_beta", 
"other_collection"};
+
+    for (String collectionName : collectionNames) {
+      CollectionAdminRequest.createCollection(collectionName, "conf", 1, 1)
+          .process(cluster.getSolrClient());
+      cluster.waitForActiveCollection(collectionName, 1, 1);
+    }
+
+    SolrClient client = cluster.getSolrClient();
+
+    // Test name filter with pattern
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.set("view", "graph");
+    params.set("filterType", "name");
+    params.set("filter", "filter_test*");
+
+    GenericSolrRequest req =
+        new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/zookeeper", 
params);
+    req.setResponseParser(new JsonMapResponseParser());
+
+    SimpleSolrResponse response = req.process(client);
+    NamedList<Object> responseData = response.getResponse();
+
+    assertNotNull("Response should not be null", responseData);
+    @SuppressWarnings("unchecked")
+    Map<String, Object> znode = (Map<String, Object>) 
responseData.get("znode");
+    assertNotNull("Response should contain 'znode'", znode);
+
+    // Verify paging information includes filter
+    String paging = (String) znode.get("paging");
+    assertNotNull("Paging information should be present", paging);
+    assertTrue("Paging should include filter type", paging.contains("name"));
+    assertTrue("Paging should include filter pattern", 
paging.contains("filter_test*"));
+
+    // Verify data field contains collection state (already parsed by 
JsonMapResponseParser)
+    Object dataObj = znode.get("data");
+    assertNotNull("Data field should be present", dataObj);
+
+    // Data should be a Map containing collection information
+    @SuppressWarnings("unchecked")
+    Map<String, Object> collectionData = (Map<String, Object>) dataObj;
+    assertNotNull("Collection data should not be null", collectionData);
+
+    // Verify filtered collections are present in the data
+    assertTrue(
+        "Should contain filter_test_alpha or filter_test_beta",
+        collectionData.containsKey("filter_test_alpha")
+            || collectionData.containsKey("filter_test_beta"));
+  }
+
+  @Test
+  public void testZkGraphResponseBuilderWithDetailParameter() throws Exception 
{
+    // Create a test collection
+    String collectionName = "zkgraph_detail_test";
+    CollectionAdminRequest.createCollection(collectionName, "conf", 1, 1)
+        .process(cluster.getSolrClient());
+    cluster.waitForActiveCollection(collectionName, 1, 1);
+
+    SolrClient client = cluster.getSolrClient();
+
+    // Test with detail parameter
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.set("view", "graph");
+    params.set("detail", "true");
+
+    GenericSolrRequest req =
+        new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/zookeeper", 
params);
+    req.setResponseParser(new JsonMapResponseParser());
+
+    SimpleSolrResponse response = req.process(client);
+    NamedList<Object> responseData = response.getResponse();
+
+    assertNotNull("Response should not be null", responseData);
+    @SuppressWarnings("unchecked")
+    Map<String, Object> znode = (Map<String, Object>) 
responseData.get("znode");
+    assertNotNull("Response should contain 'znode'", znode);
+
+    // Verify data field contains collection state (already parsed by 
JsonMapResponseParser)
+    Object dataObj = znode.get("data");
+    assertNotNull("Data field should be present", dataObj);
+
+    // Data should be a Map containing collection information
+    @SuppressWarnings("unchecked")
+    Map<String, Object> collectionData = (Map<String, Object>) dataObj;
+    assertNotNull("Collection data should not be null", collectionData);
+    assertFalse("Collection data should contain collections", 
collectionData.isEmpty());
+
+    // Verify the collection exists in the data
+    assertTrue(
+        "Data should contain the test collection", 
collectionData.containsKey(collectionName));
+
+    // Verify collection has expected structure (shards, replicas, etc.)
+    @SuppressWarnings("unchecked")
+    Map<String, Object> collectionState = (Map<String, Object>) 
collectionData.get(collectionName);
+    assertNotNull("Collection state should not be null", collectionState);
+    assertTrue("Collection should have shards", 
collectionState.containsKey("shards"));
+  }
+
+  @Test
+  public void testZkInfoHandlerForcesJsonResponse() throws Exception {
+    // Create a test collection
+    String collectionName = "zkinfo_wt_test";
+    CollectionAdminRequest.createCollection(collectionName, "conf", 1, 1)
+        .process(cluster.getSolrClient());
+    cluster.waitForActiveCollection(collectionName, 1, 1);
+
+    SolrClient client = cluster.getSolrClient();
+
+    // Try to request XML format (wt=xml), but handler should force JSON
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.set("view", "graph");
+    params.set(CommonParams.WT, "xml");
+
+    GenericSolrRequest req =
+        new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/zookeeper", 
params);
+    req.setResponseParser(new JsonMapResponseParser());
+
+    // Should still get valid JSON response despite wt=xml parameter
+    SimpleSolrResponse response = req.process(client);
+    NamedList<Object> responseData = response.getResponse();
+
+    assertNotNull("Response should not be null", responseData);
+    @SuppressWarnings("unchecked")
+    Map<String, Object> znode = (Map<String, Object>) 
responseData.get("znode");
+    assertNotNull("Response should contain 'znode' (JSON format)", znode);
+
+    // Verify we got proper JSON structure with data as Map
+    Object dataObj = znode.get("data");
+    assertNotNull("Data field should be present", dataObj);
+    assertTrue("Data should be a Map (JSON was parsed), not XML string", 
dataObj instanceof Map);
+  }
 }
diff --git a/solr/webapp/web/js/angular/controllers/cloud.js 
b/solr/webapp/web/js/angular/controllers/cloud.js
index 5bce7213b4a..e88209fda56 100644
--- a/solr/webapp/web/js/angular/controllers/cloud.js
+++ b/solr/webapp/web/js/angular/controllers/cloud.js
@@ -790,9 +790,8 @@ var graphSubController = function ($scope, Zookeeper) {
                 params.filter = filter;
             }
 
-            Zookeeper.clusterState(params, function (data) {
-                    var state = $.parseJSON(data.znode.data);
-
+          Zookeeper.clusterState(params, function (data) { 
+                    var state = data.znode.data;
                     var leaf_count = 0;
                     var graph_data = {
                         name: null,
diff --git a/solr/webapp/web/js/angular/services.js 
b/solr/webapp/web/js/angular/services.js
index 6af7cc8e309..424de8d58f5 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -99,7 +99,7 @@ solrAdminServices.factory('System',
     return $resource('admin/zookeeper', {wt:'json', _:Date.now()}, {
       "simple": {},
       "liveNodes": {params: {path: '/live_nodes'}},
-      "clusterState": {params: {detail: "true", path: "/clusterstate.json"}},
+      "clusterState": {params: {detail: "true"}},
       "detail": {params: {detail: "true", path: "@path"}},
       "configs": {params: {detail:false, path: "/configs/"}},
       "aliases": {params: {detail: "true", path: "/aliases.json"}, 
transformResponse:function(data) {
diff --git a/solr/webapp/web/partials/cloud.html 
b/solr/webapp/web/partials/cloud.html
index d5715fbaff3..ed72cbbc90e 100644
--- a/solr/webapp/web/partials/cloud.html
+++ b/solr/webapp/web/partials/cloud.html
@@ -269,7 +269,7 @@ limitations under the License.
          Filter by:&nbsp;<select ng-model="filterType" ng-change="initGraph()" 
id="cloudGraphPagingFilterType">
            <option value="status">Status</option>
            <option value="name">Name</option>
-         </select>&nbsp;T:{{filterType}}
+         </select>&nbsp;
 
          <span ng-show="filterType=='status'">
            <select ng-model="pagingStatusFilter" 
id="cloudGraphPagingStatusFilter" ng-change="resetGraph()">
@@ -281,9 +281,9 @@ limitations under the License.
            </select>
          </span>
          <span ng-show="filterType=='name'">
-           <input ng-model="pagingFilter" type="text" size="10" name="filter" 
ng-change="resetGraph()"/>&nbsp;
+           <input ng-model="pagingFilter" type="text" size="20" name="filter" 
ng-change="resetGraph()"/>&nbsp;
          </span>
-         Show <input ng-model="rows" ng-change="resetGraph()" type="text" 
size="2" name="rows" /> per page.
+         &nbsp;Show <input ng-model="rows" ng-change="resetGraph()" 
type="text" size="2" name="rows" /> per page.
          <button ng-show="nextEnabled" ng-click="next()">Next &gt;</button>
         </div>
       </div>


Reply via email to