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:09 UTC

[solr] branch branch_9x updated (34c85d34038 -> 3a0771202cf)

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

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


    from 34c85d34038 SOLR-16638: Fix Http2SolrClient's exception message when serverBaseUrl is null (#1356)
     new e53cb0bc8d0 SOLR-16372: Migrate collection listing, deletion to JAX-RS (#1412)
     new 3a0771202cf SOLR-16391: Migrate collprop APIs to JAX-RS (#1441)

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 solr/CHANGES.txt                                   |   4 +
 .../src/java/org/apache/solr/api/V2HttpCall.java   |   7 +-
 .../org/apache/solr/handler/CollectionsAPI.java    |  15 ----
 .../solr/handler/admin/CollectionsHandler.java     |  65 +++++++-------
 .../handler/admin/api/CollectionPropertyAPI.java   | 100 +++++++++++++++++++++
 .../handler/admin/api/DeleteCollectionAPI.java     |  95 +++++++++++++++-----
 .../solr/handler/admin/api/ListCollectionsAPI.java |  75 ++++++++++++++++
 .../admin/api/SetCollectionPropertyAPI.java        |  71 ---------------
 .../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 +-
 ...nFilter.java => MediaTypeOverridingFilter.java} |  55 +++++++-----
 .../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     |  10 ---
 .../handler/admin/V2CollectionsAPIMappingTest.java |   8 --
 .../handler/admin/api/DeleteCollectionAPITest.java |  62 +++++++++++++
 .../admin/api/V2CollectionAPIMappingTest.java      |  19 ----
 .../pages/collection-management.adoc               |  23 +++--
 .../beans/SetCollectionPropertyPayload.java        |  27 ------
 .../solr/client/solrj/request/TestV2Request.java   |   7 +-
 24 files changed, 521 insertions(+), 262 deletions(-)
 create mode 100644 solr/core/src/java/org/apache/solr/handler/admin/api/CollectionPropertyAPI.java
 create mode 100644 solr/core/src/java/org/apache/solr/handler/admin/api/ListCollectionsAPI.java
 delete mode 100644 solr/core/src/java/org/apache/solr/handler/admin/api/SetCollectionPropertyAPI.java
 copy solr/core/src/java/org/apache/solr/jersey/{PostRequestDecorationFilter.java => MediaTypeOverridingFilter.java} (50%)
 create mode 100644 solr/core/src/java/org/apache/solr/jersey/SubResponseAccumulatingJerseyResponse.java
 create mode 100644 solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionAPITest.java
 delete mode 100644 solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetCollectionPropertyPayload.java


[solr] 02/02: SOLR-16391: Migrate collprop APIs to JAX-RS (#1441)

Posted by ge...@apache.org.
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 3a0771202cfe7b3841a1aca249b39b35b5df54c0
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Mon Mar 13 12:45:23 2023 -0400

    SOLR-16391: Migrate collprop APIs to JAX-RS (#1441)
    
    This commit makes various cosmetic improvements to Solr's v2 collprop
    CRUD APIs, to bring them more into line with the more REST-ful v2
    design.  In the process it also converts these APIs to our JAX-RS
    framework.
    
    As of this commit, our v2 collprop APIs are now:
      - PUT /api/collections/collName/properties/propName {value: newVal}
      - DELETE /api/collections/collName/properties/propName
---
 solr/CHANGES.txt                                   |   4 +
 .../solr/handler/admin/CollectionsHandler.java     |  31 +++----
 .../handler/admin/api/CollectionPropertyAPI.java   | 100 +++++++++++++++++++++
 .../admin/api/SetCollectionPropertyAPI.java        |  71 ---------------
 .../solr/handler/admin/TestCollectionAPIs.java     |   7 --
 .../admin/api/V2CollectionAPIMappingTest.java      |  18 ----
 .../pages/collection-management.adoc               |  23 +++--
 .../beans/SetCollectionPropertyPayload.java        |  27 ------
 8 files changed, 135 insertions(+), 146 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index a6373a82a54..fd3dcbb1439 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -152,6 +152,10 @@ Improvements
 
 * SOLR-16397: /mlt now has a v2 API available at `GET /api/collections/collName/mlt` (Ameer Albahem via Jason Gerlowski)
 
+* SOLR-16391: The path of the v2 "collprop" API has been tweaked slightly to be more intuitive.  Endpoints are now
+  available under the `PUT` and `DELETE` verbs at `/api/collections/collName/properties/propName` depending on whether the property is
+  being upserted or deleted. (Jason Gerlowski)
+
 Optimizations
 ---------------------
 
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 42430bf39e7..e154824ea29 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
@@ -171,7 +171,6 @@ import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.cloud.ClusterProperties;
 import org.apache.solr.common.cloud.ClusterState;
-import org.apache.solr.common.cloud.CollectionProperties;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.DocCollection.CollectionStateProps;
 import org.apache.solr.common.cloud.ImplicitDocRouter;
@@ -209,6 +208,7 @@ import org.apache.solr.handler.admin.api.AddReplicaAPI;
 import org.apache.solr.handler.admin.api.AddReplicaPropertyAPI;
 import org.apache.solr.handler.admin.api.AdminAPIBase;
 import org.apache.solr.handler.admin.api.BalanceShardUniqueAPI;
+import org.apache.solr.handler.admin.api.CollectionPropertyAPI;
 import org.apache.solr.handler.admin.api.CollectionStatusAPI;
 import org.apache.solr.handler.admin.api.CreateShardAPI;
 import org.apache.solr.handler.admin.api.DeleteCollectionAPI;
@@ -226,7 +226,6 @@ import org.apache.solr.handler.admin.api.RebalanceLeadersAPI;
 import org.apache.solr.handler.admin.api.ReloadCollectionAPI;
 import org.apache.solr.handler.admin.api.RenameCollectionAPI;
 import org.apache.solr.handler.admin.api.ReplaceNodeAPI;
-import org.apache.solr.handler.admin.api.SetCollectionPropertyAPI;
 import org.apache.solr.handler.admin.api.SplitShardAPI;
 import org.apache.solr.handler.admin.api.SyncShardAPI;
 import org.apache.solr.handler.api.V2ApiUtils;
@@ -1049,18 +1048,20 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     COLLECTIONPROP_OP(
         COLLECTIONPROP,
         (req, rsp, h) -> {
-          String extCollection = req.getParams().required().get(NAME);
-          String collection =
-              h.coreContainer
-                  .getZkController()
-                  .getZkStateReader()
-                  .getAliases()
-                  .resolveSimpleAlias(extCollection);
-          String name = req.getParams().required().get(PROPERTY_NAME);
-          String val = req.getParams().get(PROPERTY_VALUE);
-          CollectionProperties cp =
-              new CollectionProperties(h.coreContainer.getZkController().getZkClient());
-          cp.setCollectionProperty(collection, name, val);
+          final String collection = req.getParams().required().get(NAME);
+          final String propName = req.getParams().required().get(PROPERTY_NAME);
+          final String val = req.getParams().get(PROPERTY_VALUE);
+
+          final CollectionPropertyAPI setCollPropApi =
+              new CollectionPropertyAPI(h.coreContainer, req, rsp);
+          final SolrJerseyResponse setPropRsp =
+              (val != null)
+                  ? setCollPropApi.createOrUpdateCollectionProperty(
+                      collection,
+                      propName,
+                      new CollectionPropertyAPI.UpdateCollectionPropertyRequestBody(val))
+                  : setCollPropApi.deleteCollectionProperty(collection, propName);
+          V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, setPropRsp);
           return null;
         }),
     REQUESTSTATUS_OP(
@@ -2077,6 +2078,7 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
         DeleteReplicaPropertyAPI.class,
         ListCollectionsAPI.class,
         ReplaceNodeAPI.class,
+        CollectionPropertyAPI.class,
         DeleteNodeAPI.class,
         ListAliasesAPI.class);
   }
@@ -2097,7 +2099,6 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     apis.addAll(AnnotatedApi.getApis(new MoveReplicaAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new RebalanceLeadersAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new ReloadCollectionAPI(this)));
-    apis.addAll(AnnotatedApi.getApis(new SetCollectionPropertyAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new CollectionStatusAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new RenameCollectionAPI(this)));
     return apis;
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionPropertyAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionPropertyAPI.java
new file mode 100644
index 00000000000..72d3c1e8145
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionPropertyAPI.java
@@ -0,0 +1,100 @@
+/*
+ * 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_EDIT_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.IOException;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import org.apache.solr.common.cloud.CollectionProperties;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+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 modifying collection-level properties.
+ *
+ * <p>These APIs (PUT and DELETE /api/collections/collName/properties/propName) are analogous to the
+ * v1 /admin/collections?action=COLLECTIONPROP command.
+ */
+@Path("/collections/{collName}/properties/{propName}")
+public class CollectionPropertyAPI extends AdminAPIBase {
+
+  public CollectionPropertyAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @PUT
+  @PermissionName(COLL_EDIT_PERM)
+  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  public SolrJerseyResponse createOrUpdateCollectionProperty(
+      @PathParam("collName") String collName,
+      @PathParam("propName") String propName,
+      UpdateCollectionPropertyRequestBody requestBody)
+      throws Exception {
+    final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    recordCollectionForLogAndTracing(collName, solrQueryRequest);
+    modifyCollectionProperty(collName, propName, requestBody.value);
+    return response;
+  }
+
+  @DELETE
+  @PermissionName(COLL_EDIT_PERM)
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  public SolrJerseyResponse deleteCollectionProperty(
+      @PathParam("collName") String collName, @PathParam("propName") String propName)
+      throws Exception {
+    final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    recordCollectionForLogAndTracing(collName, solrQueryRequest);
+    modifyCollectionProperty(collName, propName, null);
+    return response;
+  }
+
+  private void modifyCollectionProperty(
+      String collection, String propertyName, String propertyValue /* May be null for deletes */)
+      throws IOException {
+    String resolvedCollection = coreContainer.getAliases().resolveSimpleAlias(collection);
+    CollectionProperties cp =
+        new CollectionProperties(coreContainer.getZkController().getZkClient());
+    cp.setCollectionProperty(resolvedCollection, propertyName, propertyValue);
+  }
+
+  public static class UpdateCollectionPropertyRequestBody implements JacksonReflectMapWriter {
+    public UpdateCollectionPropertyRequestBody() {}
+
+    public UpdateCollectionPropertyRequestBody(String value) {
+      this.value = value;
+    }
+
+    @JsonProperty(required = true)
+    public String value;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/SetCollectionPropertyAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/SetCollectionPropertyAPI.java
deleted file mode 100644
index ea3c47abf03..00000000000
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/SetCollectionPropertyAPI.java
+++ /dev/null
@@ -1,71 +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.solr.handler.admin.api;
-
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
-import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
-import static org.apache.solr.common.params.CommonParams.ACTION;
-import static org.apache.solr.common.params.CommonParams.NAME;
-import static org.apache.solr.handler.ClusterAPI.wrapParams;
-import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
-
-import java.util.HashMap;
-import java.util.Map;
-import org.apache.solr.api.Command;
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.api.PayloadObj;
-import org.apache.solr.client.solrj.request.beans.SetCollectionPropertyPayload;
-import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.handler.admin.CollectionsHandler;
-
-/**
- * V2 API for modifying collection-level properties.
- *
- * <p>This API (POST /v2/collections/collectionName {'set-collection-property': {...}}) is analogous
- * to the v1 /admin/collections?action=COLLECTIONPROP command.
- *
- * @see SetCollectionPropertyPayload
- */
-@EndPoint(
-    path = {"/c/{collection}", "/collections/{collection}"},
-    method = POST,
-    permission = COLL_EDIT_PERM)
-public class SetCollectionPropertyAPI {
-  private static final String V2_SET_COLLECTION_PROPERTY_CMD = "set-collection-property";
-
-  private final CollectionsHandler collectionsHandler;
-
-  public SetCollectionPropertyAPI(CollectionsHandler collectionsHandler) {
-    this.collectionsHandler = collectionsHandler;
-  }
-
-  @Command(name = V2_SET_COLLECTION_PROPERTY_CMD)
-  public void setCollectionProperty(PayloadObj<SetCollectionPropertyPayload> obj) throws Exception {
-    final SetCollectionPropertyPayload v2Body = obj.get();
-    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
-
-    v1Params.put("propertyName", v1Params.remove("name"));
-    if (v2Body.value != null) {
-      v1Params.put("propertyValue", v2Body.value);
-    }
-    v1Params.put(ACTION, CollectionParams.CollectionAction.COLLECTIONPROP.toLower());
-    v1Params.put(NAME, obj.getRequest().getPathTemplateValues().get(COLLECTION));
-
-    collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
-  }
-}
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 680eaa95747..326066239ad 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
@@ -223,13 +223,6 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
         POST,
         "{migrate-docs : {forwardTimeout: 1800, target: coll2, splitKey: 'a123!'} }",
         "{operation : migrate ,collection : coll1, target.collection:coll2, forward.timeout:1800, split.key:'a123!'}");
-
-    compareOutput(
-        apiBag,
-        "/collections/coll1",
-        POST,
-        "{set-collection-property : {name: 'foo', value:'bar'} }",
-        "{operation : collectionprop, name : coll1, propertyName:'foo', propertyValue:'bar'}");
   }
 
   static ZkNodeProps compareOutput(
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 e2ead1e452d..fa943c80740 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
@@ -60,7 +60,6 @@ public class V2CollectionAPIMappingTest extends V2ApiMappingTest<CollectionsHand
     apiBag.registerObject(new MoveReplicaAPI(collectionsHandler));
     apiBag.registerObject(new RebalanceLeadersAPI(collectionsHandler));
     apiBag.registerObject(new ReloadCollectionAPI(collectionsHandler));
-    apiBag.registerObject(new SetCollectionPropertyAPI(collectionsHandler));
     apiBag.registerObject(new CollectionStatusAPI(collectionsHandler));
     apiBag.registerObject(new RenameCollectionAPI(collectionsHandler));
   }
@@ -226,21 +225,4 @@ public class V2CollectionAPIMappingTest extends V2ApiMappingTest<CollectionsHand
     assertEquals(123, v1Params.getPrimitiveInt("maxAtOnce"));
     assertEquals(456, v1Params.getPrimitiveInt("maxWaitSeconds"));
   }
-
-  @Test
-  public void testSetCollectionPropertyAllProperties() throws Exception {
-    final SolrParams v1Params =
-        captureConvertedV1Params(
-            "/collections/collName",
-            "POST",
-            "{ 'set-collection-property': {"
-                + "'name': 'somePropertyName', "
-                + "'value': 'somePropertyValue' "
-                + "}}");
-
-    assertEquals(CollectionParams.CollectionAction.COLLECTIONPROP.lowerName, v1Params.get(ACTION));
-    assertEquals("collName", v1Params.get(NAME));
-    assertEquals("somePropertyName", v1Params.get("propertyName"));
-    assertEquals("somePropertyValue", v1Params.get("propertyValue"));
-  }
 }
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
index cc330d9f36b..aeb9c249b9d 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
@@ -703,23 +703,28 @@ http://localhost:8983/solr/admin/collections?action=COLLECTIONPROP&name=techprod
 ====
 [.tab-label]*V2 API*
 
+To create or update a collection property:
 [source,bash]
 ----
-curl -X POST http://localhost:8983/api/collections/techproducts_v2 -H 'Content-Type: application/json' -d '
+curl -X PUT http://localhost:8983/api/collections/techproducts_v2/properties/foo -H 'Content-Type: application/json' -d '
   {
-    "set-collection-property": {
-      "name": "foo",
-      "value": "bar"
-    }
+    "value": "bar"
   }
 '
 ----
+
+To delete an existing collection property:
+
+[source,bash]
+----
+curl -X DELETE http://localhost:8983/api/collections/techproducts_v2/properties/foo 
+----
 ====
 --
 
 === COLLECTIONPROP Parameters
 
-`name`::
+`name` (v1)::
 +
 [%autowidth,frame=none]
 |===
@@ -727,8 +732,9 @@ curl -X POST http://localhost:8983/api/collections/techproducts_v2 -H 'Content-T
 |===
 +
 The name of the collection for which the property would be set.
+Appears in the path of v2 requests.
 
-`propertyName` (v1), `name` (v2)::
+`propertyName` (v1)::
 +
 [%autowidth,frame=none]
 |===
@@ -736,6 +742,7 @@ The name of the collection for which the property would be set.
 |===
 +
 The name of the property.
+Appears in the path of v2 requests.
 
 `propertyValue` (v1), `value` (v2)::
 +
@@ -745,7 +752,7 @@ The name of the property.
 |===
 +
 The value of the property.
-When not provided, the property is deleted.
+When not provided in v1 requests, the property is deleted.
 
 === COLLECTIONPROP Response
 
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetCollectionPropertyPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetCollectionPropertyPayload.java
deleted file mode 100644
index 2719266c975..00000000000
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetCollectionPropertyPayload.java
+++ /dev/null
@@ -1,27 +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.solr.client.solrj.request.beans;
-
-import org.apache.solr.common.annotation.JsonProperty;
-import org.apache.solr.common.util.ReflectMapWriter;
-
-public class SetCollectionPropertyPayload implements ReflectMapWriter {
-  @JsonProperty(required = true)
-  public String name;
-
-  @JsonProperty public String value = null;
-}


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

Posted by ge...@apache.org.
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");