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 2021/11/16 01:45:39 UTC

[solr] branch main updated: SOLR-15536: Rewrite /c//shards v2 APIs using annotations (#218)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 81d3478  SOLR-15536: Rewrite /c/<coll>/shards v2 APIs using annotations (#218)
81d3478 is described below

commit 81d3478fc5eed2f5460ee7401e62875de74638d6
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Mon Nov 15 20:45:34 2021 -0500

    SOLR-15536: Rewrite /c/<coll>/shards v2 APIs using annotations (#218)
---
 .../java/org/apache/solr/core/CoreContainer.java   |   1 +
 .../org/apache/solr/handler/CollectionsAPI.java    |  10 +-
 ...ModifyCollectionAPI.java => AddReplicaAPI.java} |  57 +++---
 ...odifyCollectionAPI.java => CreateShardAPI.java} |  59 +++---
 .../handler/admin/api/ModifyCollectionAPI.java     |  10 +-
 ...ModifyCollectionAPI.java => SplitShardAPI.java} |  57 +++---
 .../org/apache/solr/handler/api/ApiRegistrar.java  |   6 +
 .../org/apache/solr/handler/api/V2ApiUtils.java    |  36 ++++
 .../solr/handler/admin/TestApiFramework.java       |   1 +
 .../solr/handler/admin/TestCollectionAPIs.java     |  13 +-
 .../admin/api/V2CollectionAPIMappingTest.java      |   2 +-
 .../handler/admin/api/V2ShardsAPIMappingTest.java  | 224 +++++++++++++++++++++
 .../client/solrj/request/CollectionApiMapping.java |  33 +--
 .../solrj/request/beans/AddReplicaPayload.java     |  73 +++++++
 .../solrj/request/beans/CreateShardPayload.java    |  56 ++++++
 .../solrj/request/beans/SplitShardPayload.java     |  63 ++++++
 .../solr/common/params/CommonAdminParams.java      |   2 +
 .../collections.collection.shards.Commands.json    | 129 ------------
 .../client/solrj/request/TestV1toV2ApiMapper.java  |  41 ----
 .../apache/solr/common/util/JsonValidatorTest.java |   1 -
 20 files changed, 553 insertions(+), 321 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index 1b91ebc..d4d2030 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -769,6 +769,7 @@ public class CoreContainer {
     collectionsHandler = createHandler(COLLECTIONS_HANDLER_PATH, cfg.getCollectionsHandlerClass(), CollectionsHandler.class);
     final CollectionsAPI collectionsAPI = new CollectionsAPI(collectionsHandler);
     ApiRegistrar.registerCollectionApis(containerHandlers.getApiBag(), collectionsHandler);
+    ApiRegistrar.registerShardApis(containerHandlers.getApiBag(), collectionsHandler);
     containerHandlers.getApiBag().registerObject(collectionsAPI);
     containerHandlers.getApiBag().registerObject(collectionsAPI.collectionsCommands);
     final CollectionBackupsAPI collectionBackupsAPI = new CollectionBackupsAPI(collectionsHandler);
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 a071d0a..707a491 100644
--- a/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
@@ -49,6 +49,7 @@ import static org.apache.solr.common.params.CollectionAdminParams.PROPERTY_PREFI
 import static org.apache.solr.common.params.CollectionAdminParams.ROUTER_PREFIX;
 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;
 
@@ -206,14 +207,5 @@ public class CollectionsAPI {
                 }
             }
         }
-
-        private void flattenMapWithPrefix(Map<String, Object> toFlatten, Map<String, Object> destination,
-                                          String additionalPrefix) {
-            if (toFlatten == null || toFlatten.isEmpty() || destination == null) {
-                return;
-            }
-
-            toFlatten.forEach((k, v) -> destination.put(additionalPrefix + k, v));
-        }
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaAPI.java
similarity index 54%
copy from solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java
copy to solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaAPI.java
index 516fac7..4b3920a 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaAPI.java
@@ -6,7 +6,7 @@
  * (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
+ *      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,
@@ -14,12 +14,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.solr.handler.admin.api;
 
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
 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.ModifyCollectionPayload;
+import org.apache.solr.client.solrj.request.beans.AddReplicaPayload;
 import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.handler.admin.CollectionsHandler;
 
@@ -27,58 +30,46 @@ import java.util.HashMap;
 import java.util.Map;
 
 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.CollectionAdminParams.COLL_CONF;
+import static org.apache.solr.common.params.CollectionAdminParams.*;
 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;
 
 /**
- * V2 API for modifying collections.
+ * V2 API for adding a new replica to an existing shard.
  *
- * The new API (POST /v2/collections/collectionName {'modify-collection': {...}}) is equivalent to the v1
- * /admin/collections?action=MODIFYCOLLECTION command.
+ * This API (POST /v2/collections/collectionName/shards {'add-replica': {...}}) is analogous to the v1
+ * /admin/collections?action=ADDREPLICA command.
  *
- * @see ModifyCollectionPayload
+ * @see AddReplicaPayload
  */
 @EndPoint(
-        path = {"/c/{collection}", "/collections/{collection}"},
+        path = {"/c/{collection}/shards", "/collections/{collection}/shards"},
         method = POST,
         permission = COLL_EDIT_PERM)
-public class ModifyCollectionAPI {
-  private static final String V2_MODIFY_COLLECTION_CMD = "modify";
+public class AddReplicaAPI {
+  private static final String V2_ADD_REPLICA_CMD = "add-replica";
 
   private final CollectionsHandler collectionsHandler;
 
-  public ModifyCollectionAPI(CollectionsHandler collectionsHandler) {
+  public AddReplicaAPI(CollectionsHandler collectionsHandler) {
     this.collectionsHandler = collectionsHandler;
   }
 
-  @Command(name = V2_MODIFY_COLLECTION_CMD)
-  public void modifyCollection(PayloadObj<ModifyCollectionPayload> obj) throws Exception {
-    final ModifyCollectionPayload v2Body = obj.get();
-
+  @Command(name = V2_ADD_REPLICA_CMD)
+  public void addReplica(PayloadObj<AddReplicaPayload> obj) throws Exception {
+    final AddReplicaPayload v2Body = obj.get();
     final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
-    v1Params.put(ACTION, CollectionParams.CollectionAction.MODIFYCOLLECTION.toLower());
+    v1Params.put(ACTION, CollectionParams.CollectionAction.ADDREPLICA.toLower());
     v1Params.put(COLLECTION, obj.getRequest().getPathTemplateValues().get(COLLECTION));
-    if (v2Body.config != null) {
-      v1Params.remove("config");
-      v1Params.put(COLL_CONF, v2Body.config);
+
+    if (MapUtils.isNotEmpty(v2Body.coreProperties)) {
+      flattenMapWithPrefix(v2Body.coreProperties, v1Params, PROPERTY_PREFIX);
     }
-    if (v2Body.properties != null && !v2Body.properties.isEmpty()) {
-      v1Params.remove("properties");
-      flattenMapWithPrefix(v2Body.properties, v1Params, "property.");
+    if (CollectionUtils.isNotEmpty(v2Body.createNodeSet)) {
+      v1Params.replace(CREATE_NODE_SET_PARAM, String.join(",", v2Body.createNodeSet));
     }
-
     collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
   }
-
-  private void flattenMapWithPrefix(Map<String, Object> toFlatten, Map<String, Object> destination,
-                                    String additionalPrefix) {
-    if (toFlatten == null || toFlatten.isEmpty() || destination == null) {
-      return;
-    }
-
-    toFlatten.forEach((k, v) -> destination.put(additionalPrefix + k, v));
-  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShardAPI.java
similarity index 54%
copy from solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java
copy to solr/core/src/java/org/apache/solr/handler/admin/api/CreateShardAPI.java
index 516fac7..2663897 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShardAPI.java
@@ -6,7 +6,7 @@
  * (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
+ *      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,
@@ -14,71 +14,70 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.solr.handler.admin.api;
 
+import org.apache.commons.collections4.MapUtils;
 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.ModifyCollectionPayload;
+import org.apache.solr.client.solrj.request.beans.CreateShardPayload;
 import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.handler.admin.CollectionsHandler;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 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.CollectionAdminParams.COLL_CONF;
+import static org.apache.solr.common.params.CollectionAdminParams.*;
 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;
 
 /**
- * V2 API for modifying collections.
+ * V2 API for creating a new shard in a collection.
  *
- * The new API (POST /v2/collections/collectionName {'modify-collection': {...}}) is equivalent to the v1
- * /admin/collections?action=MODIFYCOLLECTION command.
+ * This API (POST /v2/collections/collectionName/shards {'create': {...}}) is analogous to the v1
+ * /admin/collections?action=CREATESHARD command.
  *
- * @see ModifyCollectionPayload
+ * @see CreateShardAPI
  */
 @EndPoint(
-        path = {"/c/{collection}", "/collections/{collection}"},
+        path = {"/c/{collection}/shards", "/collections/{collection}/shards"},
         method = POST,
         permission = COLL_EDIT_PERM)
-public class ModifyCollectionAPI {
-  private static final String V2_MODIFY_COLLECTION_CMD = "modify";
+public class CreateShardAPI {
+  private static final String V2_CREATE_CMD = "create";
 
   private final CollectionsHandler collectionsHandler;
 
-  public ModifyCollectionAPI(CollectionsHandler collectionsHandler) {
+  public CreateShardAPI(CollectionsHandler collectionsHandler) {
     this.collectionsHandler = collectionsHandler;
   }
 
-  @Command(name = V2_MODIFY_COLLECTION_CMD)
-  public void modifyCollection(PayloadObj<ModifyCollectionPayload> obj) throws Exception {
-    final ModifyCollectionPayload v2Body = obj.get();
-
+  @Command(name = V2_CREATE_CMD)
+  public void createShard(PayloadObj<CreateShardPayload> obj) throws Exception {
+    final CreateShardPayload v2Body = obj.get();
     final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
-    v1Params.put(ACTION, CollectionParams.CollectionAction.MODIFYCOLLECTION.toLower());
+    v1Params.put(ACTION, CollectionParams.CollectionAction.CREATESHARD.toLower());
     v1Params.put(COLLECTION, obj.getRequest().getPathTemplateValues().get(COLLECTION));
-    if (v2Body.config != null) {
-      v1Params.remove("config");
-      v1Params.put(COLL_CONF, v2Body.config);
-    }
-    if (v2Body.properties != null && !v2Body.properties.isEmpty()) {
-      v1Params.remove("properties");
-      flattenMapWithPrefix(v2Body.properties, v1Params, "property.");
+
+    if (v2Body.nodeSet != null) {
+      v1Params.put(CREATE_NODE_SET_PARAM, buildV1CreateNodeSetValue(v2Body.nodeSet));
     }
 
+    if (MapUtils.isNotEmpty(v2Body.coreProperties)) {
+      flattenMapWithPrefix(v2Body.coreProperties, v1Params, PROPERTY_PREFIX);
+    }
     collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
   }
 
-  private void flattenMapWithPrefix(Map<String, Object> toFlatten, Map<String, Object> destination,
-                                    String additionalPrefix) {
-    if (toFlatten == null || toFlatten.isEmpty() || destination == null) {
-      return;
+  private String buildV1CreateNodeSetValue(List<String> nodeSet) {
+    if (nodeSet.size() > 0) {
+      return String.join(",", nodeSet);
     }
-
-    toFlatten.forEach((k, v) -> destination.put(additionalPrefix + k, v));
+    return "EMPTY";
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java
index 516fac7..c3f56b9 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java
@@ -31,6 +31,7 @@ import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
 import static org.apache.solr.common.params.CollectionAdminParams.COLL_CONF;
 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;
 
 /**
@@ -72,13 +73,4 @@ public class ModifyCollectionAPI {
 
     collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
   }
-
-  private void flattenMapWithPrefix(Map<String, Object> toFlatten, Map<String, Object> destination,
-                                    String additionalPrefix) {
-    if (toFlatten == null || toFlatten.isEmpty() || destination == null) {
-      return;
-    }
-
-    toFlatten.forEach((k, v) -> destination.put(additionalPrefix + k, v));
-  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/SplitShardAPI.java
similarity index 56%
copy from solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java
copy to solr/core/src/java/org/apache/solr/handler/admin/api/SplitShardAPI.java
index 516fac7..e6e46ef 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/SplitShardAPI.java
@@ -6,7 +6,7 @@
  * (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
+ *      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,
@@ -14,13 +14,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.solr.handler.admin.api;
 
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
 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.ModifyCollectionPayload;
+import org.apache.solr.client.solrj.request.beans.SplitShardPayload;
 import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.common.params.CommonAdminParams;
 import org.apache.solr.handler.admin.CollectionsHandler;
 
 import java.util.HashMap;
@@ -28,57 +32,46 @@ import java.util.Map;
 
 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.CollectionAdminParams.COLL_CONF;
+import static org.apache.solr.common.params.CollectionAdminParams.PROPERTY_PREFIX;
 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;
 
 /**
- * V2 API for modifying collections.
+ * V2 API for splitting an existing shard up into multiple pieces.
  *
- * The new API (POST /v2/collections/collectionName {'modify-collection': {...}}) is equivalent to the v1
- * /admin/collections?action=MODIFYCOLLECTION command.
+ * This API (POST /v2/collections/collectionName/shards {'split': {...}}) is analogous to the v1
+ * /admin/collections?action=SPLITSHARD command.
  *
- * @see ModifyCollectionPayload
+ * @see SplitShardPayload
  */
 @EndPoint(
-        path = {"/c/{collection}", "/collections/{collection}"},
+        path = {"/c/{collection}/shards", "/collections/{collection}/shards"},
         method = POST,
         permission = COLL_EDIT_PERM)
-public class ModifyCollectionAPI {
-  private static final String V2_MODIFY_COLLECTION_CMD = "modify";
+public class SplitShardAPI {
+  private static final String V2_SPLIT_CMD = "split";
 
   private final CollectionsHandler collectionsHandler;
 
-  public ModifyCollectionAPI(CollectionsHandler collectionsHandler) {
+  public SplitShardAPI(CollectionsHandler collectionsHandler) {
     this.collectionsHandler = collectionsHandler;
   }
 
-  @Command(name = V2_MODIFY_COLLECTION_CMD)
-  public void modifyCollection(PayloadObj<ModifyCollectionPayload> obj) throws Exception {
-    final ModifyCollectionPayload v2Body = obj.get();
-
+  @Command(name = V2_SPLIT_CMD)
+  public void splitShard(PayloadObj<SplitShardPayload> obj) throws Exception {
+    final SplitShardPayload v2Body = obj.get();
     final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
-    v1Params.put(ACTION, CollectionParams.CollectionAction.MODIFYCOLLECTION.toLower());
+    v1Params.put(ACTION, CollectionParams.CollectionAction.SPLITSHARD.toLower());
     v1Params.put(COLLECTION, obj.getRequest().getPathTemplateValues().get(COLLECTION));
-    if (v2Body.config != null) {
-      v1Params.remove("config");
-      v1Params.put(COLL_CONF, v2Body.config);
+
+    if (StringUtils.isNotEmpty(v2Body.splitKey)) {
+      v1Params.put(CommonAdminParams.SPLIT_KEY, v2Body.splitKey);
     }
-    if (v2Body.properties != null && !v2Body.properties.isEmpty()) {
-      v1Params.remove("properties");
-      flattenMapWithPrefix(v2Body.properties, v1Params, "property.");
+    if (MapUtils.isNotEmpty(v2Body.coreProperties)) {
+      flattenMapWithPrefix(v2Body.coreProperties, v1Params, PROPERTY_PREFIX);
     }
-
     collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
   }
-
-  private void flattenMapWithPrefix(Map<String, Object> toFlatten, Map<String, Object> destination,
-                                    String additionalPrefix) {
-    if (toFlatten == null || toFlatten.isEmpty() || destination == null) {
-      return;
-    }
-
-    toFlatten.forEach((k, v) -> destination.put(additionalPrefix + k, v));
-  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java b/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java
index 33d9a25..e699bd8 100644
--- a/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java
+++ b/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java
@@ -43,4 +43,10 @@ public class ApiRegistrar {
     apiBag.registerObject(new SetCollectionPropertyAPI(collectionsHandler));
     apiBag.registerObject(new CollectionStatusAPI(collectionsHandler));
   }
+
+  public static void registerShardApis(ApiBag apiBag, CollectionsHandler collectionsHandler) {
+    apiBag.registerObject(new SplitShardAPI(collectionsHandler));
+    apiBag.registerObject(new CreateShardAPI(collectionsHandler));
+    apiBag.registerObject(new AddReplicaAPI(collectionsHandler));
+  }
 }
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
new file mode 100644
index 0000000..9827c59
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
@@ -0,0 +1,36 @@
+/*
+ * 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.api;
+
+import java.util.Map;
+
+/**
+ * Utilities helpful for common V2 API declaration tasks.
+ */
+public class V2ApiUtils {
+  private V2ApiUtils() { /* Private ctor prevents instantiation */ }
+
+  public static void flattenMapWithPrefix(Map<String, Object> toFlatten, Map<String, Object> destination,
+                                    String additionalPrefix) {
+    if (toFlatten == null || toFlatten.isEmpty() || destination == null) {
+      return;
+    }
+
+    toFlatten.forEach((k, v) -> destination.put(additionalPrefix + k, v));
+  }
+}
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 a192f1b..c9d355c 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
@@ -83,6 +83,7 @@ public class TestApiFramework extends SolrTestCaseJ4 {
     containerHandlers.put(COLLECTIONS_HANDLER_PATH, collectionsHandler);
     containerHandlers.getApiBag().registerObject(new CollectionsAPI(collectionsHandler));
     ApiRegistrar.registerCollectionApis(containerHandlers.getApiBag(), collectionsHandler);
+    ApiRegistrar.registerShardApis(containerHandlers.getApiBag(), collectionsHandler);
     containerHandlers.put(CORES_HANDLER_PATH, new CoreAdminHandler(mockCC));
     containerHandlers.put(CONFIGSETS_HANDLER_PATH, new ConfigSetsHandler(mockCC));
     out.put("getRequestHandlers", containerHandlers);
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 fa6e96e..d6601783 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
@@ -89,6 +89,7 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
       apiBag.registerObject(new CollectionsAPI(collectionsHandler));
       apiBag.registerObject(collectionsAPI.collectionsCommands);
       ApiRegistrar.registerCollectionApis(apiBag, collectionsHandler);
+      ApiRegistrar.registerShardApis(apiBag, collectionsHandler);
       Collection<Api> apis = collectionsHandler.getApis();
       for (Api api : apis) apiBag.register(api, Collections.emptyMap());
 
@@ -161,11 +162,13 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
         "{add-replica:{shard: shard1, node: 'localhost_8978' , type:'PULL' }}", null,
         "{collection: collName , shard : shard1, node :'localhost_8978', operation : addreplica, type: PULL}"
     );
-    
-    assertErrorContains(apiBag, "/collections/collName/shards", POST,
-        "{add-replica:{shard: shard1, node: 'localhost_8978' , type:'foo' }}", null,
-        "Value of enum must be one of"
-    );
+
+    // TODO annotation-based v2 APIs still miss enum support to validate the 'type' parameter as this test requires.
+    // Uncomment this test after fixing SOLR-15796
+//    assertErrorContains(apiBag, "/collections/collName/shards", POST,
+//        "{add-replica:{shard: shard1, node: 'localhost_8978' , type:'foo' }}", null,
+//        "Value of enum must be one of"
+//    );
 
     compareOutput(apiBag, "/collections/collName", POST,
         "{add-replica-property : {name:propA , value: VALA, shard: shard1, replica:replica1}}", null,
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 c2a13f7..0f073e9 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
@@ -53,7 +53,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
 /**
- * Unit tests for the V2 APIs found in {@link org.apache.solr.handler.admin.api}.
+ * Unit tests for the V2 APIs found in {@link org.apache.solr.handler.admin.api} that use the /c/{collection} path.
  *
  * This test bears many similarities to {@link TestCollectionAPIs} which appears to test the mappings indirectly by
  * checking message sent to the ZK overseer (which is similar, but not identical to the v1 param list).  If there's no
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
new file mode 100644
index 0000000..769fc9e
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
@@ -0,0 +1,224 @@
+/*
+ * 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 com.google.common.collect.Maps;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
+import org.apache.solr.common.params.CollectionAdminParams;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.common.util.ContentStreamBase;
+import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.handler.admin.TestCollectionAPIs;
+import org.apache.solr.handler.api.ApiRegistrar;
+import org.apache.solr.request.LocalSolrQueryRequest;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.solr.common.cloud.ZkStateReader.*;
+import static org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CommonAdminParams.*;
+import static org.apache.solr.common.params.CommonParams.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Unit tests for the V2 APIs found in {@link org.apache.solr.handler.admin.api} that use the /c/{collection}/shards path.
+ *
+ * This test bears many similarities to {@link TestCollectionAPIs} which appears to test the mappings indirectly by
+ * checking message sent to the ZK overseer (which is similar, but not identical to the v1 param list).  If there's no
+ * particular benefit to testing the mappings in this way (there very well may be), then we should combine these two
+ * test classes at some point in the future using the simpler approach here.
+ *
+ * Note that the V2 requests made by these tests are not necessarily semantically valid.  They shouldn't be taken as
+ * examples. In several instances, mutually exclusive JSON parameters are provided.  This is done to exercise conversion
+ * of all parameters, even if particular combinations are never expected in the same request.
+ */
+public class V2ShardsAPIMappingTest extends SolrTestCaseJ4 {
+  private ApiBag apiBag;
+
+  private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
+  private CollectionsHandler mockCollectionsHandler;
+
+  @BeforeClass
+  public static void ensureWorkingMockito() {
+    assumeWorkingMockito();
+  }
+
+  @Before
+  public void setupApiBag() throws Exception {
+    mockCollectionsHandler = mock(CollectionsHandler.class);
+    queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
+
+    apiBag = new ApiBag(false);
+    ApiRegistrar.registerShardApis(apiBag, mockCollectionsHandler);
+  }
+
+  @Test
+  public void testSplitShardAllProperties() throws Exception {
+    final SolrParams v1Params = captureConvertedV1Params("/collections/collName/shards", "POST",
+            "{ 'split': {" +
+                    "'shard': 'shard1', " +
+                    "'ranges': 'someRangeValues', " +
+                    "'splitKey': 'someSplitKey', " +
+                    "'numSubShards': 123, " +
+                    "'splitFuzz': 'some_fuzz_value', " +
+                    "'timing': true, " +
+                    "'splitByPrefix': true, " +
+                    "'followAliases': true, " +
+                    "'splitMethod': 'rewrite', " +
+                    "'async': 'some_async_id', " +
+                    "'waitForFinalState': true, " +
+                    "'coreProperties': {" +
+                    "    'foo': 'foo1', " +
+                    "    'bar': 'bar1', " +
+                    "}}}");
+
+    assertEquals(CollectionParams.CollectionAction.SPLITSHARD.lowerName, v1Params.get(ACTION));
+    assertEquals("collName", v1Params.get(CollectionAdminParams.COLLECTION));
+    assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
+    assertEquals("someRangeValues", v1Params.get(CoreAdminParams.RANGES));
+    assertEquals("someSplitKey", v1Params.get(SPLIT_KEY));
+    assertEquals(123, v1Params.getPrimitiveInt(NUM_SUB_SHARDS));
+    assertEquals("some_fuzz_value", v1Params.get(SPLIT_FUZZ));
+    assertEquals(true, v1Params.getPrimitiveBool(TIMING));
+    assertEquals(true, v1Params.getPrimitiveBool(SPLIT_BY_PREFIX));
+    assertEquals(true, v1Params.getPrimitiveBool(FOLLOW_ALIASES));
+    assertEquals("rewrite", v1Params.get(SPLIT_METHOD));
+    assertEquals("some_async_id", v1Params.get(ASYNC));
+    assertEquals(true, v1Params.getPrimitiveBool(WAIT_FOR_FINAL_STATE));
+    assertEquals("foo1", v1Params.get("property.foo"));
+    assertEquals("bar1", v1Params.get("property.bar"));
+  }
+
+  @Test
+  public void testCreateShardAllProperties() throws Exception {
+    final SolrParams v1Params = captureConvertedV1Params("/collections/collName/shards", "POST",
+            "{ 'create': {" +
+                    "'shard': 'shard1', " +
+                    "'nodeSet': ['foo', 'bar', 'baz'], " +
+                    "'followAliases': true, " +
+                    "'async': 'some_async_id', " +
+                    "'waitForFinalState': true, " +
+                    "'replicationFactor': 123, " +
+                    "'nrtReplicas': 456, " +
+                    "'tlogReplicas': 789, " +
+                    "'pullReplicas': 101, " +
+                    "'coreProperties': {" +
+                    "    'foo': 'foo1', " +
+                    "    'bar': 'bar1', " +
+                    "}}}");
+
+    assertEquals(CollectionParams.CollectionAction.CREATESHARD.lowerName, v1Params.get(ACTION));
+    assertEquals("collName", v1Params.get(CollectionAdminParams.COLLECTION));
+    assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
+    assertEquals("foo,bar,baz", v1Params.get(CREATE_NODE_SET_PARAM));
+    assertEquals(true, v1Params.getPrimitiveBool(FOLLOW_ALIASES));
+    assertEquals("some_async_id", v1Params.get(ASYNC));
+    assertEquals(true, v1Params.getPrimitiveBool(WAIT_FOR_FINAL_STATE));
+    assertEquals(123, v1Params.getPrimitiveInt(REPLICATION_FACTOR));
+    assertEquals(456, v1Params.getPrimitiveInt(NRT_REPLICAS));
+    assertEquals(789, v1Params.getPrimitiveInt(TLOG_REPLICAS));
+    assertEquals(101, v1Params.getPrimitiveInt(PULL_REPLICAS));
+    assertEquals("foo1", v1Params.get("property.foo"));
+    assertEquals("bar1", v1Params.get("property.bar"));
+  }
+
+  @Test
+  public void testAddReplicaAllProperties() throws Exception {
+    final SolrParams v1Params = captureConvertedV1Params("/collections/collName/shards", "POST",
+            "{ 'add-replica': {" +
+                    "'shard': 'shard1', " +
+                    "'_route_': 'someRouteValue', " +
+                    "'node': 'someNodeValue', " +
+                    "'name': 'someName', " +
+                    "'instanceDir': 'dir1', " +
+                    "'dataDir': 'dir2', " +
+                    "'ulogDir': 'dir3', " +
+                    "'createNodeSet': ['foo', 'bar', 'baz'], " +
+                    "'followAliases': true, " +
+                    "'async': 'some_async_id', " +
+                    "'waitForFinalState': true, " +
+                    "'skipNodeAssignment': true, " +
+                    "'type': 'tlog', " +
+                    "'coreProperties': {" +
+                    "    'foo': 'foo1', " +
+                    "    'bar': 'bar1', " +
+                    "}}}");
+
+    assertEquals(CollectionParams.CollectionAction.ADDREPLICA.lowerName, v1Params.get(ACTION));
+    assertEquals("collName", v1Params.get(CollectionAdminParams.COLLECTION));
+    assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
+    assertEquals("someRouteValue", v1Params.get("_route_"));
+    assertEquals("someNodeValue", v1Params.get("node"));
+    assertEquals("foo,bar,baz", v1Params.get(CREATE_NODE_SET_PARAM));
+    assertEquals("someName", v1Params.get(NAME));
+    assertEquals("dir1", v1Params.get("instanceDir"));
+    assertEquals("dir2", v1Params.get("dataDir"));
+    assertEquals("dir3", v1Params.get("ulogDir"));
+    assertEquals(true, v1Params.getPrimitiveBool(FOLLOW_ALIASES));
+    assertEquals("some_async_id", v1Params.get(ASYNC));
+    assertEquals(true, v1Params.getPrimitiveBool(WAIT_FOR_FINAL_STATE));
+    assertEquals(true, v1Params.getPrimitiveBool("skipNodeAssignment"));
+    assertEquals("tlog", v1Params.get("type"));
+    assertEquals("foo1", v1Params.get("property.foo"));
+    assertEquals("bar1", v1Params.get("property.bar"));
+  }
+
+  private SolrParams captureConvertedV1Params(String path, String method, String v2RequestBody) throws Exception {
+    final HashMap<String, String> parts = new HashMap<>();
+    final Api api = apiBag.lookup(path, method, parts);
+    final SolrQueryResponse rsp = new SolrQueryResponse();
+    final LocalSolrQueryRequest req = new LocalSolrQueryRequest(null, Maps.newHashMap()) {
+      @Override
+      public List<CommandOperation> getCommands(boolean validateInput) {
+        if (v2RequestBody == null) return Collections.emptyList();
+        return ApiBag.getCommandOperations(new ContentStreamBase.StringStream(v2RequestBody), api.getCommandSchema(), true);
+      }
+
+      @Override
+      public Map<String, String> getPathTemplateValues() {
+        return parts;
+      }
+
+      @Override
+      public String getHttpMethod() {
+        return method;
+      }
+    };
+
+    api.call(req, rsp);
+    verify(mockCollectionsHandler).handleRequestBody(queryRequestCaptor.capture(), any());
+    return queryRequestCaptor.getValue().getParams();
+  }
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java
index 7fe8bed..bd9767a 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java
@@ -19,7 +19,6 @@ package org.apache.solr.client.solrj.request;
 
 
 import org.apache.solr.client.solrj.SolrRequest;
-import org.apache.solr.client.solrj.request.beans.V2ApiConstants;
 import org.apache.solr.common.params.CollectionParams.CollectionAction;
 import org.apache.solr.common.util.CommandOperation;
 import org.apache.solr.common.util.Pair;
@@ -45,35 +44,8 @@ import static org.apache.solr.common.params.CollectionParams.CollectionAction.*;
 public class CollectionApiMapping {
 
   public enum Meta implements CommandMeta {
-    CREATE_SHARD(PER_COLLECTION_SHARDS_COMMANDS,
-        POST,
-        CREATESHARD,
-        "create",
-        Map.of("createNodeSet", V2ApiConstants.NODE_SET),
-        Map.of("property.", "coreProperties.")) {
-      @Override
-      public String getParamSubstitute(String param) {
-        return super.getParamSubstitute(param);
-      }
-    },
-    SPLIT_SHARD(PER_COLLECTION_SHARDS_COMMANDS,
-        POST,
-        SPLITSHARD,
-        "split",
-        Map.of("split.key", "splitKey"),
-        Map.of("property.", "coreProperties.")),
-    DELETE_SHARD(PER_COLLECTION_PER_SHARD_DELETE,
-        DELETE, DELETESHARD),
-
-    CREATE_REPLICA(PER_COLLECTION_SHARDS_COMMANDS,
-        POST,
-        ADDREPLICA,
-        "add-replica",
-        null,
-        Map.of("property.", "coreProperties.")),
-
-    DELETE_REPLICA(PER_COLLECTION_PER_SHARD_PER_REPLICA_DELETE,
-        DELETE, DELETEREPLICA),
+    DELETE_SHARD(PER_COLLECTION_PER_SHARD_DELETE, DELETE, DELETESHARD),
+    DELETE_REPLICA(PER_COLLECTION_PER_SHARD_PER_REPLICA_DELETE, DELETE, DELETEREPLICA),
     SYNC_SHARD(PER_COLLECTION_PER_SHARD_COMMANDS,
         POST,
         CollectionAction.SYNCSHARD,
@@ -198,7 +170,6 @@ public class CollectionApiMapping {
   }
 
   public enum EndPoint implements V2EndPoint {
-    PER_COLLECTION_SHARDS_COMMANDS("collections.collection.shards.Commands"),
     PER_COLLECTION_PER_SHARD_COMMANDS("collections.collection.shards.shard.Commands"),
     PER_COLLECTION_PER_SHARD_DELETE("collections.collection.shards.shard.delete"),
     PER_COLLECTION_PER_SHARD_PER_REPLICA_DELETE("collections.collection.shards.shard.replica.delete");
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/AddReplicaPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/AddReplicaPayload.java
new file mode 100644
index 0000000..f10fbf3
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/AddReplicaPayload.java
@@ -0,0 +1,73 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Map;
+
+public class AddReplicaPayload implements ReflectMapWriter {
+  @JsonProperty
+  public String shard;
+
+  @JsonProperty
+  public String _route_;
+
+  // TODO Remove in favor of a createNodeSet/nodeSet param with size=1 (see SOLR-15542)
+  @JsonProperty
+  public String node;
+
+  // TODO Rename to 'nodeSet' to match the name used by create-shard and other APIs (see SOLR-15542)
+  @JsonProperty
+  public List<String> createNodeSet;
+
+  @JsonProperty
+  public String name;
+
+  @JsonProperty
+  public String instanceDir;
+
+  @JsonProperty
+  public String dataDir;
+
+  @JsonProperty
+  public String ulogDir;
+
+  @JsonProperty
+  public Map<String, Object> coreProperties;
+
+  @JsonProperty
+  public String async;
+
+  @JsonProperty
+  public Boolean waitForFinalState;
+
+  @JsonProperty
+  public Boolean followAliases;
+
+  @JsonProperty
+  public Boolean skipNodeAssignment;
+
+  // TODO Make this an enum - see SOLR-15796
+  @JsonProperty
+  public String type;
+}
+
+
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateShardPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateShardPayload.java
new file mode 100644
index 0000000..8fe7103
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateShardPayload.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Map;
+
+public class CreateShardPayload implements ReflectMapWriter {
+  @JsonProperty(required = true)
+  public String shard;
+
+  @JsonProperty
+  public List<String> nodeSet;
+
+  @JsonProperty
+  public Map<String, Object> coreProperties;
+
+  @JsonProperty
+  public Boolean followAliases;
+
+  @JsonProperty
+  public String async;
+
+  @JsonProperty
+  public Boolean waitForFinalState;
+
+  @JsonProperty
+  public Integer replicationFactor;
+
+  @JsonProperty
+  public Integer nrtReplicas;
+
+  @JsonProperty
+  public Integer tlogReplicas;
+
+  @JsonProperty
+  public Integer pullReplicas;
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SplitShardPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SplitShardPayload.java
new file mode 100644
index 0000000..e2b9daf
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SplitShardPayload.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+import java.util.Map;
+
+public class SplitShardPayload implements ReflectMapWriter {
+  @JsonProperty
+  public String shard;
+
+  @JsonProperty
+  public String ranges;
+
+  @JsonProperty
+  public String splitKey;
+
+  @JsonProperty
+  public Integer numSubShards;
+
+  @JsonProperty
+  public String splitFuzz;
+
+  @JsonProperty
+  public Boolean timing;
+
+  @JsonProperty
+  public Boolean splitByPrefix;
+
+  @JsonProperty
+  public Boolean followAliases;
+
+  // TODO Should/can this be an enum?  Does the annotation framework have support for enums like apispec files did?
+  @JsonProperty
+  public String splitMethod;
+
+
+  @JsonProperty
+  public Map<String, Object> coreProperties;
+
+  @JsonProperty
+  public String async;
+
+  @JsonProperty
+  public Boolean waitForFinalState;
+}
diff --git a/solr/solrj/src/java/org/apache/solr/common/params/CommonAdminParams.java b/solr/solrj/src/java/org/apache/solr/common/params/CommonAdminParams.java
index 148746b..9177564 100644
--- a/solr/solrj/src/java/org/apache/solr/common/params/CommonAdminParams.java
+++ b/solr/solrj/src/java/org/apache/solr/common/params/CommonAdminParams.java
@@ -27,6 +27,8 @@ public interface CommonAdminParams
   String IN_PLACE_MOVE = "inPlaceMove";
   /** Method to use for shard splitting. */
   String SPLIT_METHOD = "splitMethod";
+  /** Key to use during shard splitting */
+  String SPLIT_KEY = "split.key";
   /** Check distribution of documents to prefixes in shard to determine how to split */
   String SPLIT_BY_PREFIX = "splitByPrefix";
   /** Number of sub-shards to create. **/
diff --git a/solr/solrj/src/resources/apispec/collections.collection.shards.Commands.json b/solr/solrj/src/resources/apispec/collections.collection.shards.Commands.json
deleted file mode 100644
index 0d6eb52..0000000
--- a/solr/solrj/src/resources/apispec/collections.collection.shards.Commands.json
+++ /dev/null
@@ -1,129 +0,0 @@
-{
-  "documentation": "https://lucene.apache.org/solr/guide/shard-management.html",
-  "description": "Allows you to create a shard, split an existing shard or add a new replica.",
-  "methods": [
-    "POST"
-  ],
-  "url": {
-    "paths": [
-      "/collections/{collection}/shards",
-      "/c/{collection}/shards"
-    ]
-  },
-  "commands": {
-    "split": {
-      "type" : "object",
-      "documentation":"https://lucene.apache.org/solr/guide/shard-management.html#splitshard",
-      "description": "Splits an existing shard into two or more new shards. During this action, the existing shard will continue to contain the original data, but new data will be routed to the new shards once the split is complete. New shards will have as many replicas as the existing shards. A soft commit will be done automatically. An explicit commit request is not required because the index is automatically saved to disk during the split operation. New shards will use the original sh [...]
-      "properties": {
-        "shard":{
-          "type":"string",
-          "description":"The name of the shard to be split."
-        },
-        "ranges" : {
-          "description" : "A comma-separated list of hexadecimal hash ranges that will be used to split the shard into new shards containing each defined range, e.g. ranges=0-1f4,1f5-3e8,3e9-5dc. This is the only option that allows splitting a single shard into more than 2 additional shards. If neither this parameter nor splitKey are defined, the shard will be split into two equal new shards.",
-          "type":"string"
-        },
-        "splitKey":{
-          "description" : "A route key to use for splitting the index. If this is defined, the shard parameter is not required because the route key will identify the correct shard. A route key that spans more than a single shard is not supported. If neither this parameter nor ranges are defined, the shard will be split into two equal new shards.",
-          "type":"string"
-        },
-        "coreProperties":{
-          "type":"object",
-          "documentation": "https://solr.apache.org/guide/core-discovery.html",
-          "description": "Allows adding core.properties for the collection. Some examples of core properties you may want to modify include the config set, the node name, the data directory, among others.",
-          "additionalProperties":true
-        },
-        "async": {
-          "type": "string",
-          "description": "Defines a request ID that can be used to track this action after it's submitted. The action will be processed asynchronously when this is defined. This command can be long-running, so running it asynchronously is recommended."
-        },
-        "waitForFinalState": {
-          "type": "boolean",
-          "description": "If true then request will complete only when all affected replicas become active.",
-          "default": false
-        }
-      }
-    },
-    "create": {
-      "type":"object",
-      "properties": {
-        "nodeSet": {
-          "description": "Defines nodes to spread the new collection across. If not provided, the collection will be spread across all live Solr nodes. The names to use are the 'node_name', which can be found by a request to the cluster/nodes endpoint.",
-          "type": "array",
-          "items": {
-            "type": "string"
-          }
-        },
-        "shard": {
-          "description": "The name of the shard to be created.",
-          "type": "string"
-        },
-        "coreProperties": {
-          "type": "object",
-          "documentation": "https://solr.apache.org/solr/core-discovery.html",
-          "description": "Allows adding core.properties for the collection. Some examples of core properties you may want to modify include the config set, the node name, the data directory, among others.",
-          "additionalProperties": true
-        },
-        "async": {
-          "type": "string",
-          "description": "Defines a request ID that can be used to track this action after it's submitted. The action will be processed asynchronously when this is defined."
-        },
-        "waitForFinalState": {
-          "type": "boolean",
-          "description": "If true then request will complete only when all affected replicas become active.",
-          "default": false
-        }
-      },
-      "required":["shard"]
-    },
-    "add-replica": {
-      "documentation":"https://lucene.apache.org/solr/guide/replica-management.html#addreplica",
-      "description": "",
-      "type" : "object",
-      "properties": {
-        "shard": {
-          "type": "string",
-          "description": "The name of the shard in which this replica should be created. If this parameter is not specified, then '_route_' must be defined."
-        },
-        "_route_": {
-          "type": "string",
-          "description": "If the exact shard name is not known, users may pass the _route_ value and the system would identify the name of the shard. Ignored if the shard param is also specified. If the 'shard' parameter is also defined, this parameter will be ignored."
-        },
-        "node": {
-          "type": "string",
-          "description": "The name of the node where the replica should be created."
-        },
-        "instanceDir": {
-          "type": "string",
-          "description": "An optional custom instanceDir for this replica."
-        },
-        "dataDir": {
-          "type": "string",
-          "description": "An optional custom directory used to store index data for this replica."
-        },
-        "coreProperties": {
-          "type": "object",
-          "documentation": "https://solr.apache.org/guide/core-discovery.html",
-          "description": "Allows adding core.properties for the collection. Some examples of core properties you may want to modify include the config set and the node name, among others.",
-          "additionalProperties": true
-        },
-        "async": {
-          "type": "string",
-          "description": "Defines a request ID that can be used to track this action after it's submitted. The action will be processed asynchronously when this is defined."
-        },
-        "type": {
-          "type": "string",
-          "enum":["NRT", "TLOG", "PULL"],
-          "description": "The type of replica to add. NRT (default), TLOG or PULL"
-        },
-        "waitForFinalState": {
-          "type": "boolean",
-          "description": "If true then request will complete only when all affected replicas become active.",
-          "default": false
-        }
-      },
-      "required":["shard"]
-    }
-  }
-}
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/TestV1toV2ApiMapper.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/TestV1toV2ApiMapper.java
deleted file mode 100644
index 25d8264..0000000
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/request/TestV1toV2ApiMapper.java
+++ /dev/null
@@ -1,41 +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;
-
-import org.apache.solr.SolrTestCase;
-import org.apache.solr.client.solrj.impl.BinaryRequestWriter;
-import org.apache.solr.common.util.ContentStreamBase;
-import org.apache.solr.common.util.Utils;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.Map;
-
-public class TestV1toV2ApiMapper extends SolrTestCase {
-
-  @Test
-  // commented out on: 24-Dec-2018   @BadApple(bugUrl="https://issues.apache.org/jira/browse/SOLR-12028") // added 20-Sep-2018
-  public void testAddReplica() throws IOException {
-    CollectionAdminRequest.AddReplica addReplica = CollectionAdminRequest.addReplicaToShard("mycoll", "shard1");
-    V2Request v2r = V1toV2ApiMapper.convert(addReplica).build();
-    Map<?,?> m = (Map<?,?>) Utils.fromJSON(ContentStreamBase.create(new BinaryRequestWriter(), v2r).getStream());
-    assertEquals("/c/mycoll/shards", v2r.getPath());
-    assertEquals("shard1", Utils.getObjectByPath(m,true,"/add-replica/shard"));
-    assertEquals("NRT", Utils.getObjectByPath(m,true,"/add-replica/type"));
-  }
-}
diff --git a/solr/solrj/src/test/org/apache/solr/common/util/JsonValidatorTest.java b/solr/solrj/src/test/org/apache/solr/common/util/JsonValidatorTest.java
index 3ab121e..184ea47 100644
--- a/solr/solrj/src/test/org/apache/solr/common/util/JsonValidatorTest.java
+++ b/solr/solrj/src/test/org/apache/solr/common/util/JsonValidatorTest.java
@@ -28,7 +28,6 @@ import static org.apache.solr.common.util.ValidatingJsonMap.NOT_NULL;
 public class JsonValidatorTest extends SolrTestCaseJ4  {
 
   public void testSchema() {
-    checkSchema("collections.collection.shards.Commands");
     checkSchema("collections.collection.shards.shard.Commands");
     checkSchema("cores.Commands");
     checkSchema("cores.core.Commands");