You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2023/03/15 13:46:10 UTC

[solr] 01/02: SOLR-16372: Migrate collection listing, deletion to JAX-RS (#1412)

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

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

commit e53cb0bc8d08fd5d259c7a731b9f2c769a51b253
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Thu Mar 9 10:22:43 2023 -0500

    SOLR-16372: Migrate collection listing, deletion to JAX-RS (#1412)
    
    The endpoints themselves remain the same, other than moving to the
    JAX-RS framework.
---
 .../src/java/org/apache/solr/api/V2HttpCall.java   |  7 +-
 .../org/apache/solr/handler/CollectionsAPI.java    | 15 ----
 .../solr/handler/admin/CollectionsHandler.java     | 34 ++++----
 .../handler/admin/api/DeleteCollectionAPI.java     | 95 +++++++++++++++++-----
 .../solr/handler/admin/api/ListCollectionsAPI.java | 75 +++++++++++++++++
 .../org/apache/solr/handler/api/V2ApiUtils.java    | 20 +++++
 .../solr/jersey/CatchAllExceptionMapper.java       | 22 +----
 .../org/apache/solr/jersey/InjectionFactories.java |  1 +
 .../org/apache/solr/jersey/JerseyApplications.java |  6 +-
 .../solr/jersey/MediaTypeOverridingFilter.java     | 69 ++++++++++++++++
 .../org/apache/solr/jersey/SolrJacksonMapper.java  | 31 ++++++-
 .../SubResponseAccumulatingJerseyResponse.java     | 55 +++++++++++++
 .../apache/solr/handler/V2ApiIntegrationTest.java  |  4 +-
 .../solr/handler/admin/TestApiFramework.java       |  1 -
 .../solr/handler/admin/TestCollectionAPIs.java     |  3 -
 .../handler/admin/V2CollectionsAPIMappingTest.java |  8 --
 .../handler/admin/api/DeleteCollectionAPITest.java | 62 ++++++++++++++
 .../admin/api/V2CollectionAPIMappingTest.java      |  1 -
 .../solr/client/solrj/request/TestV2Request.java   |  7 +-
 19 files changed, 424 insertions(+), 92 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
index 1493a032036..ad8c4308fe6 100644
--- a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
+++ b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
@@ -140,7 +140,7 @@ public class V2HttpCall extends HttpSolrCall {
         assert core == null;
       }
 
-      if ("c".equals(prefix) || "collections".equals(prefix)) {
+      if (pathSegments.size() > 1 && ("c".equals(prefix) || "collections".equals(prefix))) {
         origCorename = pathSegments.get(1);
 
         DocCollection collection =
@@ -151,6 +151,11 @@ public class V2HttpCall extends HttpSolrCall {
                 SolrException.ErrorCode.BAD_REQUEST, "no such collection or alias");
           }
         } else {
+          // Certain HTTP methods are only used for admin APIs, check for those and short-circuit
+          if (List.of("delete").contains(req.getMethod().toLowerCase(Locale.ROOT))) {
+            initAdminRequest(path);
+            return;
+          }
           boolean isPreferLeader = (path.endsWith("/update") || path.contains("/update/"));
           core = getCoreByCollection(collection.getName(), isPreferLeader);
           if (core == null) {
diff --git a/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java b/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
index 11f1ee5df4a..a2b76ced5b8 100644
--- a/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
@@ -17,7 +17,6 @@
 
 package org.apache.solr.handler;
 
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
 import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
 import static org.apache.solr.client.solrj.request.beans.V2ApiConstants.ROUTER_KEY;
 import static org.apache.solr.cloud.api.collections.RoutedAlias.CREATE_COLLECTION_PREFIX;
@@ -27,9 +26,7 @@ import static org.apache.solr.common.params.CommonParams.ACTION;
 import static org.apache.solr.handler.ClusterAPI.wrapParams;
 import static org.apache.solr.handler.api.V2ApiUtils.flattenMapWithPrefix;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
-import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM;
 
-import com.google.common.collect.Maps;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -49,8 +46,6 @@ import org.apache.solr.client.solrj.request.beans.V2ApiConstants;
 import org.apache.solr.common.params.CollectionAdminParams;
 import org.apache.solr.common.params.CollectionParams.CollectionAction;
 import org.apache.solr.handler.admin.CollectionsHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
 
 /** All V2 APIs for collection management */
 public class CollectionsAPI {
@@ -70,16 +65,6 @@ public class CollectionsAPI {
     this.collectionsHandler = collectionsHandler;
   }
 
-  @EndPoint(
-      path = {"/c", "/collections"},
-      method = GET,
-      permission = COLL_READ_PERM)
-  public void getCollections(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    final Map<String, Object> v1Params = Maps.newHashMap();
-    v1Params.put(ACTION, CollectionAction.LIST.toLower());
-    collectionsHandler.handleRequestBody(wrapParams(req, v1Params), rsp);
-  }
-
   @EndPoint(
       path = {"/c", "/collections"},
       method = POST,
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index e59ce947cb5..42430bf39e7 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -218,6 +218,7 @@ import org.apache.solr.handler.admin.api.DeleteReplicaPropertyAPI;
 import org.apache.solr.handler.admin.api.DeleteShardAPI;
 import org.apache.solr.handler.admin.api.ForceLeaderAPI;
 import org.apache.solr.handler.admin.api.ListAliasesAPI;
+import org.apache.solr.handler.admin.api.ListCollectionsAPI;
 import org.apache.solr.handler.admin.api.MigrateDocsAPI;
 import org.apache.solr.handler.admin.api.ModifyCollectionAPI;
 import org.apache.solr.handler.admin.api.MoveReplicaAPI;
@@ -666,8 +667,17 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     DELETE_OP(
         DELETE,
         (req, rsp, h) -> {
-          Map<String, Object> map = copy(req.getParams().required(), null, NAME);
-          return copy(req.getParams(), map, FOLLOW_ALIASES);
+          final RequiredSolrParams requiredParams = req.getParams().required();
+          final DeleteCollectionAPI deleteCollectionAPI =
+              new DeleteCollectionAPI(h.coreContainer, req, rsp);
+          final SolrJerseyResponse deleteCollResponse =
+              deleteCollectionAPI.deleteCollection(
+                  requiredParams.get(NAME),
+                  req.getParams().getBool(FOLLOW_ALIASES),
+                  req.getParams().get(ASYNC));
+          V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, deleteCollResponse);
+
+          return null;
         }),
     // XXX should this command support followAliases?
     RELOAD_OP(
@@ -1232,19 +1242,10 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     LIST_OP(
         LIST,
         (req, rsp, h) -> {
-          NamedList<Object> results = new NamedList<>();
-          Map<String, DocCollection> collections =
-              h.coreContainer
-                  .getZkController()
-                  .getZkStateReader()
-                  .getClusterState()
-                  .getCollectionsMap();
-          List<String> collectionList = new ArrayList<>(collections.keySet());
-          Collections.sort(collectionList);
-          // XXX should we add aliases here?
-          results.add("collections", collectionList);
-          SolrResponse response = new OverseerSolrResponse(results);
-          rsp.getValues().addAll(response.getResponse());
+          final ListCollectionsAPI listCollectionsAPI =
+              new ListCollectionsAPI(h.coreContainer, req, rsp);
+          final SolrJerseyResponse listCollectionsResponse = listCollectionsAPI.listCollections();
+          V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, listCollectionsResponse);
           return null;
         }),
     /**
@@ -2072,7 +2073,9 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
   public Collection<Class<? extends JerseyResource>> getJerseyResources() {
     return List.of(
         AddReplicaPropertyAPI.class,
+        DeleteCollectionAPI.class,
         DeleteReplicaPropertyAPI.class,
+        ListCollectionsAPI.class,
         ReplaceNodeAPI.class,
         DeleteNodeAPI.class,
         ListAliasesAPI.class);
@@ -2089,7 +2092,6 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     apis.addAll(AnnotatedApi.getApis(new ForceLeaderAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new DeleteReplicaAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new BalanceShardUniqueAPI(this)));
-    apis.addAll(AnnotatedApi.getApis(new DeleteCollectionAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new MigrateDocsAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new ModifyCollectionAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new MoveReplicaAPI(this)));
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionAPI.java
index 80e4ff192db..2c38da38a87 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionAPI.java
@@ -16,16 +16,29 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
-import static org.apache.solr.common.params.CommonParams.ACTION;
+import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
-import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.common.cloud.ZkStateReader;
+import java.util.HashMap;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.core.CoreContainer;
 import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 
@@ -35,26 +48,64 @@ import org.apache.solr.response.SolrQueryResponse;
  * <p>This API (DELETE /v2/collections/collectionName) is equivalent to the v1
  * /admin/collections?action=DELETE command.
  */
-public class DeleteCollectionAPI {
+@Path("collections/")
+public class DeleteCollectionAPI extends AdminAPIBase {
 
-  private final CollectionsHandler collectionsHandler;
+  @Inject
+  public DeleteCollectionAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @DELETE
+  @Path("{collectionName}")
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(COLL_EDIT_PERM)
+  public SubResponseAccumulatingJerseyResponse deleteCollection(
+      @PathParam("collectionName") String collectionName,
+      @QueryParam("followAliases") Boolean followAliases,
+      @QueryParam("async") String asyncId)
+      throws Exception {
+    final SubResponseAccumulatingJerseyResponse response =
+        instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
+    recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
+
+    final ZkNodeProps remoteMessage = createRemoteMessage(collectionName, followAliases, asyncId);
+    final SolrResponse remoteResponse =
+        CollectionsHandler.submitCollectionApiCommand(
+            coreContainer,
+            coreContainer.getDistributedCollectionCommandRunner(),
+            remoteMessage,
+            CollectionParams.CollectionAction.DELETE,
+            DEFAULT_COLLECTION_OP_TIMEOUT);
+    if (remoteResponse.getException() != null) {
+      throw remoteResponse.getException();
+    }
+
+    if (asyncId != null) {
+      response.requestId = asyncId;
+      return response;
+    }
 
-  public DeleteCollectionAPI(CollectionsHandler collectionsHandler) {
-    this.collectionsHandler = collectionsHandler;
+    // Values fetched from remoteResponse may be null
+    response.successfulSubResponsesByNodeName = remoteResponse.getResponse().get("success");
+    response.failedSubResponsesByNodeName = remoteResponse.getResponse().get("failure");
+
+    return response;
   }
 
-  @EndPoint(
-      path = {"/c/{collection}", "/collections/{collection}"},
-      method = DELETE,
-      permission = COLL_EDIT_PERM)
-  public void deleteCollection(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    req =
-        wrapParams(
-            req,
-            ACTION,
-            CollectionParams.CollectionAction.DELETE.toString(),
-            NAME,
-            req.getPathTemplateValues().get(ZkStateReader.COLLECTION_PROP));
-    collectionsHandler.handleRequestBody(req, rsp);
+  public static ZkNodeProps createRemoteMessage(
+      String collectionName, Boolean followAliases, String asyncId) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+
+    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.DELETE.toLower());
+    remoteMessage.put(NAME, collectionName);
+    if (followAliases != null) remoteMessage.put(FOLLOW_ALIASES, followAliases);
+    if (asyncId != null) remoteMessage.put(ASYNC, asyncId);
+
+    return new ZkNodeProps(remoteMessage);
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollectionsAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollectionsAPI.java
new file mode 100644
index 00000000000..df4fc547bf2
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollectionsAPI.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.handler.admin.api;
+
+import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for listing collections.
+ *
+ * <p>This API (GET /v2/collections) is equivalent to the v1 /admin/collections?action=LIST command
+ */
+@Path("/collections")
+public class ListCollectionsAPI extends AdminAPIBase {
+
+  @Inject
+  public ListCollectionsAPI(
+      CoreContainer coreContainer, SolrQueryRequest req, SolrQueryResponse rsp) {
+    super(coreContainer, req, rsp);
+  }
+
+  @GET
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(COLL_READ_PERM)
+  public ListCollectionsResponse listCollections() {
+    final ListCollectionsResponse response =
+        instantiateJerseyResponse(ListCollectionsResponse.class);
+    validateZooKeeperAwareCoreContainer(coreContainer);
+
+    Map<String, DocCollection> collections =
+        coreContainer.getZkController().getZkStateReader().getClusterState().getCollectionsMap();
+    List<String> collectionList = new ArrayList<>(collections.keySet());
+    Collections.sort(collectionList);
+    // XXX should we add aliases here?
+    response.collections = collectionList;
+
+    return response;
+  }
+
+  public static class ListCollectionsResponse extends SolrJerseyResponse {
+    @JsonProperty("collections")
+    public List<String> collections;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java b/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
index d459a85caf6..300082406e7 100644
--- a/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
+++ b/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
@@ -17,12 +17,16 @@
 
 package org.apache.solr.handler.api;
 
+import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.common.params.CommonParams.WT;
+
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 import org.apache.solr.common.MapWriter.EntryWriter;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 
 /** Utilities helpful for common V2 API declaration tasks. */
@@ -81,6 +85,22 @@ public class V2ApiUtils {
     squashIntoNamedList(destination, mw, false);
   }
 
+  public static String getMediaTypeFromWtParam(
+      SolrQueryRequest solrQueryRequest, String defaultMediaType) {
+    final String wtParam = solrQueryRequest.getParams().get(WT);
+    if (wtParam == null) return "application/json";
+
+    // The only currently-supported response-formats for JAX-RS v2 endpoints.
+    switch (wtParam) {
+      case "xml":
+        return "application/xml";
+      case "javabin":
+        return BINARY_CONTENT_TYPE_V2;
+      default:
+        return defaultMediaType;
+    }
+  }
+
   private static void squashIntoNamedList(
       NamedList<Object> destination, JacksonReflectMapWriter mw, boolean trimHeader) {
     try {
diff --git a/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java b/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
index 2ff8253216d..6dfec0fe1ba 100644
--- a/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
+++ b/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
@@ -17,9 +17,7 @@
 
 package org.apache.solr.jersey;
 
-import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
 import static org.apache.solr.common.SolrException.ErrorCode.getErrorCode;
-import static org.apache.solr.common.params.CommonParams.WT;
 import static org.apache.solr.jersey.RequestContextKeys.HANDLER_METRICS;
 import static org.apache.solr.jersey.RequestContextKeys.SOLR_JERSEY_RESPONSE;
 import static org.apache.solr.jersey.RequestContextKeys.SOLR_QUERY_REQUEST;
@@ -30,10 +28,12 @@ import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.container.ResourceContext;
 import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.ext.ExceptionMapper;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.handler.RequestHandlerBase;
+import org.apache.solr.handler.api.V2ApiUtils;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.servlet.ResponseUtils;
@@ -110,25 +110,11 @@ public class CatchAllExceptionMapper implements ExceptionMapper<Exception> {
 
     response.error = ResponseUtils.getTypedErrorInfo(normalizedException, log);
     response.responseHeader.status = response.error.code;
-    final String mediaType = getMediaType(solrQueryRequest);
+    final String mediaType =
+        V2ApiUtils.getMediaTypeFromWtParam(solrQueryRequest, MediaType.APPLICATION_JSON);
     return Response.status(response.error.code).type(mediaType).entity(response).build();
   }
 
-  private static String getMediaType(SolrQueryRequest solrQueryRequest) {
-    final String wtParam = solrQueryRequest.getParams().get(WT);
-    if (wtParam == null) return "application/json";
-
-    // The only currently-supported response-formats for JAX-RS v2 endpoints.
-    switch (wtParam) {
-      case "xml":
-        return "application/xml";
-      case "javabin":
-        return BINARY_CONTENT_TYPE_V2;
-      default:
-        return "application/json";
-    }
-  }
-
   private Response processWebApplicationException(WebApplicationException wae) {
     return wae.getResponse();
   }
diff --git a/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java b/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
index 758f32adbe3..2cbc5f99cfd 100644
--- a/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
+++ b/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
@@ -27,6 +27,7 @@ import org.apache.solr.response.SolrQueryResponse;
 import org.glassfish.hk2.api.Factory;
 
 public class InjectionFactories {
+
   public static class SolrQueryRequestFactory implements Factory<SolrQueryRequest> {
 
     private final ContainerRequestContext containerRequestContext;
diff --git a/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java b/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
index cb6b5bf0cde..ef9587ceb90 100644
--- a/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
+++ b/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
@@ -62,6 +62,7 @@ public class JerseyApplications {
       // Request lifecycle logic
       register(CatchAllExceptionMapper.class);
       register(NotFoundExceptionMapper.class);
+      register(MediaTypeOverridingFilter.class);
       register(RequestMetricHandling.PreRequestMetricsFilter.class);
       register(RequestMetricHandling.PostRequestMetricsFilter.class);
       register(PostRequestDecorationFilter.class);
@@ -84,9 +85,12 @@ public class JerseyApplications {
             }
           });
 
+      // Explicit Jersey logging is disabled by default but useful for debugging (pt 1)
+      // register(LoggingFeature.class);
+
       setProperties(
           Map.of(
-              // Explicit Jersey logging is disabled by default but useful for debugging
+              // Explicit Jersey logging is disabled by default but useful for debugging (pt 2)
               // "jersey.config.server.tracing.type", "ALL",
               // "jersey.config.server.tracing.threshold", "VERBOSE",
               "jersey.config.server.wadl.disableWadl", "true",
diff --git a/solr/core/src/java/org/apache/solr/jersey/MediaTypeOverridingFilter.java b/solr/core/src/java/org/apache/solr/jersey/MediaTypeOverridingFilter.java
new file mode 100644
index 00000000000..00cda1f08a9
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/jersey/MediaTypeOverridingFilter.java
@@ -0,0 +1,69 @@
+/*
+ * 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.solr.jersey;
+
+import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
+import static org.apache.solr.jersey.RequestContextKeys.SOLR_QUERY_REQUEST;
+
+import java.io.IOException;
+import java.util.List;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerResponseContext;
+import javax.ws.rs.container.ContainerResponseFilter;
+import javax.ws.rs.container.ResourceInfo;
+import javax.ws.rs.core.Context;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.handler.admin.ZookeeperReadAPI;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.request.SolrQueryRequest;
+
+// TODO Deprecate or remove support for the 'wt' parameter in the v2 APIs in favor of the more
+//  HTTP-compliant 'Accept' header
+/** Overrides the content-type of the response based on an optional user-provided 'wt' parameter */
+public class MediaTypeOverridingFilter implements ContainerResponseFilter {
+
+  private static final List<Class<? extends JerseyResource>> EXEMPTED_RESOURCES =
+      List.of(ZookeeperReadAPI.class);
+
+  @Context private ResourceInfo resourceInfo;
+
+  @Override
+  public void filter(
+      ContainerRequestContext requestContext, ContainerResponseContext responseContext)
+      throws IOException {
+
+    // Solr has historically ignored 'wt' for client or server error responses, so maintain that
+    // behavior here for compatibility.
+    if (responseContext.getStatus() >= 400) {
+      return;
+    }
+
+    // Some endpoints have their own media-type logic and opt out of the overriding behavior this
+    // filter provides.
+    if (EXEMPTED_RESOURCES.contains(resourceInfo.getResourceClass())) {
+      return;
+    }
+
+    final SolrQueryRequest solrQueryRequest =
+        (SolrQueryRequest) requestContext.getProperty(SOLR_QUERY_REQUEST);
+    final String mediaType = V2ApiUtils.getMediaTypeFromWtParam(solrQueryRequest, null);
+    if (mediaType != null) {
+      responseContext.getHeaders().putSingle(CONTENT_TYPE, mediaType);
+    }
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java b/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
index 94d8e090b21..00513d6ce82 100644
--- a/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
+++ b/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
@@ -18,15 +18,44 @@
 package org.apache.solr.jersey;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import java.io.IOException;
 import javax.ws.rs.ext.ContextResolver;
 import javax.ws.rs.ext.Provider;
+import org.apache.solr.common.util.NamedList;
 
 /** Customizes the ObjectMapper settings used for serialization/deserialization in Jersey */
+@SuppressWarnings("rawtypes")
 @Provider
 public class SolrJacksonMapper implements ContextResolver<ObjectMapper> {
   @Override
   public ObjectMapper getContext(Class<?> type) {
-    return new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
+    final SimpleModule customTypeModule = new SimpleModule();
+    customTypeModule.addSerializer(new NamedListSerializer(NamedList.class));
+
+    return new ObjectMapper()
+        .setSerializationInclusion(JsonInclude.Include.NON_NULL)
+        .registerModule(customTypeModule);
+  }
+
+  public static class NamedListSerializer extends StdSerializer<NamedList> {
+
+    public NamedListSerializer() {
+      this(null);
+    }
+
+    public NamedListSerializer(Class<NamedList> nlClazz) {
+      super(nlClazz);
+    }
+
+    @Override
+    public void serialize(NamedList value, JsonGenerator gen, SerializerProvider provider)
+        throws IOException {
+      gen.writeObject(value.asShallowMap());
+    }
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/jersey/SubResponseAccumulatingJerseyResponse.java b/solr/core/src/java/org/apache/solr/jersey/SubResponseAccumulatingJerseyResponse.java
new file mode 100644
index 00000000000..ab476771d75
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/jersey/SubResponseAccumulatingJerseyResponse.java
@@ -0,0 +1,55 @@
+/*
+ * 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.solr.jersey;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Represents API responses composed of the responses of various sub-requests.
+ *
+ * <p>Many Solr APIs, particularly those historically reliant on overseer processing, return a
+ * response to the user that is composed in large part of the responses from all sub-requests made
+ * during the APIs execution. (e.g. the collection-deletion response itself contains the responses
+ * from the 'UNLOAD' call send to each core.) This class encapsulates those responses as possible.
+ */
+public class SubResponseAccumulatingJerseyResponse extends SolrJerseyResponse {
+
+  @JsonProperty("requestid")
+  public String requestId;
+
+  // TODO The 'Object' value in this and the failure prop below have a more defined structure.
+  //  Specifically, each value is a map whose keys are node names and whose values are full
+  //  responses (in NamedList form) of all shard or replica requests made to that node by the
+  //  overseer.  We've skipped being more explicit here, type-wise, for a few reasons:
+  //  1. While the overseer response comes back as a raw NamedList, there's no good way to
+  //     serialize it into a more strongly-typed response without some ugly NL inspection code
+  //  2. The overseer response can include duplicate keys when multiple replica-requests are sent
+  //     by the overseer to the same node.  This makes the overseer response invalid JSON, and
+  //     prevents utilizing Jackson for serde.
+  //  3. This would still all be surmountable if the user response for overseer-based APIs was
+  //     especially worth preserving, but it's not.  We should rework this response format to be
+  //     less verbose in the successful case and to be more explicit in the failure case about
+  //     which internal replica requests failed.
+  //  We should either change this response format to be more helpful, or add stronger typing to
+  //  overseer responses so that being more type-explicit here is feasible.
+  @JsonProperty("success")
+  public Object successfulSubResponsesByNodeName;
+
+  @JsonProperty("failure")
+  public Object failedSubResponsesByNodeName;
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
index 893125713f1..6c904312160 100644
--- a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
@@ -77,7 +77,9 @@ public class V2ApiIntegrationTest extends SolrCloudTestCase {
     BaseHttpSolrClient.RemoteSolrException ex =
         expectThrows(
             BaseHttpSolrClient.RemoteSolrException.class,
-            () -> v2Request.process(cluster.getSolrClient()));
+            () -> {
+              v2Request.process(cluster.getSolrClient());
+            });
     assertEquals(expectedCode, ex.code());
   }
 
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
index c5352514356..af2a4e0c590 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
@@ -137,7 +137,6 @@ public class TestApiFramework extends SolrTestCaseJ4 {
     methodNames.add(rsp.getValues()._getStr("/spec[0]/methods[0]", null));
     methodNames.add(rsp.getValues()._getStr("/spec[1]/methods[0]", null));
     methodNames.add(rsp.getValues()._getStr("/spec[2]/methods[0]", null));
-    assertTrue(methodNames.contains("DELETE"));
     assertTrue(methodNames.contains("POST"));
     assertTrue(methodNames.contains("GET"));
 
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java b/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
index e074f0ba40d..680eaa95747 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
@@ -139,9 +139,6 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
     compareOutput(
         apiBag, "/collections/collName", POST, "{reload:{}}", "{name:collName, operation :reload}");
 
-    compareOutput(
-        apiBag, "/collections/collName", DELETE, null, "{name:collName, operation :delete}");
-
     compareOutput(
         apiBag,
         "/collections/collName/shards/shard1",
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
index 230bf604f73..2949fd38a67 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
@@ -113,14 +113,6 @@ public class V2CollectionsAPIMappingTest extends V2ApiMappingTest<CollectionsHan
     assertEquals(1, v1Params.getPrimitiveInt(CollectionAdminParams.NUM_SHARDS));
   }
 
-  @Test
-  public void testListCollectionsAllProperties() throws Exception {
-    final String noBody = null;
-    final SolrParams v1Params = captureConvertedV1Params("/collections", "GET", noBody);
-
-    assertEquals(CollectionParams.CollectionAction.LIST.lowerName, v1Params.get(ACTION));
-  }
-
   @Test
   public void testCreateAliasAllProperties() throws Exception {
     final SolrParams v1Params =
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionAPITest.java
new file mode 100644
index 00000000000..a87dad3c900
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionAPITest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.solr.handler.admin.api;
+
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CollectionParams.NAME;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+
+import java.util.Map;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.hamcrest.MatcherAssert;
+import org.junit.Test;
+
+/** Unit tests for {@link DeleteCollectionAPI} */
+public class DeleteCollectionAPITest extends SolrTestCaseJ4 {
+
+  @Test
+  public void testConstructsValidOverseerMessage() {
+    // Only required properties provided
+    {
+      final ZkNodeProps message =
+          DeleteCollectionAPI.createRemoteMessage("someCollName", null, null);
+      final Map<String, Object> rawMessage = message.getProperties();
+      assertEquals(2, rawMessage.size());
+      MatcherAssert.assertThat(rawMessage.keySet(), containsInAnyOrder(QUEUE_OPERATION, NAME));
+      assertEquals("delete", rawMessage.get(QUEUE_OPERATION));
+      assertEquals("someCollName", rawMessage.get(NAME));
+    }
+
+    // Optional properties ('followAliases' and 'async') also provided
+    {
+      final ZkNodeProps message =
+          DeleteCollectionAPI.createRemoteMessage("someCollName", Boolean.TRUE, "someAsyncId");
+      final Map<String, Object> rawMessage = message.getProperties();
+      assertEquals(4, rawMessage.size());
+      MatcherAssert.assertThat(
+          rawMessage.keySet(), containsInAnyOrder(QUEUE_OPERATION, NAME, ASYNC, FOLLOW_ALIASES));
+      assertEquals("delete", rawMessage.get(QUEUE_OPERATION));
+      assertEquals("someCollName", rawMessage.get(NAME));
+      assertEquals(Boolean.TRUE, rawMessage.get(FOLLOW_ALIASES));
+      assertEquals("someAsyncId", rawMessage.get(ASYNC));
+    }
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java
index 65feab737db..e2ead1e452d 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java
@@ -55,7 +55,6 @@ public class V2CollectionAPIMappingTest extends V2ApiMappingTest<CollectionsHand
   public void populateApiBag() {
     final CollectionsHandler collectionsHandler = getRequestHandler();
     apiBag.registerObject(new BalanceShardUniqueAPI(collectionsHandler));
-    apiBag.registerObject(new DeleteCollectionAPI(collectionsHandler));
     apiBag.registerObject(new MigrateDocsAPI(collectionsHandler));
     apiBag.registerObject(new ModifyCollectionAPI(collectionsHandler));
     apiBag.registerObject(new MoveReplicaAPI(collectionsHandler));
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/TestV2Request.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/TestV2Request.java
index 315bced3ee0..ab8c81c7f24 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/request/TestV2Request.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/TestV2Request.java
@@ -112,8 +112,6 @@ public class TestV2Request extends SolrCloudTestCase {
                     + "}"
                     + "/* ignore comment*/")
             .build());
-    assertSuccess(client, new V2Request.Builder("/c").build());
-    assertSuccess(client, new V2Request.Builder("/c/_introspect").build());
 
     String requestHandlerName = "/x" + random().nextInt();
     assertSuccess(
@@ -127,8 +125,9 @@ public class TestV2Request extends SolrCloudTestCase {
             .build());
 
     assertSuccess(
-        client, new V2Request.Builder("/c/test").withMethod(SolrRequest.METHOD.DELETE).build());
-    NamedList<Object> res = client.request(new V2Request.Builder("/c").build());
+        client,
+        new V2Request.Builder("/collections/test").withMethod(SolrRequest.METHOD.DELETE).build());
+    NamedList<Object> res = client.request(new V2Request.Builder("/collections").build());
 
     // TODO: this is not guaranteed now - beast test if you try to fix
     // List collections = (List) res.get("collections");