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/06/11 14:11:49 UTC

[solr] branch main updated: SOLR-15351: Convert /v2/c/ APIs to POJO impl (#81)

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 39cc2b8  SOLR-15351: Convert /v2/c/<coll> APIs to POJO impl (#81)
39cc2b8 is described below

commit 39cc2b8f66f893184ef4a369176912112b1b4e70
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Fri Jun 11 14:11:42 2021 +0000

    SOLR-15351: Convert /v2/c/<coll> APIs to POJO impl (#81)
---
 .../src/java/org/apache/solr/api/AnnotatedApi.java |   5 +-
 solr/core/src/java/org/apache/solr/api/ApiBag.java |  73 +++++-
 .../java/org/apache/solr/core/CoreContainer.java   |  68 +++---
 .../org/apache/solr/handler/CollectionsAPI.java    |  16 +-
 .../handler/admin/api/AddReplicaPropertyAPI.java   |  67 +++++
 .../handler/admin/api/BalanceShardUniqueAPI.java   |  65 +++++
 .../handler/admin/api/DeleteCollectionAPI.java     |  54 ++++
 .../admin/api/DeleteReplicaPropertyAPI.java        |  66 +++++
 .../solr/handler/admin/api/MigrateDocsAPI.java     |  78 ++++++
 .../handler/admin/api/ModifyCollectionAPI.java     |  84 +++++++
 .../solr/handler/admin/api/MoveReplicaAPI.java     |  65 +++++
 .../handler/admin/api/RebalanceLeadersAPI.java     |  65 +++++
 .../handler/admin/api/ReloadCollectionAPI.java     |  66 +++++
 .../admin/api/SetCollectionPropertyAPI.java        |  72 ++++++
 .../solr/handler/admin/api/package-info.java       |  21 ++
 .../org/apache/solr/handler/api/ApiRegistrar.java  |  54 ++++
 .../org/apache/solr/handler/api/package-info.java  |  21 ++
 .../solr/handler/admin/TestApiFramework.java       |   2 +
 .../solr/handler/admin/TestCollectionAPIs.java     |   2 +
 .../admin/api/V2CollectionAPIMappingTest.java      | 271 +++++++++++++++++++++
 .../solr/handler/admin/api/package-info.java       |  21 ++
 .../client/solrj/request/CollectionApiMapping.java |  86 +------
 .../request/beans/AddReplicaPropertyPayload.java   |  37 +++
 .../request/beans/BalanceShardUniquePayload.java   |  31 +++
 .../beans/DeleteReplicaPropertyPayload.java        |  31 +++
 .../solrj/request/beans/MigrateDocsPayload.java    |  37 +++
 .../request/beans/ModifyCollectionPayload.java     |  39 +++
 .../solrj/request/beans/MoveReplicaPayload.java    |  48 ++++
 .../request/beans/RebalanceLeadersPayload.java     |  28 +++
 .../request/beans/ReloadCollectionPayload.java     |  25 ++
 .../beans/SetCollectionPropertyPayload.java        |  28 +++
 .../java/org/apache/solr/common/util/PathTrie.java |  18 +-
 .../apispec/collections.collection.Commands.json   | 193 ---------------
 .../collections.collection.Commands.reload.json    |  11 -
 .../client/solrj/request/TestV1toV2ApiMapper.java  |  11 -
 .../apache/solr/common/util/JsonValidatorTest.java |   1 -
 36 files changed, 1511 insertions(+), 349 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java b/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
index f268733..2aa65fe 100644
--- a/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
+++ b/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
@@ -90,6 +90,8 @@ public class AnnotatedApi extends Api implements PermissionNameProvider , Closea
     return endPoint;
   }
 
+  public Map<String, Cmd> getCommands() { return commands; }
+
   public static List<Api> getApis(Object obj) {
     return getApis(obj.getClass(), obj, true);
   }
@@ -145,8 +147,7 @@ public class AnnotatedApi extends Api implements PermissionNameProvider , Closea
     }
   }
 
-
-  private AnnotatedApi(SpecProvider specProvider, EndPoint endPoint, Map<String, Cmd> commands, Api fallback) {
+  protected AnnotatedApi(SpecProvider specProvider, EndPoint endPoint, Map<String, Cmd> commands, Api fallback) {
     super(specProvider);
     this.endPoint = endPoint;
     this.fallback = fallback;
diff --git a/solr/core/src/java/org/apache/solr/api/ApiBag.java b/solr/core/src/java/org/apache/solr/api/ApiBag.java
index 25ff09f..8c8272b 100644
--- a/solr/core/src/java/org/apache/solr/api/ApiBag.java
+++ b/solr/core/src/java/org/apache/solr/api/ApiBag.java
@@ -20,6 +20,7 @@ package org.apache.solr.api;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -31,6 +32,7 @@ import java.util.stream.Collectors;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SpecProvider;
@@ -94,6 +96,75 @@ public class ApiBag {
     }
   }
 
+  /**
+   * PathTrie extension that combines the commands in the API being registered with any that have already been registered.
+   *
+   * This is only possible currently for AnnotatedApis.  All other Api implementations will resort to the default
+   * "overwriting" behavior of PathTrie
+   */
+  class CommandAggregatingPathTrie extends PathTrie<Api> {
+
+    public CommandAggregatingPathTrie(Set<String> reserved) {
+      super(reserved);
+    }
+
+    @Override
+    protected void attachValueToNode(PathTrie<Api>.Node node, Api o) {
+      if (node.getObject() == null) {
+        super.attachValueToNode(node, o);
+        return;
+      }
+
+      // If 'o' and 'node.obj' aren't both AnnotatedApi's then we can't aggregate the commands, so fallback to the
+      // default behavior
+      if ((!(o instanceof AnnotatedApi)) || (!(node.getObject() instanceof AnnotatedApi))) {
+        super.attachValueToNode(node, o);
+        return;
+      }
+
+      final AnnotatedApi beingRegistered = (AnnotatedApi) o;
+      final AnnotatedApi alreadyRegistered = (AnnotatedApi) node.getObject();
+      if (alreadyRegistered instanceof CommandAggregatingAnnotatedApi) {
+        final CommandAggregatingAnnotatedApi alreadyRegisteredAsCollapsing = (CommandAggregatingAnnotatedApi) alreadyRegistered;
+        alreadyRegisteredAsCollapsing.combineWith(beingRegistered);
+      } else {
+        final CommandAggregatingAnnotatedApi wrapperApi = new CommandAggregatingAnnotatedApi(alreadyRegistered);
+        wrapperApi.combineWith(beingRegistered);
+        node.setObject(wrapperApi);
+      }
+    }
+  }
+
+  class CommandAggregatingAnnotatedApi extends AnnotatedApi {
+
+    private Collection<AnnotatedApi> combinedApis;
+
+    protected CommandAggregatingAnnotatedApi(AnnotatedApi api) {
+      super(api.spec, api.getEndPoint(), api.getCommands(), null);
+      combinedApis = Lists.newArrayList();
+    }
+
+    public void combineWith(AnnotatedApi api) {
+      // Merge in new 'command' entries
+      getCommands().putAll(api.getCommands());
+
+      // Reference to Api must be saved to to merge uncached values (i.e. 'spec') lazily
+      combinedApis.add(api);
+    }
+
+    @Override
+    public ValidatingJsonMap getSpec() {
+      final ValidatingJsonMap aggregatedSpec = spec.getSpec();
+
+      for (AnnotatedApi combinedApi : combinedApis) {
+        final ValidatingJsonMap specToCombine = combinedApi.getSpec();
+        aggregatedSpec.getMap("commands").putAll(specToCombine.getMap("commands"));
+      }
+
+      return aggregatedSpec;
+    }
+  }
+
   @SuppressWarnings({"unchecked"})
   private void validateAndRegister(Api api, Map<String, String> nameSubstitutes) {
     ValidatingJsonMap spec = api.getSpec();
@@ -102,7 +173,7 @@ public class ApiBag {
     for (String method : methods) {
       PathTrie<Api> registry = apis.get(method);
 
-      if (registry == null) apis.put(method, registry = new PathTrie<>(ImmutableSet.of("_introspect")));
+      if (registry == null) apis.put(method, registry = new CommandAggregatingPathTrie(ImmutableSet.of("_introspect")));
       ValidatingJsonMap url = spec.getMap("url", NOT_NULL);
       ValidatingJsonMap params = url.getMap("params", null);
       if (params != null) {
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 820a4b1..90908cb 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -16,33 +16,6 @@
  */
 package org.apache.solr.core;
 
-import java.io.Closeable;
-import java.io.IOException;
-import java.lang.invoke.MethodHandles;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.security.spec.InvalidKeySpecException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Date;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Properties;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeoutException;
-import java.util.function.Supplier;
-
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
@@ -97,7 +70,6 @@ import org.apache.solr.handler.ClusterAPI;
 import org.apache.solr.handler.CollectionBackupsAPI;
 import org.apache.solr.handler.CollectionsAPI;
 import org.apache.solr.handler.RequestHandlerBase;
-import org.apache.solr.handler.designer.SchemaDesignerAPI;
 import org.apache.solr.handler.SnapShooter;
 import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.handler.admin.ConfigSetsHandler;
@@ -112,7 +84,9 @@ import org.apache.solr.handler.admin.SecurityConfHandlerZk;
 import org.apache.solr.handler.admin.ZookeeperInfoHandler;
 import org.apache.solr.handler.admin.ZookeeperReadAPI;
 import org.apache.solr.handler.admin.ZookeeperStatusHandler;
+import org.apache.solr.handler.api.ApiRegistrar;
 import org.apache.solr.handler.component.ShardHandlerFactory;
+import org.apache.solr.handler.designer.SchemaDesignerAPI;
 import org.apache.solr.handler.sql.CalciteSolrDriver;
 import org.apache.solr.logging.LogWatcher;
 import org.apache.solr.logging.MDCLoggingContext;
@@ -141,16 +115,35 @@ import org.apache.zookeeper.KeeperException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.spec.InvalidKeySpecException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+
 import static java.util.Objects.requireNonNull;
-import static org.apache.solr.common.params.CommonParams.AUTHC_PATH;
-import static org.apache.solr.common.params.CommonParams.AUTHZ_PATH;
-import static org.apache.solr.common.params.CommonParams.COLLECTIONS_HANDLER_PATH;
-import static org.apache.solr.common.params.CommonParams.CONFIGSETS_HANDLER_PATH;
-import static org.apache.solr.common.params.CommonParams.CORES_HANDLER_PATH;
-import static org.apache.solr.common.params.CommonParams.INFO_HANDLER_PATH;
-import static org.apache.solr.common.params.CommonParams.METRICS_PATH;
-import static org.apache.solr.common.params.CommonParams.ZK_PATH;
-import static org.apache.solr.common.params.CommonParams.ZK_STATUS_PATH;
+import static org.apache.solr.common.params.CommonParams.*;
 import static org.apache.solr.core.CorePropertiesLocator.PROPERTIES_FILENAME;
 import static org.apache.solr.security.AuthenticationPlugin.AUTHENTICATION_PLUGIN_PROP;
 
@@ -774,6 +767,7 @@ public class CoreContainer {
 
     collectionsHandler = createHandler(COLLECTIONS_HANDLER_PATH, cfg.getCollectionsHandlerClass(), CollectionsHandler.class);
     final CollectionsAPI collectionsAPI = new CollectionsAPI(collectionsHandler);
+    ApiRegistrar.registerCollectionApis(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 8a82c59..1b1abf1 100644
--- a/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
@@ -28,7 +28,6 @@ import org.apache.solr.client.solrj.request.beans.DeleteAliasPayload;
 import org.apache.solr.client.solrj.request.beans.RestoreCollectionPayload;
 import org.apache.solr.client.solrj.request.beans.SetAliasPropertyPayload;
 import org.apache.solr.client.solrj.request.beans.V2ApiConstants;
-import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.CollectionAdminParams;
 import org.apache.solr.common.params.CollectionParams.CollectionAction;
 import org.apache.solr.handler.admin.CollectionsHandler;
@@ -41,13 +40,13 @@ import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.*;
+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;
 import static org.apache.solr.common.params.CollectionAdminParams.PROPERTY_PREFIX;
 import static org.apache.solr.common.params.CollectionAdminParams.ROUTER_PREFIX;
 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 static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM;
@@ -214,15 +213,4 @@ public class CollectionsAPI {
             toFlatten.forEach((k, v) -> destination.put(additionalPrefix + k, v));
         }
   }
-
-  @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,
-        CollectionAction.DELETE.toString(),
-        NAME, req.getPathTemplateValues().get(ZkStateReader.COLLECTION_PROP));
-    collectionsHandler.handleRequestBody(req, rsp);
-  }
-
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaPropertyAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaPropertyAPI.java
new file mode 100644
index 0000000..98446f1
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaPropertyAPI.java
@@ -0,0 +1,67 @@
+/*
+ * 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 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.AddReplicaPropertyPayload;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.handler.admin.CollectionsHandler;
+
+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.CommonParams.ACTION;
+import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+/**
+ * V2 API for adding a property to a collection replica
+ *
+ * This API (POST /v2/collections/collectionName {'add-replica-property': {...}}) is analogous to the v1
+ * /admin/collections?action=ADDREPLICAPROP command.
+ *
+ * @see AddReplicaPropertyPayload
+ */
+@EndPoint(
+        path = {"/c/{collection}", "/collections/{collection}"},
+        method = POST,
+        permission = COLL_EDIT_PERM)
+public class AddReplicaPropertyAPI {
+  private static final String V2_ADD_REPLICA_PROPERTY_CMD = "add-replica-property";
+
+  private final CollectionsHandler collectionsHandler;
+
+  public AddReplicaPropertyAPI(CollectionsHandler collectionsHandler) {
+    this.collectionsHandler = collectionsHandler;
+  }
+
+  @Command(name = V2_ADD_REPLICA_PROPERTY_CMD)
+  public void addReplicaProperty(PayloadObj<AddReplicaPropertyPayload> obj) throws Exception {
+    final AddReplicaPropertyPayload v2Body = obj.get();
+    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
+    v1Params.put(ACTION, CollectionParams.CollectionAction.ADDREPLICAPROP.toLower());
+    v1Params.put(COLLECTION, obj.getRequest().getPathTemplateValues().get(COLLECTION));
+    v1Params.put("property", v1Params.remove("name"));
+    v1Params.put("property.value", v1Params.remove("value"));
+
+    collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceShardUniqueAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceShardUniqueAPI.java
new file mode 100644
index 0000000..6d1b4a6
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceShardUniqueAPI.java
@@ -0,0 +1,65 @@
+/*
+ * 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 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.BalanceShardUniquePayload;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.handler.admin.CollectionsHandler;
+
+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.CommonParams.ACTION;
+import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+/**
+ * V2 API for insuring that a particular property is distributed evenly amongst the physical nodes comprising a collection.
+ *
+ * The new API (POST /v2/collections/collectionName {'balance-shard-unique': {...}}) is analogous to the v1
+ * /admin/collections?action=BALANCESHARDUNIQUE command.
+ *
+ * @see BalanceShardUniquePayload
+ */
+@EndPoint(
+        path = {"/c/{collection}", "/collections/{collection}"},
+        method = POST,
+        permission = COLL_EDIT_PERM)
+public class BalanceShardUniqueAPI {
+  private static final String V2_BALANCE_SHARD_UNIQUE_CMD = "balance-shard-unique";
+
+  private final CollectionsHandler collectionsHandler;
+
+  public BalanceShardUniqueAPI(CollectionsHandler collectionsHandler) {
+    this.collectionsHandler = collectionsHandler;
+  }
+
+  @Command(name = V2_BALANCE_SHARD_UNIQUE_CMD)
+  public void balanceShardUnique(PayloadObj<BalanceShardUniquePayload> obj) throws Exception {
+    final BalanceShardUniquePayload v2Body = obj.get();
+    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
+    v1Params.put(ACTION, CollectionParams.CollectionAction.BALANCESHARDUNIQUE.toLower());
+    v1Params.put(COLLECTION, obj.getRequest().getPathTemplateValues().get(COLLECTION));
+
+    collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
+  }
+}
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
new file mode 100644
index 0000000..1708894
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionAPI.java
@@ -0,0 +1,54 @@
+/*
+ * 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 org.apache.solr.api.EndPoint;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
+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;
+
+/**
+ * V2 API for deleting collections.
+ *
+ * This API (DELETE /v2/collections/collectionName) is equivalent to the v1 /admin/collections?action=DELETE command.
+ */
+public class DeleteCollectionAPI {
+
+  private final CollectionsHandler collectionsHandler;
+
+  public DeleteCollectionAPI(CollectionsHandler collectionsHandler) {
+    this.collectionsHandler = collectionsHandler;
+  }
+
+  @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);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplicaPropertyAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplicaPropertyAPI.java
new file mode 100644
index 0000000..cfedb66
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplicaPropertyAPI.java
@@ -0,0 +1,66 @@
+/*
+ * 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 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.DeleteReplicaPropertyPayload;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.handler.admin.CollectionsHandler;
+
+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.CommonParams.ACTION;
+import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+/**
+ * V2 API for removing a property from a collection replica
+ *
+ * This API (POST /v2/collections/collectionName {'delete-replica-property': {...}}) is analogous to the v1
+ * /admin/collections?action=DELETEREPLICAPROP command.
+ *
+ * @see DeleteReplicaPropertyPayload
+ */
+@EndPoint(
+        path = {"/c/{collection}", "/collections/{collection}"},
+        method = POST,
+        permission = COLL_EDIT_PERM)
+public class DeleteReplicaPropertyAPI {
+  private static final String V2_DELETE_REPLICA_PROPERTY_CMD = "delete-replica-property";
+
+  private final CollectionsHandler collectionsHandler;
+
+  public DeleteReplicaPropertyAPI(CollectionsHandler collectionsHandler) {
+    this.collectionsHandler = collectionsHandler;
+  }
+
+  @Command(name = V2_DELETE_REPLICA_PROPERTY_CMD)
+  public void deleteReplicaProperty(PayloadObj<DeleteReplicaPropertyPayload> obj) throws Exception {
+    final DeleteReplicaPropertyPayload v2Body = obj.get();
+    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
+    v1Params.put(ACTION, CollectionParams.CollectionAction.DELETEREPLICAPROP.toLower());
+    v1Params.put(COLLECTION, obj.getRequest().getPathTemplateValues().get(COLLECTION));
+
+    collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/MigrateDocsAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/MigrateDocsAPI.java
new file mode 100644
index 0000000..147cb85
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/MigrateDocsAPI.java
@@ -0,0 +1,78 @@
+/*
+ * 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 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.MigrateDocsPayload;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.handler.admin.CollectionsHandler;
+
+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.CommonParams.ACTION;
+import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+/**
+ * V2 API for migrating docs from one collection to another.
+ *
+ * The new API (POST /v2/collections/collectionName {'migrate-docs': {...}}) is analogous to the v1
+ * /admin/collections?action=MIGRATE command.
+ *
+ * @see MigrateDocsPayload
+ */
+@EndPoint(
+        path = {"/c/{collection}", "/collections/{collection}"},
+        method = POST,
+        permission = COLL_EDIT_PERM)
+public class MigrateDocsAPI {
+  private static final String V2_MIGRATE_DOCS_CMD = "migrate-docs";
+
+  private final CollectionsHandler collectionsHandler;
+
+  public MigrateDocsAPI(CollectionsHandler collectionsHandler) {
+    this.collectionsHandler = collectionsHandler;
+  }
+
+  @Command(name = V2_MIGRATE_DOCS_CMD)
+  public void migrateDocs(PayloadObj<MigrateDocsPayload> obj) throws Exception {
+    final MigrateDocsPayload v2Body = obj.get();
+    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
+    v1Params.put(ACTION, CollectionParams.CollectionAction.MIGRATE.toLower());
+    v1Params.put(COLLECTION, obj.getRequest().getPathTemplateValues().get(COLLECTION));
+
+    if (v2Body.splitKey != null) {
+      v1Params.remove("splitKey");
+      v1Params.put("split.key", v2Body.splitKey);
+    }
+    if (v2Body.target != null) {
+      v1Params.remove("target");
+      v1Params.put("target.collection", v2Body.target);
+    }
+    if (v2Body.forwardTimeout != null) {
+      v1Params.remove("forwardTimeout");
+      v1Params.put("forward.timeout", v2Body.forwardTimeout);
+    }
+
+    collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
+  }
+}
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
new file mode 100644
index 0000000..516fac7
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyCollectionAPI.java
@@ -0,0 +1,84 @@
+/*
+ * 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 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.common.params.CollectionParams;
+import org.apache.solr.handler.admin.CollectionsHandler;
+
+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.CommonParams.ACTION;
+import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+/**
+ * V2 API for modifying collections.
+ *
+ * The new API (POST /v2/collections/collectionName {'modify-collection': {...}}) is equivalent to the v1
+ * /admin/collections?action=MODIFYCOLLECTION command.
+ *
+ * @see ModifyCollectionPayload
+ */
+@EndPoint(
+        path = {"/c/{collection}", "/collections/{collection}"},
+        method = POST,
+        permission = COLL_EDIT_PERM)
+public class ModifyCollectionAPI {
+  private static final String V2_MODIFY_COLLECTION_CMD = "modify";
+
+  private final CollectionsHandler collectionsHandler;
+
+  public ModifyCollectionAPI(CollectionsHandler collectionsHandler) {
+    this.collectionsHandler = collectionsHandler;
+  }
+
+  @Command(name = V2_MODIFY_COLLECTION_CMD)
+  public void modifyCollection(PayloadObj<ModifyCollectionPayload> obj) throws Exception {
+    final ModifyCollectionPayload v2Body = obj.get();
+
+    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
+    v1Params.put(ACTION, CollectionParams.CollectionAction.MODIFYCOLLECTION.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.");
+    }
+
+    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/MoveReplicaAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/MoveReplicaAPI.java
new file mode 100644
index 0000000..c9c3b91
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/MoveReplicaAPI.java
@@ -0,0 +1,65 @@
+/*
+ * 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 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.MoveReplicaPayload;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.handler.admin.CollectionsHandler;
+
+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.CommonParams.ACTION;
+import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+/**
+ * V2 API for moving a collection replica to a different physical node.
+ *
+ * The new API (POST /v2/collections/collectionName {'move-replica': {...}}) is analogous to the v1
+ * /admin/collections?action=MOVEREPLICA command.
+ *
+ * @see MoveReplicaPayload
+ */
+@EndPoint(
+        path = {"/c/{collection}", "/collections/{collection}"},
+        method = POST,
+        permission = COLL_EDIT_PERM)
+public class MoveReplicaAPI {
+  private static final String V2_MOVE_REPLICA_CMD = "move-replica";
+
+  private final CollectionsHandler collectionsHandler;
+
+  public MoveReplicaAPI(CollectionsHandler collectionsHandler) {
+    this.collectionsHandler = collectionsHandler;
+  }
+
+  @Command(name = V2_MOVE_REPLICA_CMD)
+  public void moveReplica(PayloadObj<MoveReplicaPayload> obj) throws Exception {
+    final MoveReplicaPayload v2Body = obj.get();
+    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
+    v1Params.put(ACTION, CollectionParams.CollectionAction.MOVEREPLICA.toLower());
+    v1Params.put(COLLECTION, obj.getRequest().getPathTemplateValues().get(COLLECTION));
+
+    collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/RebalanceLeadersAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/RebalanceLeadersAPI.java
new file mode 100644
index 0000000..3a3a79a
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/RebalanceLeadersAPI.java
@@ -0,0 +1,65 @@
+/*
+ * 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 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.RebalanceLeadersPayload;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.handler.admin.CollectionsHandler;
+
+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.CommonParams.ACTION;
+import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+/**
+ * V2 API for balancing shard leaders in a collection across nodes.
+ *
+ * This API (POST /v2/collections/collectionName {'rebalance-leaders': {...}}) is analogous to the v1
+ * /admin/collections?action=REBALANCELEADERS command.
+ *
+ * @see RebalanceLeadersPayload
+ */
+@EndPoint(
+        path = {"/c/{collection}", "/collections/{collection}"},
+        method = POST,
+        permission = COLL_EDIT_PERM)
+public class RebalanceLeadersAPI {
+  private static final String V2_REBALANCE_LEADERS_CMD = "rebalance-leaders";
+
+  private final CollectionsHandler collectionsHandler;
+
+  public RebalanceLeadersAPI(CollectionsHandler collectionsHandler) {
+    this.collectionsHandler = collectionsHandler;
+  }
+
+  @Command(name = V2_REBALANCE_LEADERS_CMD)
+  public void rebalanceLeaders(PayloadObj<RebalanceLeadersPayload> obj) throws Exception {
+    final RebalanceLeadersPayload v2Body = obj.get();
+    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
+    v1Params.put(ACTION, CollectionParams.CollectionAction.REBALANCELEADERS.toLower());
+    v1Params.put(COLLECTION, obj.getRequest().getPathTemplateValues().get(COLLECTION));
+
+    collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ReloadCollectionAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ReloadCollectionAPI.java
new file mode 100644
index 0000000..040c6a4
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ReloadCollectionAPI.java
@@ -0,0 +1,66 @@
+/*
+ * 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 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.ReloadCollectionPayload;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.handler.admin.CollectionsHandler;
+
+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.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;
+
+/**
+ * V2 API for reloading collections.
+ *
+ * The new API (POST /v2/collections/collectionName {'reload': {...}}) is analogous to the v1
+ * /admin/collections?action=RELOAD command.
+ *
+ * @see ReloadCollectionPayload
+ */
+@EndPoint(
+        path = {"/c/{collection}", "/collections/{collection}"},
+        method = POST,
+        permission = COLL_EDIT_PERM) // TODO Does this permission make sense for reload?
+public class ReloadCollectionAPI {
+  private static final String V2_RELOAD_COLLECTION_CMD = "reload";
+
+  private final CollectionsHandler collectionsHandler;
+
+  public ReloadCollectionAPI(CollectionsHandler collectionsHandler) {
+    this.collectionsHandler = collectionsHandler;
+  }
+
+  @Command(name = V2_RELOAD_COLLECTION_CMD)
+  public void reloadCollection(PayloadObj<ReloadCollectionPayload> obj) throws Exception {
+    final ReloadCollectionPayload v2Body = obj.get();
+    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
+    v1Params.put(ACTION, CollectionParams.CollectionAction.RELOAD.toLower());
+    v1Params.put(NAME, obj.getRequest().getPathTemplateValues().get(COLLECTION));
+
+    collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
+  }
+}
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
new file mode 100644
index 0000000..6c0aea9
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/SetCollectionPropertyAPI.java
@@ -0,0 +1,72 @@
+/*
+ * 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 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;
+
+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.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;
+
+/**
+ * V2 API for modifying collection-level properties.
+ *
+ * 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/java/org/apache/solr/handler/admin/api/package-info.java b/solr/core/src/java/org/apache/solr/handler/admin/api/package-info.java
new file mode 100644
index 0000000..921f01f
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * V2 API implementations for "admin" APIs.
+ */
+package org.apache.solr.handler.admin.api;
\ No newline at end of file
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
new file mode 100644
index 0000000..5e2a240
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java
@@ -0,0 +1,54 @@
+/*
+ * 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 org.apache.solr.api.ApiBag;
+import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.handler.admin.api.AddReplicaPropertyAPI;
+import org.apache.solr.handler.admin.api.BalanceShardUniqueAPI;
+import org.apache.solr.handler.admin.api.DeleteCollectionAPI;
+import org.apache.solr.handler.admin.api.DeleteReplicaPropertyAPI;
+import org.apache.solr.handler.admin.api.MigrateDocsAPI;
+import org.apache.solr.handler.admin.api.ModifyCollectionAPI;
+import org.apache.solr.handler.admin.api.MoveReplicaAPI;
+import org.apache.solr.handler.admin.api.RebalanceLeadersAPI;
+import org.apache.solr.handler.admin.api.ReloadCollectionAPI;
+import org.apache.solr.handler.admin.api.SetCollectionPropertyAPI;
+
+/**
+ * Registers annotation-based V2 APIs with an {@link ApiBag}
+ *
+ * Historically these APIs were registered directly by code in {@link org.apache.solr.core.CoreContainer}, but as the
+ * number of annotation-based v2 APIs grew this became increasingly unwieldy.  So {@link ApiRegistrar} serves as a single
+ * place where all APIs under a particular path can be registered together.
+ */
+public class ApiRegistrar {
+
+  public static void registerCollectionApis(ApiBag apiBag, CollectionsHandler collectionsHandler) {
+    apiBag.registerObject(new AddReplicaPropertyAPI(collectionsHandler));
+    apiBag.registerObject(new BalanceShardUniqueAPI(collectionsHandler));
+    apiBag.registerObject(new DeleteCollectionAPI(collectionsHandler));
+    apiBag.registerObject(new DeleteReplicaPropertyAPI(collectionsHandler));
+    apiBag.registerObject(new MigrateDocsAPI(collectionsHandler));
+    apiBag.registerObject(new ModifyCollectionAPI(collectionsHandler));
+    apiBag.registerObject(new MoveReplicaAPI(collectionsHandler));
+    apiBag.registerObject(new RebalanceLeadersAPI(collectionsHandler));
+    apiBag.registerObject(new ReloadCollectionAPI(collectionsHandler));
+    apiBag.registerObject(new SetCollectionPropertyAPI(collectionsHandler));
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/api/package-info.java b/solr/core/src/java/org/apache/solr/handler/api/package-info.java
new file mode 100644
index 0000000..1437ec2
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/api/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * V2 utilities useful for all API implementations.
+ */
+package org.apache.solr.handler.api;
\ No newline at end of file
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 f3ca9da..8f5a62d 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
@@ -55,6 +55,7 @@ import org.apache.solr.handler.CollectionsAPI;
 import org.apache.solr.handler.PingRequestHandler;
 import org.apache.solr.handler.SchemaHandler;
 import org.apache.solr.handler.SolrConfigHandler;
+import org.apache.solr.handler.api.ApiRegistrar;
 import org.apache.solr.request.LocalSolrQueryRequest;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrQueryRequestBase;
@@ -81,6 +82,7 @@ public class TestApiFramework extends SolrTestCaseJ4 {
     TestCollectionAPIs.MockCollectionsHandler collectionsHandler = new TestCollectionAPIs.MockCollectionsHandler();
     containerHandlers.put(COLLECTIONS_HANDLER_PATH, collectionsHandler);
     containerHandlers.getApiBag().registerObject(new CollectionsAPI(collectionsHandler));
+    ApiRegistrar.registerCollectionApis(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 cb3562c..a076d94 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
@@ -42,6 +42,7 @@ import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.handler.ClusterAPI;
 import org.apache.solr.handler.CollectionsAPI;
+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;
@@ -87,6 +88,7 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
       final CollectionsAPI collectionsAPI = new CollectionsAPI(collectionsHandler);
       apiBag.registerObject(new CollectionsAPI(collectionsHandler));
       apiBag.registerObject(collectionsAPI.collectionsCommands);
+      ApiRegistrar.registerCollectionApis(apiBag, collectionsHandler);
       Collection<Api> apis = collectionsHandler.getApis();
       for (Api api : apis) apiBag.register(api, Collections.emptyMap());
 
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
new file mode 100644
index 0000000..c6b5d70
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java
@@ -0,0 +1,271 @@
+/*
+ * 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.cloud.ZkStateReader;
+import org.apache.solr.common.params.CollectionParams;
+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.params.CollectionAdminParams.COLLECTION;
+import static org.apache.solr.common.params.CollectionAdminParams.COLL_CONF;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonParams.ACTION;
+import static org.apache.solr.common.params.CommonParams.NAME;
+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}.
+ *
+ * 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 V2CollectionAPIMappingTest 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.registerCollectionApis(apiBag, mockCollectionsHandler);
+  }
+
+  @Test
+  public void testModifyCollectionAllProperties() throws Exception {
+    final SolrParams v1Params = captureConvertedV1Params("/collections/collName", "POST",
+            "{ 'modify': {" +
+                    "'replicationFactor': 123, " +
+                    "'readOnly': true, " +
+                    "'config': 'techproducts_config', " +
+                    "'async': 'requestTrackingId', " +
+                    "'properties': {" +
+                    "     'foo': 'bar', " +
+                    "     'baz': 456 " +
+                    "}" +
+                    "}}");
+
+    assertEquals(CollectionParams.CollectionAction.MODIFYCOLLECTION.lowerName, v1Params.get(ACTION));
+    assertEquals("collName", v1Params.get(COLLECTION));
+    assertEquals(123, v1Params.getPrimitiveInt(ZkStateReader.REPLICATION_FACTOR));
+    assertEquals(true, v1Params.getPrimitiveBool(ZkStateReader.READ_ONLY));
+    assertEquals("techproducts_config", v1Params.get(COLL_CONF));
+    assertEquals("requestTrackingId", v1Params.get(ASYNC));
+    assertEquals("bar", v1Params.get("property.foo"));
+    assertEquals(456, v1Params.getPrimitiveInt("property.baz"));
+  }
+
+  @Test
+  public void testReloadCollectionAllProperties() throws Exception {
+    final SolrParams v1Params = captureConvertedV1Params("/collections/collName", "POST",
+            "{ 'reload': {'async': 'requestTrackingId'}}");
+
+    assertEquals(CollectionParams.CollectionAction.RELOAD.lowerName, v1Params.get(ACTION));
+    assertEquals("collName", v1Params.get(NAME));
+    assertEquals("requestTrackingId", v1Params.get(ASYNC));
+  }
+
+  @Test
+  public void testMoveReplicaAllProperties() throws Exception {
+    final SolrParams v1Params = captureConvertedV1Params("/collections/collName", "POST",
+            "{ 'move-replica': {" +
+                    "'sourceNode': 'someSourceNode', " +
+                    "'targetNode': 'someTargetNode', " +
+                    "'replica': 'someReplica', " +
+                    "'shard': 'someShard', " +
+                    "'waitForFinalState': true, " +
+                    "'timeout': 123, " +
+                    "'inPlaceMove': true, " +
+                    "'followAliases': true " +
+                    "}}");
+
+    assertEquals(CollectionParams.CollectionAction.MOVEREPLICA.lowerName, v1Params.get(ACTION));
+    assertEquals("collName", v1Params.get(COLLECTION));
+    assertEquals("someSourceNode", v1Params.get("sourceNode"));
+    assertEquals("someTargetNode", v1Params.get("targetNode"));
+    assertEquals("someReplica", v1Params.get("replica"));
+    assertEquals("someShard", v1Params.get("shard"));
+    assertEquals(true, v1Params.getPrimitiveBool("waitForFinalState"));
+    assertEquals(123, v1Params.getPrimitiveInt("timeout"));
+    assertEquals(true, v1Params.getPrimitiveBool("inPlaceMove"));
+    assertEquals(true, v1Params.getPrimitiveBool("followAliases"));
+  }
+
+  @Test
+  public void testMigrateDocsAllProperties() throws Exception {
+    final SolrParams v1Params = captureConvertedV1Params("/collections/collName", "POST",
+            "{ 'migrate-docs': {" +
+                    "'target': 'someTargetCollection', " +
+                    "'splitKey': 'someSplitKey', " +
+                    "'forwardTimeout': 123, " +
+                    "'followAliases': true, " +
+                    "'async': 'requestTrackingId' " +
+                    "}}");
+
+    assertEquals(CollectionParams.CollectionAction.MIGRATE.lowerName, v1Params.get(ACTION));
+    assertEquals("collName", v1Params.get(COLLECTION));
+    assertEquals("someTargetCollection", v1Params.get("target.collection"));
+    assertEquals("someSplitKey", v1Params.get("split.key"));
+    assertEquals(123, v1Params.getPrimitiveInt("forward.timeout"));
+    assertEquals(true, v1Params.getPrimitiveBool("followAliases"));
+    assertEquals("requestTrackingId", v1Params.get(ASYNC));
+  }
+
+  @Test
+  public void testBalanceShardUniqueAllProperties() throws Exception {
+    final SolrParams v1Params = captureConvertedV1Params("/collections/collName", "POST",
+            "{ 'balance-shard-unique': {" +
+                    "'property': 'somePropertyToBalance', " +
+                    "'onlyactivenodes': false, " +
+                    "'shardUnique': true" +
+                    "}}");
+
+    assertEquals(CollectionParams.CollectionAction.BALANCESHARDUNIQUE.lowerName, v1Params.get(ACTION));
+    assertEquals("collName", v1Params.get(COLLECTION));
+    assertEquals("somePropertyToBalance", v1Params.get("property"));
+    assertEquals(false, v1Params.getPrimitiveBool("onlyactivenodes"));
+    assertEquals(true, v1Params.getPrimitiveBool("shardUnique"));
+  }
+
+  @Test
+  public void testRebalanceLeadersAllProperties() throws Exception {
+    final SolrParams v1Params = captureConvertedV1Params("/collections/collName", "POST",
+            "{ 'rebalance-leaders': {" +
+                    "'maxAtOnce': 123, " +
+                    "'maxWaitSeconds': 456" +
+                    "}}");
+
+    assertEquals(CollectionParams.CollectionAction.REBALANCELEADERS.lowerName, v1Params.get(ACTION));
+    assertEquals("collName", v1Params.get(COLLECTION));
+    assertEquals(123, v1Params.getPrimitiveInt("maxAtOnce"));
+    assertEquals(456, v1Params.getPrimitiveInt("maxWaitSeconds"));
+  }
+
+  @Test
+  public void testAddReplicaPropertyAllProperties() throws Exception {
+    final SolrParams v1Params = captureConvertedV1Params("/collections/collName", "POST",
+            "{ 'add-replica-property': {" +
+                    "'shard': 'someShardName', " +
+                    "'replica': 'someReplicaName', " +
+                    "'name': 'somePropertyName', " +
+                    "'value': 'somePropertyValue'" +
+                    "}}");
+
+    assertEquals(CollectionParams.CollectionAction.ADDREPLICAPROP.lowerName, v1Params.get(ACTION));
+    assertEquals("collName", v1Params.get(COLLECTION));
+    assertEquals("someShardName", v1Params.get("shard"));
+    assertEquals("someReplicaName", v1Params.get("replica"));
+    assertEquals("somePropertyName", v1Params.get("property"));
+    assertEquals("somePropertyValue", v1Params.get("property.value"));
+  }
+
+  @Test
+  public void testDeleteReplicaPropertyAllProperties() throws Exception {
+    final SolrParams v1Params = captureConvertedV1Params("/collections/collName", "POST",
+            "{ 'delete-replica-property': {" +
+                    "'shard': 'someShardName', " +
+                    "'replica': 'someReplicaName', " +
+                    "'property': 'somePropertyName' " +
+                    "}}");
+
+    assertEquals(CollectionParams.CollectionAction.DELETEREPLICAPROP.lowerName, v1Params.get(ACTION));
+    assertEquals("collName", v1Params.get(COLLECTION));
+    assertEquals("someShardName", v1Params.get("shard"));
+    assertEquals("someReplicaName", v1Params.get("replica"));
+    assertEquals("somePropertyName", v1Params.get("property"));
+  }
+
+  @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"));
+  }
+
+  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/core/src/test/org/apache/solr/handler/admin/api/package-info.java b/solr/core/src/test/org/apache/solr/handler/admin/api/package-info.java
new file mode 100644
index 0000000..4282156
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Unit tests for v2 "admin" API implementations.
+ */
+package org.apache.solr.handler.admin.api;
\ No newline at end of file
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 cda91db..76e4b1c 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
@@ -18,6 +18,12 @@
 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;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -27,36 +33,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 
-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;
-
 import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
-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.CollectionApiMapping.EndPoint.COLLECTION_STATE;
-import static org.apache.solr.client.solrj.request.CollectionApiMapping.EndPoint.PER_COLLECTION;
-import static org.apache.solr.client.solrj.request.CollectionApiMapping.EndPoint.PER_COLLECTION_PER_SHARD_COMMANDS;
-import static org.apache.solr.client.solrj.request.CollectionApiMapping.EndPoint.PER_COLLECTION_PER_SHARD_DELETE;
-import static org.apache.solr.client.solrj.request.CollectionApiMapping.EndPoint.PER_COLLECTION_PER_SHARD_PER_REPLICA_DELETE;
-import static org.apache.solr.client.solrj.request.CollectionApiMapping.EndPoint.PER_COLLECTION_SHARDS_COMMANDS;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICA;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.BALANCESHARDUNIQUE;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.CLUSTERSTATUS;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.COLLECTIONPROP;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.CREATESHARD;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.DELETEREPLICA;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.DELETEREPLICAPROP;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.DELETESHARD;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.MIGRATE;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.MODIFYCOLLECTION;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.MOVEREPLICA;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.REBALANCELEADERS;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.RELOAD;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.SPLITSHARD;
-import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.*;
+import static org.apache.solr.client.solrj.request.CollectionApiMapping.EndPoint.*;
+import static org.apache.solr.common.params.CollectionParams.CollectionAction.*;
 
 /**
  * Stores the mapping of v1 API parameters to v2 API parameters
@@ -66,29 +46,6 @@ public class CollectionApiMapping {
 
   public enum Meta implements CommandMeta {
     GET_A_COLLECTION(COLLECTION_STATE, GET, CLUSTERSTATUS),
-    RELOAD_COLL(PER_COLLECTION,
-        POST,
-        RELOAD,
-        RELOAD.toLower(),
-        Map.of(NAME, "collection")),
-    MODIFY_COLLECTION(PER_COLLECTION,
-        POST,
-        MODIFYCOLLECTION,
-        "modify",null),
-    MIGRATE_DOCS(PER_COLLECTION,
-        POST,
-        MIGRATE,
-        "migrate-docs",
-        Map.of("split.key", "splitKey",
-            "target.collection", "target",
-            "forward.timeout", "forwardTimeout"
-        )),
-    MOVE_REPLICA(PER_COLLECTION,
-        POST, MOVEREPLICA, "move-replica", null),
-    REBALANCE_LEADERS(PER_COLLECTION,
-        POST,
-        REBALANCELEADERS,
-        "rebalance-leaders", null),
     CREATE_SHARD(PER_COLLECTION_SHARDS_COMMANDS,
         POST,
         CREATESHARD,
@@ -100,7 +57,6 @@ public class CollectionApiMapping {
         return super.getParamSubstitute(param);
       }
     },
-
     SPLIT_SHARD(PER_COLLECTION_SHARDS_COMMANDS,
         POST,
         SPLITSHARD,
@@ -119,33 +75,12 @@ public class CollectionApiMapping {
 
     DELETE_REPLICA(PER_COLLECTION_PER_SHARD_PER_REPLICA_DELETE,
         DELETE, DELETEREPLICA),
-
     SYNC_SHARD(PER_COLLECTION_PER_SHARD_COMMANDS,
         POST,
         CollectionAction.SYNCSHARD,
         "synch-shard",
         null),
-    ADD_REPLICA_PROPERTY(PER_COLLECTION,
-        POST,
-        CollectionAction.ADDREPLICAPROP,
-        "add-replica-property",
-        Map.of("property", "name", "property.value", "value")),
-    DELETE_REPLICA_PROPERTY(PER_COLLECTION,
-        POST,
-        DELETEREPLICAPROP,
-        "delete-replica-property",
-        null),
-    SET_COLLECTION_PROPERTY(PER_COLLECTION,
-        POST,
-        COLLECTIONPROP,
-        "set-collection-property",
-        Map.of(
-            NAME, "collection",
-            "propertyName", "name",
-            "propertyValue", "value")),
-    FORCE_LEADER(PER_COLLECTION_PER_SHARD_COMMANDS, POST, CollectionAction.FORCELEADER, "force-leader", null),
-    BALANCE_SHARD_UNIQUE(PER_COLLECTION, POST, BALANCESHARDUNIQUE,"balance-shard-unique" , null)
-    ;
+    FORCE_LEADER(PER_COLLECTION_PER_SHARD_COMMANDS, POST, CollectionAction.FORCELEADER, "force-leader", null);
 
     public final String commandName;
     public final EndPoint endPoint;
@@ -265,7 +200,6 @@ public class CollectionApiMapping {
 
   public enum EndPoint implements V2EndPoint {
     COLLECTION_STATE("collections.collection"),
-    PER_COLLECTION("collections.collection.Commands"),
     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"),
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/AddReplicaPropertyPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/AddReplicaPropertyPayload.java
new file mode 100644
index 0000000..3ba1988
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/AddReplicaPropertyPayload.java
@@ -0,0 +1,37 @@
+/*
+ * 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 AddReplicaPropertyPayload implements ReflectMapWriter {
+  @JsonProperty(required =  true)
+  public String shard;
+
+  @JsonProperty(required = true)
+  public String replica;
+
+  @JsonProperty(required = true)
+  public String name;
+
+  @JsonProperty(required = true)
+  public String value;
+
+  @JsonProperty
+  public Boolean shardUnique = null;
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/BalanceShardUniquePayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/BalanceShardUniquePayload.java
new file mode 100644
index 0000000..1fdc6c7
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/BalanceShardUniquePayload.java
@@ -0,0 +1,31 @@
+/*
+ * 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 BalanceShardUniquePayload implements ReflectMapWriter {
+  @JsonProperty(required = true)
+  public String property;
+
+  @JsonProperty
+  public Boolean onlyactivenodes = null;
+
+  @JsonProperty
+  public Boolean shardUnique;
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/DeleteReplicaPropertyPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/DeleteReplicaPropertyPayload.java
new file mode 100644
index 0000000..fd8d95f
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/DeleteReplicaPropertyPayload.java
@@ -0,0 +1,31 @@
+/*
+ * 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 DeleteReplicaPropertyPayload implements ReflectMapWriter {
+  @JsonProperty(required = true)
+  public String shard;
+
+  @JsonProperty(required = true)
+  public String replica;
+
+  @JsonProperty(required = true)
+  public String property;
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/MigrateDocsPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/MigrateDocsPayload.java
new file mode 100644
index 0000000..6b9ef25
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/MigrateDocsPayload.java
@@ -0,0 +1,37 @@
+/*
+ * 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 MigrateDocsPayload implements ReflectMapWriter {
+  @JsonProperty(required = true)
+  public String target;
+
+  @JsonProperty(required = true)
+  public String splitKey;
+
+  @JsonProperty
+  public Integer forwardTimeout = 60;
+
+  @JsonProperty
+  public Boolean followAliases;
+
+  @JsonProperty
+  public String async;
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/ModifyCollectionPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/ModifyCollectionPayload.java
new file mode 100644
index 0000000..e2c744e7
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/ModifyCollectionPayload.java
@@ -0,0 +1,39 @@
+/*
+ * 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 ModifyCollectionPayload implements ReflectMapWriter {
+  @JsonProperty
+  public Integer replicationFactor;
+
+  @JsonProperty
+  public Boolean readOnly;
+
+  @JsonProperty
+  public String config;
+
+  @JsonProperty
+  public Map<String, Object> properties;
+
+  @JsonProperty
+  public String async;
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/MoveReplicaPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/MoveReplicaPayload.java
new file mode 100644
index 0000000..5fc81df
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/MoveReplicaPayload.java
@@ -0,0 +1,48 @@
+/*
+ * 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 MoveReplicaPayload implements ReflectMapWriter {
+  @JsonProperty(required = true)
+  public String targetNode;
+
+  @JsonProperty
+  public String replica;
+
+  @JsonProperty
+  public String shard;
+
+  @JsonProperty
+  public String sourceNode;
+
+  @JsonProperty
+  public Boolean waitForFinalState = false;
+
+  @JsonProperty
+  public Integer timeout = 600;
+
+  @JsonProperty
+  public Boolean inPlaceMove = true;
+
+  @JsonProperty
+  public Boolean followAliases;
+
+  // TODO Should this support 'async'? Does 'waitForFinalState' replace 'async' here?
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/RebalanceLeadersPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/RebalanceLeadersPayload.java
new file mode 100644
index 0000000..3b7eabb
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/RebalanceLeadersPayload.java
@@ -0,0 +1,28 @@
+/*
+ * 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 RebalanceLeadersPayload implements ReflectMapWriter {
+  @JsonProperty
+  public Integer maxAtOnce;
+
+  @JsonProperty
+  public Integer maxWaitSeconds = 60;
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/ReloadCollectionPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/ReloadCollectionPayload.java
new file mode 100644
index 0000000..7b106d4
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/ReloadCollectionPayload.java
@@ -0,0 +1,25 @@
+/*
+ * 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 ReloadCollectionPayload implements ReflectMapWriter {
+  @JsonProperty
+  public String async;
+}
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
new file mode 100644
index 0000000..75eeb3b
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetCollectionPropertyPayload.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+}
diff --git a/solr/solrj/src/java/org/apache/solr/common/util/PathTrie.java b/solr/solrj/src/java/org/apache/solr/common/util/PathTrie.java
index 621fb21..c5f5d69 100644
--- a/solr/solrj/src/java/org/apache/solr/common/util/PathTrie.java
+++ b/solr/solrj/src/java/org/apache/solr/common/util/PathTrie.java
@@ -49,7 +49,7 @@ public class PathTrie<T> {
 
   public void insert(List<String> parts, Map<String, String> replacements, T o) {
     if (parts.isEmpty()) {
-      root.obj = o;
+      attachValueToNode(root, o);
       return;
     }
     replaceTemplates(parts, replacements);
@@ -120,7 +120,16 @@ public class PathTrie<T> {
 
   }
 
-  class Node {
+  /**
+   * Attaches the provided object to the PathTrie node
+   *
+   * The default implementation overwrites any existing values, but this can be overwritten by subclasses.
+   */
+  protected void attachValueToNode(PathTrie<T>.Node node, T o) {
+    node.obj = o;
+  }
+
+  public class Node {
     String name;
     Map<String, Node> children;
     T obj;
@@ -139,6 +148,9 @@ public class PathTrie<T> {
       if (path.isEmpty()) obj = o;
     }
 
+    public T getObject() { return obj; }
+    public void setObject(T o) { obj = o; }
+
 
     private synchronized void insert(List<String> path, T o) {
       String part = path.get(0);
@@ -164,7 +176,7 @@ public class PathTrie<T> {
       if (!path.isEmpty()) {
         matchedChild.insert(path, o);
       } else {
-        matchedChild.obj = o;
+        attachValueToNode(matchedChild, o);
       }
 
     }
diff --git a/solr/solrj/src/resources/apispec/collections.collection.Commands.json b/solr/solrj/src/resources/apispec/collections.collection.Commands.json
deleted file mode 100644
index b4545d3..0000000
--- a/solr/solrj/src/resources/apispec/collections.collection.Commands.json
+++ /dev/null
@@ -1,193 +0,0 @@
-{
-  "documentation": "https://lucene.apache.org/solr/guide/collections-api.html",
-  "description": "Several collection-level operations are supported with this endpoint: modify collection attributes; reload a collection; migrate documents to a different collection; rebalance collection leaders; balance properties across shards; and add or delete a replica property.",
-  "methods": [
-    "POST"
-  ],
-  "url": {
-    "paths": [
-      "/collections/{collection}",
-      "/c/{collection}"
-    ]
-  },
-  "commands": {
-    "modify": {
-      "#include": "collections.collection.Commands.modify"
-    },
-    "reload": {
-      "#include": "collections.collection.Commands.reload"
-    },
-    "move-replica": {
-      "type": "object",
-      "documentation": "https://lucene.apache.org/solr/guide/replica-management.html#movereplica",
-      "description": "This command moves a replica from one node to a new node. In case of shared filesystems the `dataDir` and `ulogDir` may be reused.",
-      "properties": {
-        "replica": {
-          "type": "string",
-          "description": "The name of the replica to move. Either this parameter or shard + sourceNode is required, this parameter takes precedence."
-        },
-        "shard": {
-          "type": "string",
-          "description": "The name of the shard for which a replica should be moved. Either this parameter or replica is required. If replica is specified, this parameter is ignored."
-        },
-        "sourceNode": {
-          "type": "string",
-          "description": "The name of the node that contains the replica. Either this parameter or replica is required. If replica is specified, this parameter is ignored."
-        },
-        "targetNode": {
-          "type": "string",
-          "description": "The name of the destination node. This parameter is required."
-        },
-        "waitForFinalState": {
-          "type": "boolean",
-          "default": "false",
-          "description": "Wait for the moved replica to become active."
-        },
-        "timeout": {
-          "type": "integer",
-          "default": 600,
-          "description": "Number of seconds to wait for replica to become active before failing. For very large replicas this may need to be increased to ensure the old replica is deleted. Ignored for hdfs replicas."
-        },
-        "inPlaceMove": {
-          "type": "boolean",
-          "default": "true",
-          "description": "For replicas that use shared filesystems allow 'in-place' move that reuses shared data."
-        }
-      }
-    },
-    "migrate-docs":{
-      "type":"object",
-      "documentation":"https://lucene.apache.org/solr/guide/collection-management.html#migrate",
-      "description": "Moves documents with a given routing key to another collection. The source collection will continue to have the same documents, but will start re-routing write requests to the new collection. This command only works on collections using the 'compositeId' type of document routing.",
-      "properties":{
-        "target":{
-          "type":"string",
-          "description":"The name of the collection to which documents will be migrated."
-        },
-        "splitKey":{
-          "type":"string",
-          "description":"The routing key prefix. For example, if uniqueKey is a!123, then you would use split.key=a! This key may span multiple shards on source and target collections. The migration will be completed shard-by-shard in a single thread."
-        },
-        "forwardTimeout":{
-          "type":"integer",
-          "description":"The timeout, in seconds, until which write requests made to the source collection for the given splitKey will be forwarded to the target shard. Once this time is up, write requests will be routed to the target collection. Any applications sending read or write requests should be modified once the migration is complete to send documents to the right collection.",
-          "default": "60"
-        },
-        "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."
-        }
-      },
-      "required":["target", "splitKey"]
-    },
-    "balance-shard-unique":{
-      "type":"object",
-      "documentation": "https://lucene.apache.org/solr/guide/cluster-node-management.html#balanceshardunique",
-      "description": "Ensures a property is distributed equally across all physical nodes of a collection. If the property already exists on a replica, effort is made to leave it there. However, if it does not exist on any repica, a shard will be chosen and the property added.",
-      "properties":{
-        "property":{
-          "type":"string",
-          "description": "The property to balance across nodes. This can be entered as 'property.<property>' or simply '<property>'. If the 'property.' prefix is not defined, it will be added automatically."
-       },
-        "onlyactivenodes":{
-          "type":"boolean",
-          "description": "Normally, a property is instantiated on active nodes only. If this parameter is specified as 'false', then inactive nodes are also included for distribution.",
-          "default": "true"
-        },
-        "shardUnique":{
-          "type":"boolean",
-          "description": "There is one pre-defined property (preferredLeader) that defaults this value to 'true'. For all other properties that are balanced, this must be set to 'true' or an error message is returned."
-        }
-      },
-      "required":["property"]
-    },
-    "rebalance-leaders" :{
-      "type":"object",
-      "documentation": "https://lucene.apache.org/solr/guide/replica-management.html#rebalanceleaders",
-      "description": "Reassign leaders in a collection according to the preferredLeader property across active nodes. This command should be run after the preferredLeader property has been set with the balance-shards or add-replica-property commands.",
-      "properties":{
-        "maxAtOnce":{
-          "type":"integer",
-          "description":"The maximum number of reassignments to have in the queue at one time. Values <=0 use the default value Integer.MAX_VALUE. When this number is reached, the process waits for one or more leaders to be successfully assigned before adding more to the queue."
-        },
-        "maxWaitSeconds":{
-          "type":"integer",
-          "description":"Timeout, in seconds, when waiting for leaders to be reassigned. If maxAtOnce is less than the number of reassignments pending, this is the maximum interval for any single reassignment.",
-          "default": "60"
-        }
-      }
-    },
-    "add-replica-property": {
-      "documentation": "https://lucene.apache.org/solr/guide/replica-management.html#addreplicaprop",
-      "description": "Assign an arbitrary property to a particular replica and give it the value specified. If the property already exists, it will be overwritten with the new value.",
-      "type": "object",
-      "properties": {
-        "shard": {
-          "type": "string",
-          "description": "The name of the shard the replica belongs to."
-        },
-        "replica": {
-          "type": "string",
-          "description": "The name of the replica."
-        },
-        "name": {
-          "type": "string",
-          "description": "The name of the property. This can be entered as 'property.<property>' or simply '<property>'. If the 'property.' prefix is not defined, it will be added automatically."
-        },
-        "value": {
-          "type": "string",
-          "description": "The value to assign to the property."
-        },
-        "shardUnique": {
-          "type": "boolean",
-          "description": "If true, setting this property in one replica will remove the property from all other replicas in that shard.",
-          "default": "false"
-        }
-      },
-      "required": [
-        "name",
-        "value",
-        "shard",
-        "replica"
-      ]
-    },
-    "delete-replica-property": {
-      "description": "Deletes an arbitrary property from a particular replica",
-      "documentation": "https://lucene.apache.org/solr/guide/replica-management.html#deletereplicaprop",
-      "type": "object",
-      "properties": {
-        "shard": {
-          "type": "string",
-          "description": "The name of the shard the replica belongs to."
-        },
-        "replica": {
-          "type": "string",
-          "description": "The name of the replica."
-        },
-        "property": {
-          "type": "string",
-          "description": "The name of the property to remove."
-        }
-      },
-      "required":["shard","replica","property"]
-    },
-    "set-collection-property": {
-      "documentation": "https://lucene.apache.org/solr/guide/collection-management.html#collectionprop",
-      "description": "Sets a property for the collection",
-      "type": "object",
-      "properties": {
-        "name": {
-          "type": "string",
-          "description": "The name of the property"
-        },
-        "value": {
-          "type": "string",
-          "description": "The value of the property. When not provided, the property is deleted"
-        }
-      },
-      "required": [
-        "name"
-      ]
-    }
-  }
-}
diff --git a/solr/solrj/src/resources/apispec/collections.collection.Commands.reload.json b/solr/solrj/src/resources/apispec/collections.collection.Commands.reload.json
deleted file mode 100644
index ed94858..0000000
--- a/solr/solrj/src/resources/apispec/collections.collection.Commands.reload.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "documentation": "https://lucene.apache.org/solr/guide/collection-management.html#reload",
-  "description": "Reloads a collection so new configuration changes can take effect and be available for use by the system.",
-  "type" : "object",
-  "properties":{
-    "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."
-    }
-  }
-}
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
index a57d859..25d8264 100644
--- 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
@@ -38,15 +38,4 @@ public class TestV1toV2ApiMapper extends SolrTestCase {
     assertEquals("shard1", Utils.getObjectByPath(m,true,"/add-replica/shard"));
     assertEquals("NRT", Utils.getObjectByPath(m,true,"/add-replica/type"));
   }
-
-  @Test
-  // commented out on: 24-Dec-2018   @BadApple(bugUrl="https://issues.apache.org/jira/browse/SOLR-12028") // added 20-Sep-2018
-  public void testSetCollectionProperty() throws IOException {
-    CollectionAdminRequest.CollectionProp collectionProp = CollectionAdminRequest.setCollectionProperty("mycoll", "prop", "value");
-    V2Request v2r = V1toV2ApiMapper.convert(collectionProp).build();
-    Map<?,?> m = (Map<?,?>) Utils.fromJSON(ContentStreamBase.create(new BinaryRequestWriter(), v2r).getStream());
-    assertEquals("/c/mycoll", v2r.getPath());
-    assertEquals("prop", Utils.getObjectByPath(m,true,"/set-collection-property/name"));
-    assertEquals("value", Utils.getObjectByPath(m,true,"/set-collection-property/value"));
-  }
 }
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 66aa39f..5f7c2c7 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.Commands");
     checkSchema("collections.collection.shards.Commands");
     checkSchema("collections.collection.shards.shard.Commands");
     checkSchema("cores.Commands");