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 <[email protected]>
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");

Reply via email to