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 9b1f8dd  SOLR-15792: Rewrite shardname APIs using annotation framework 
(#415)
9b1f8dd is described below

commit 9b1f8ddab33ebee870780918554699980bd635ad
Author: Jason Gerlowski <[email protected]>
AuthorDate: Mon Nov 15 22:00:29 2021 -0500

    SOLR-15792: Rewrite shardname APIs using annotation framework (#415)
    
    V2 APIs implementations are in the process of migrating from a JSON file
    'apispec' framework, to an annotation-based framework.
    
    This commit makes this change for Solr's
    /v2/collections/<collName>/shards/<shardName> APIs: delete-shard,
    force-leader, and sync-shard.
---
 .../solr/handler/admin/api/DeleteShardAPI.java     |  63 ++++
 .../solr/handler/admin/api/ForceLeaderAPI.java     |  68 ++++
 .../solr/handler/admin/api/SyncShardAPI.java       |  68 ++++
 .../org/apache/solr/handler/api/ApiRegistrar.java  |   3 +
 .../handler/admin/api/V2ShardsAPIMappingTest.java  | 385 ++++++++++++---------
 .../client/solrj/request/CollectionApiMapping.java |  24 +-
 .../solrj/request/beans/ForceLeaderPayload.java    |  25 ++
 .../solrj/request/beans/SyncShardPayload.java      |  25 ++
 ...llections.collection.shards.shard.Commands.json |  24 --
 ...collections.collection.shards.shard.delete.json |  27 --
 .../apache/solr/common/util/JsonValidatorTest.java |   5 +-
 11 files changed, 471 insertions(+), 246 deletions(-)

diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteShardAPI.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteShardAPI.java
new file mode 100644
index 0000000..4ab541c
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteShardAPI.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.handler.admin.api;
+
+import com.google.common.collect.Maps;
+import org.apache.solr.api.EndPoint;
+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 java.util.Map;
+
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
+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.CoreAdminParams.SHARD;
+import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+/**
+ * V2 API for deleting a particular shard from its collection.
+ *
+ * This API (DELETE /v2/collections/collectionName/shards/shardName) is 
analogous to the v1
+ * /admin/collections?action=DELETESHARD command.
+ */
+public class DeleteShardAPI {
+    private final CollectionsHandler collectionsHandler;
+
+    public DeleteShardAPI(CollectionsHandler collectionsHandler) {
+        this.collectionsHandler = collectionsHandler;
+    }
+
+    @EndPoint(
+            path = {"/c/{collection}/shards/{shard}", 
"/collections/{collection}/shards/{shard}"},
+            method = DELETE,
+            permission = COLL_EDIT_PERM)
+    public void deleteShard(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
+        final Map<String, String> pathParams = req.getPathTemplateValues();
+
+        final Map<String, Object> addedV1Params = Maps.newHashMap();
+        addedV1Params.put(ACTION, 
CollectionParams.CollectionAction.DELETESHARD.toLower());
+        addedV1Params.put(COLLECTION, pathParams.get(COLLECTION));
+        addedV1Params.put(SHARD, pathParams.get(SHARD));
+
+        collectionsHandler.handleRequestBody(wrapParams(req, addedV1Params), 
rsp);
+    }
+}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/ForceLeaderAPI.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/ForceLeaderAPI.java
new file mode 100644
index 0000000..09d1771
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ForceLeaderAPI.java
@@ -0,0 +1,68 @@
+/*
+ * 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.api.Command;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.api.PayloadObj;
+import org.apache.solr.client.solrj.request.beans.ForceLeaderPayload;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.handler.admin.CollectionsHandler;
+
+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.CollectionParams.ACTION;
+import static org.apache.solr.common.params.CoreAdminParams.SHARD;
+import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+/**
+ * V2 API for triggering a leader election on a particular collection and 
shard.
+ *
+ * This API (POST /v2/collections/collectionName/shards/shardName 
{'force-leader': {}}) is analogous to the v1
+ * /admin/collections?action=FORCELEADER command.
+ *
+ * @see ForceLeaderPayload
+ */
+@EndPoint(
+        path = {"/c/{collection}/shards/{shard}", 
"/collections/{collection}/shards/{shard}"},
+        method = POST,
+        permission = COLL_EDIT_PERM)
+public class ForceLeaderAPI {
+    private static final String V2_FORCE_LEADER_CMD = "force-leader";
+
+    private final CollectionsHandler collectionsHandler;
+
+    public ForceLeaderAPI(CollectionsHandler collectionsHandler) {
+        this.collectionsHandler = collectionsHandler;
+    }
+
+    @Command(name = V2_FORCE_LEADER_CMD)
+    public void forceLeader(PayloadObj<ForceLeaderPayload> obj) throws 
Exception {
+        final Map<String, Object> addedV1Params = Maps.newHashMap();
+        final Map<String, String> pathParams = 
obj.getRequest().getPathTemplateValues();
+        addedV1Params.put(ACTION, 
CollectionParams.CollectionAction.FORCELEADER.toLower());
+        addedV1Params.put(COLLECTION, pathParams.get(COLLECTION));
+        addedV1Params.put(SHARD, pathParams.get(SHARD));
+
+        collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), 
addedV1Params), obj.getResponse());
+    }
+}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/SyncShardAPI.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/SyncShardAPI.java
new file mode 100644
index 0000000..94a1953
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/SyncShardAPI.java
@@ -0,0 +1,68 @@
+/*
+ * 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.api.Command;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.api.PayloadObj;
+import org.apache.solr.client.solrj.request.beans.SyncShardPayload;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.handler.admin.CollectionsHandler;
+
+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.CollectionParams.ACTION;
+import static org.apache.solr.common.params.CoreAdminParams.SHARD;
+import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+/**
+ * V2 API for triggering a shard-sync operation within a particular collection 
and shard.
+ *
+ * This API (POST /v2/collections/collectionName/shards/shardName 
{'sync-shard': {}}) is analogous to the v1
+ * /admin/collections?action=SYNCSHARD command.
+ *
+ * @see SyncShardPayload
+ */
+@EndPoint(
+        path = {"/c/{collection}/shards/{shard}", 
"/collections/{collection}/shards/{shard}"},
+        method = POST,
+        permission = COLL_EDIT_PERM)
+public class SyncShardAPI {
+    private static final String V2_SYNC_SHARD_CMD = "sync-shard";
+
+    private final CollectionsHandler collectionsHandler;
+
+    public SyncShardAPI(CollectionsHandler collectionsHandler) {
+        this.collectionsHandler = collectionsHandler;
+    }
+
+    @Command(name = V2_SYNC_SHARD_CMD)
+    public void syncShard(PayloadObj<SyncShardPayload> obj) throws Exception {
+        final Map<String, Object> addedV1Params = Maps.newHashMap();
+        final Map<String, String> pathParams = 
obj.getRequest().getPathTemplateValues();
+        addedV1Params.put(ACTION, 
CollectionParams.CollectionAction.SYNCSHARD.toLower());
+        addedV1Params.put(COLLECTION, pathParams.get(COLLECTION));
+        addedV1Params.put(SHARD, pathParams.get(SHARD));
+
+        collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), 
addedV1Params), obj.getResponse());
+    }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java 
b/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java
index e699bd8..0da03a6 100644
--- a/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java
+++ b/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java
@@ -48,5 +48,8 @@ public class ApiRegistrar {
     apiBag.registerObject(new SplitShardAPI(collectionsHandler));
     apiBag.registerObject(new CreateShardAPI(collectionsHandler));
     apiBag.registerObject(new AddReplicaAPI(collectionsHandler));
+    apiBag.registerObject(new DeleteShardAPI(collectionsHandler));
+    apiBag.registerObject(new SyncShardAPI(collectionsHandler));
+    apiBag.registerObject(new ForceLeaderAPI(collectionsHandler));
   }
 }
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
index 769fc9e..b7da82b 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
@@ -6,7 +6,11 @@
  * (the "License"); you may not use this file except in compliance with
  * the License.  You may obtain a copy of the License at
  *
+<<<<<<< HEAD
+ *     http://www.apache.org/licenses/LICENSE-2.0
+=======
  *      http://www.apache.org/licenses/LICENSE-2.0
+>>>>>>> main
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,18 +21,12 @@
 
 package org.apache.solr.handler.admin.api;
 
-import com.google.common.collect.Maps;
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.api.Api;
 import org.apache.solr.api.ApiBag;
-import org.apache.solr.common.params.CollectionAdminParams;
-import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.common.params.CoreAdminParams;
-import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.params.*;
 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;
@@ -43,182 +41,225 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
 import static org.apache.solr.common.cloud.ZkStateReader.*;
-import static 
org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
-import static 
org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CollectionAdminParams.*;
 import static org.apache.solr.common.params.CommonAdminParams.*;
 import static org.apache.solr.common.params.CommonParams.*;
+import static org.apache.solr.common.params.CoreAdminParams.SHARD;
+import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
 /**
- * Unit tests for the V2 APIs found in {@link 
org.apache.solr.handler.admin.api} that use the /c/{collection}/shards path.
- *
- * This test bears many similarities to {@link TestCollectionAPIs} which 
appears to test the mappings indirectly by
- * checking message sent to the ZK overseer (which is similar, but not 
identical to the v1 param list).  If there's no
- * particular benefit to testing the mappings in this way (there very well may 
be), then we should combine these two
- * test classes at some point in the future using the simpler approach here.
+ * Unit tests for the V2 APIs that use the /c/{collection}/shards or 
/c/{collection}/shards/{shard} paths.
  *
  * Note that the V2 requests made by these tests are not necessarily 
semantically valid.  They shouldn't be taken as
  * examples. In several instances, mutually exclusive JSON parameters are 
provided.  This is done to exercise conversion
  * of all parameters, even if particular combinations are never expected in 
the same request.
  */
-public class V2ShardsAPIMappingTest extends SolrTestCaseJ4 {
-  private ApiBag apiBag;
-
-  private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
-  private CollectionsHandler mockCollectionsHandler;
-
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
-  }
-
-  @Before
-  public void setupApiBag() throws Exception {
-    mockCollectionsHandler = mock(CollectionsHandler.class);
-    queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
-
-    apiBag = new ApiBag(false);
-    ApiRegistrar.registerShardApis(apiBag, mockCollectionsHandler);
-  }
-
-  @Test
-  public void testSplitShardAllProperties() throws Exception {
-    final SolrParams v1Params = 
captureConvertedV1Params("/collections/collName/shards", "POST",
-            "{ 'split': {" +
-                    "'shard': 'shard1', " +
-                    "'ranges': 'someRangeValues', " +
-                    "'splitKey': 'someSplitKey', " +
-                    "'numSubShards': 123, " +
-                    "'splitFuzz': 'some_fuzz_value', " +
-                    "'timing': true, " +
-                    "'splitByPrefix': true, " +
-                    "'followAliases': true, " +
-                    "'splitMethod': 'rewrite', " +
-                    "'async': 'some_async_id', " +
-                    "'waitForFinalState': true, " +
-                    "'coreProperties': {" +
-                    "    'foo': 'foo1', " +
-                    "    'bar': 'bar1', " +
-                    "}}}");
-
-    assertEquals(CollectionParams.CollectionAction.SPLITSHARD.lowerName, 
v1Params.get(ACTION));
-    assertEquals("collName", v1Params.get(CollectionAdminParams.COLLECTION));
-    assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
-    assertEquals("someRangeValues", v1Params.get(CoreAdminParams.RANGES));
-    assertEquals("someSplitKey", v1Params.get(SPLIT_KEY));
-    assertEquals(123, v1Params.getPrimitiveInt(NUM_SUB_SHARDS));
-    assertEquals("some_fuzz_value", v1Params.get(SPLIT_FUZZ));
-    assertEquals(true, v1Params.getPrimitiveBool(TIMING));
-    assertEquals(true, v1Params.getPrimitiveBool(SPLIT_BY_PREFIX));
-    assertEquals(true, v1Params.getPrimitiveBool(FOLLOW_ALIASES));
-    assertEquals("rewrite", v1Params.get(SPLIT_METHOD));
-    assertEquals("some_async_id", v1Params.get(ASYNC));
-    assertEquals(true, v1Params.getPrimitiveBool(WAIT_FOR_FINAL_STATE));
-    assertEquals("foo1", v1Params.get("property.foo"));
-    assertEquals("bar1", v1Params.get("property.bar"));
-  }
-
-  @Test
-  public void testCreateShardAllProperties() throws Exception {
-    final SolrParams v1Params = 
captureConvertedV1Params("/collections/collName/shards", "POST",
-            "{ 'create': {" +
-                    "'shard': 'shard1', " +
-                    "'nodeSet': ['foo', 'bar', 'baz'], " +
-                    "'followAliases': true, " +
-                    "'async': 'some_async_id', " +
-                    "'waitForFinalState': true, " +
-                    "'replicationFactor': 123, " +
-                    "'nrtReplicas': 456, " +
-                    "'tlogReplicas': 789, " +
-                    "'pullReplicas': 101, " +
-                    "'coreProperties': {" +
-                    "    'foo': 'foo1', " +
-                    "    'bar': 'bar1', " +
-                    "}}}");
-
-    assertEquals(CollectionParams.CollectionAction.CREATESHARD.lowerName, 
v1Params.get(ACTION));
-    assertEquals("collName", v1Params.get(CollectionAdminParams.COLLECTION));
-    assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
-    assertEquals("foo,bar,baz", v1Params.get(CREATE_NODE_SET_PARAM));
-    assertEquals(true, v1Params.getPrimitiveBool(FOLLOW_ALIASES));
-    assertEquals("some_async_id", v1Params.get(ASYNC));
-    assertEquals(true, v1Params.getPrimitiveBool(WAIT_FOR_FINAL_STATE));
-    assertEquals(123, v1Params.getPrimitiveInt(REPLICATION_FACTOR));
-    assertEquals(456, v1Params.getPrimitiveInt(NRT_REPLICAS));
-    assertEquals(789, v1Params.getPrimitiveInt(TLOG_REPLICAS));
-    assertEquals(101, v1Params.getPrimitiveInt(PULL_REPLICAS));
-    assertEquals("foo1", v1Params.get("property.foo"));
-    assertEquals("bar1", v1Params.get("property.bar"));
-  }
-
-  @Test
-  public void testAddReplicaAllProperties() throws Exception {
-    final SolrParams v1Params = 
captureConvertedV1Params("/collections/collName/shards", "POST",
-            "{ 'add-replica': {" +
-                    "'shard': 'shard1', " +
-                    "'_route_': 'someRouteValue', " +
-                    "'node': 'someNodeValue', " +
-                    "'name': 'someName', " +
-                    "'instanceDir': 'dir1', " +
-                    "'dataDir': 'dir2', " +
-                    "'ulogDir': 'dir3', " +
-                    "'createNodeSet': ['foo', 'bar', 'baz'], " +
-                    "'followAliases': true, " +
-                    "'async': 'some_async_id', " +
-                    "'waitForFinalState': true, " +
-                    "'skipNodeAssignment': true, " +
-                    "'type': 'tlog', " +
-                    "'coreProperties': {" +
-                    "    'foo': 'foo1', " +
-                    "    'bar': 'bar1', " +
-                    "}}}");
-
-    assertEquals(CollectionParams.CollectionAction.ADDREPLICA.lowerName, 
v1Params.get(ACTION));
-    assertEquals("collName", v1Params.get(CollectionAdminParams.COLLECTION));
-    assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
-    assertEquals("someRouteValue", v1Params.get("_route_"));
-    assertEquals("someNodeValue", v1Params.get("node"));
-    assertEquals("foo,bar,baz", v1Params.get(CREATE_NODE_SET_PARAM));
-    assertEquals("someName", v1Params.get(NAME));
-    assertEquals("dir1", v1Params.get("instanceDir"));
-    assertEquals("dir2", v1Params.get("dataDir"));
-    assertEquals("dir3", v1Params.get("ulogDir"));
-    assertEquals(true, v1Params.getPrimitiveBool(FOLLOW_ALIASES));
-    assertEquals("some_async_id", v1Params.get(ASYNC));
-    assertEquals(true, v1Params.getPrimitiveBool(WAIT_FOR_FINAL_STATE));
-    assertEquals(true, v1Params.getPrimitiveBool("skipNodeAssignment"));
-    assertEquals("tlog", v1Params.get("type"));
-    assertEquals("foo1", v1Params.get("property.foo"));
-    assertEquals("bar1", v1Params.get("property.bar"));
-  }
-
-  private SolrParams captureConvertedV1Params(String path, String method, 
String v2RequestBody) throws Exception {
-    final HashMap<String, String> parts = new HashMap<>();
-    final Api api = apiBag.lookup(path, method, parts);
-    final SolrQueryResponse rsp = new SolrQueryResponse();
-    final LocalSolrQueryRequest req = new LocalSolrQueryRequest(null, 
Maps.newHashMap()) {
-      @Override
-      public List<CommandOperation> getCommands(boolean validateInput) {
-        if (v2RequestBody == null) return Collections.emptyList();
-        return ApiBag.getCommandOperations(new 
ContentStreamBase.StringStream(v2RequestBody), api.getCommandSchema(), true);
-      }
-
-      @Override
-      public Map<String, String> getPathTemplateValues() {
-        return parts;
-      }
-
-      @Override
-      public String getHttpMethod() {
-        return method;
-      }
-    };
-
-    api.call(req, rsp);
-    
verify(mockCollectionsHandler).handleRequestBody(queryRequestCaptor.capture(), 
any());
-    return queryRequestCaptor.getValue().getParams();
-  }
+public class V2ShardsAPIMappingTest {
+    private ApiBag apiBag;
+
+    private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
+    private CollectionsHandler mockCollectionsHandler;
+
+    @BeforeClass
+    public static void ensureWorkingMockito() {
+        assumeWorkingMockito();
+    }
+
+    @Before
+    public void setupApiBag() throws Exception {
+        mockCollectionsHandler = mock(CollectionsHandler.class);
+        queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
+
+        apiBag = new ApiBag(false);
+        ApiRegistrar.registerShardApis(apiBag, mockCollectionsHandler);
+    }
+
+    @Test
+    public void testForceLeaderAllProperties() throws Exception {
+        final SolrParams v1Params = 
captureConvertedV1Params("/collections/collName/shards/shardName", "POST",
+                "{ 'force-leader': {}}");
+
+        assertEquals(CollectionParams.CollectionAction.FORCELEADER.lowerName, 
v1Params.get(ACTION));
+        assertEquals("collName", v1Params.get(COLLECTION));
+        assertEquals("shardName", v1Params.get(SHARD));
+    }
+
+    @Test
+    public void testSyncShardAllProperties() throws Exception {
+        final SolrParams v1Params = 
captureConvertedV1Params("/collections/collName/shards/shardName", "POST",
+                "{ 'sync-shard': {}}");
+
+        assertEquals(CollectionParams.CollectionAction.SYNCSHARD.lowerName, 
v1Params.get(ACTION));
+        assertEquals("collName", v1Params.get(COLLECTION));
+        assertEquals("shardName", v1Params.get(SHARD));
+    }
+
+    @Test
+    public void testDeleteShardAllProperties() throws Exception {
+        final ModifiableSolrParams v2QueryParams = new ModifiableSolrParams();
+        v2QueryParams.add("deleteIndex", "true");
+        v2QueryParams.add("deleteDataDir", "true");
+        v2QueryParams.add("deleteInstanceDir", "true");
+        v2QueryParams.add("followAliases", "true");
+        final SolrParams v1Params = 
captureConvertedV1Params("/collections/collName/shards/shardName", "DELETE", 
v2QueryParams);
+
+        assertEquals(CollectionParams.CollectionAction.DELETESHARD.lowerName, 
v1Params.get(ACTION));
+        assertEquals("collName", v1Params.get(COLLECTION));
+        assertEquals("shardName", v1Params.get(SHARD));
+        assertEquals("true", v1Params.get("deleteIndex"));
+        assertEquals("true", v1Params.get("deleteDataDir"));
+        assertEquals("true", v1Params.get("deleteInstanceDir"));
+        assertEquals("true", v1Params.get("followAliases"));
+    }
+
+    @Test
+    public void testSplitShardAllProperties() throws Exception {
+        final SolrParams v1Params = 
captureConvertedV1Params("/collections/collName/shards", "POST",
+                "{ 'split': {" +
+                        "'shard': 'shard1', " +
+                        "'ranges': 'someRangeValues', " +
+                        "'splitKey': 'someSplitKey', " +
+                        "'numSubShards': 123, " +
+                        "'splitFuzz': 'some_fuzz_value', " +
+                        "'timing': true, " +
+                        "'splitByPrefix': true, " +
+                        "'followAliases': true, " +
+                        "'splitMethod': 'rewrite', " +
+                        "'async': 'some_async_id', " +
+                        "'waitForFinalState': true, " +
+                        "'coreProperties': {" +
+                        "    'foo': 'foo1', " +
+                        "    'bar': 'bar1', " +
+                        "}}}");
+
+        assertEquals(CollectionParams.CollectionAction.SPLITSHARD.lowerName, 
v1Params.get(ACTION));
+        assertEquals("collName", 
v1Params.get(CollectionAdminParams.COLLECTION));
+        assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
+        assertEquals("someRangeValues", v1Params.get(CoreAdminParams.RANGES));
+        assertEquals("someSplitKey", v1Params.get(SPLIT_KEY));
+        assertEquals(123, v1Params.getPrimitiveInt(NUM_SUB_SHARDS));
+        assertEquals("some_fuzz_value", v1Params.get(SPLIT_FUZZ));
+        assertEquals(true, v1Params.getPrimitiveBool(TIMING));
+        assertEquals(true, v1Params.getPrimitiveBool(SPLIT_BY_PREFIX));
+        assertEquals(true, v1Params.getPrimitiveBool(FOLLOW_ALIASES));
+        assertEquals("rewrite", v1Params.get(SPLIT_METHOD));
+        assertEquals("some_async_id", v1Params.get(ASYNC));
+        assertEquals(true, v1Params.getPrimitiveBool(WAIT_FOR_FINAL_STATE));
+        assertEquals("foo1", v1Params.get("property.foo"));
+        assertEquals("bar1", v1Params.get("property.bar"));
+    }
+
+    @Test
+    public void testCreateShardAllProperties() throws Exception {
+        final SolrParams v1Params = 
captureConvertedV1Params("/collections/collName/shards", "POST",
+                "{ 'create': {" +
+                        "'shard': 'shard1', " +
+                        "'nodeSet': ['foo', 'bar', 'baz'], " +
+                        "'followAliases': true, " +
+                        "'async': 'some_async_id', " +
+                        "'waitForFinalState': true, " +
+                        "'replicationFactor': 123, " +
+                        "'nrtReplicas': 456, " +
+                        "'tlogReplicas': 789, " +
+                        "'pullReplicas': 101, " +
+                        "'coreProperties': {" +
+                        "    'foo': 'foo1', " +
+                        "    'bar': 'bar1', " +
+                        "}}}");
+
+        assertEquals(CollectionParams.CollectionAction.CREATESHARD.lowerName, 
v1Params.get(ACTION));
+        assertEquals("collName", 
v1Params.get(CollectionAdminParams.COLLECTION));
+        assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
+        assertEquals("foo,bar,baz", v1Params.get(CREATE_NODE_SET_PARAM));
+        assertEquals(true, v1Params.getPrimitiveBool(FOLLOW_ALIASES));
+        assertEquals("some_async_id", v1Params.get(ASYNC));
+        assertEquals(true, v1Params.getPrimitiveBool(WAIT_FOR_FINAL_STATE));
+        assertEquals(123, v1Params.getPrimitiveInt(REPLICATION_FACTOR));
+        assertEquals(456, v1Params.getPrimitiveInt(NRT_REPLICAS));
+        assertEquals(789, v1Params.getPrimitiveInt(TLOG_REPLICAS));
+        assertEquals(101, v1Params.getPrimitiveInt(PULL_REPLICAS));
+        assertEquals("foo1", v1Params.get("property.foo"));
+        assertEquals("bar1", v1Params.get("property.bar"));
+    }
+
+    @Test
+    public void testAddReplicaAllProperties() throws Exception {
+        final SolrParams v1Params = 
captureConvertedV1Params("/collections/collName/shards", "POST",
+                "{ 'add-replica': {" +
+                        "'shard': 'shard1', " +
+                        "'_route_': 'someRouteValue', " +
+                        "'node': 'someNodeValue', " +
+                        "'name': 'someName', " +
+                        "'instanceDir': 'dir1', " +
+                        "'dataDir': 'dir2', " +
+                        "'ulogDir': 'dir3', " +
+                        "'createNodeSet': ['foo', 'bar', 'baz'], " +
+                        "'followAliases': true, " +
+                        "'async': 'some_async_id', " +
+                        "'waitForFinalState': true, " +
+                        "'skipNodeAssignment': true, " +
+                        "'type': 'tlog', " +
+                        "'coreProperties': {" +
+                        "    'foo': 'foo1', " +
+                        "    'bar': 'bar1', " +
+                        "}}}");
+
+        assertEquals(CollectionParams.CollectionAction.ADDREPLICA.lowerName, 
v1Params.get(ACTION));
+        assertEquals("collName", 
v1Params.get(CollectionAdminParams.COLLECTION));
+        assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
+        assertEquals("someRouteValue", v1Params.get("_route_"));
+        assertEquals("someNodeValue", v1Params.get("node"));
+        assertEquals("foo,bar,baz", v1Params.get(CREATE_NODE_SET_PARAM));
+        assertEquals("someName", v1Params.get(NAME));
+        assertEquals("dir1", v1Params.get("instanceDir"));
+        assertEquals("dir2", v1Params.get("dataDir"));
+        assertEquals("dir3", v1Params.get("ulogDir"));
+        assertEquals(true, v1Params.getPrimitiveBool(FOLLOW_ALIASES));
+        assertEquals("some_async_id", v1Params.get(ASYNC));
+        assertEquals(true, v1Params.getPrimitiveBool(WAIT_FOR_FINAL_STATE));
+        assertEquals(true, v1Params.getPrimitiveBool("skipNodeAssignment"));
+        assertEquals("tlog", v1Params.get("type"));
+        assertEquals("foo1", v1Params.get("property.foo"));
+        assertEquals("bar1", v1Params.get("property.bar"));
+    }
+
+    private SolrParams captureConvertedV1Params(String path, String method, 
SolrParams queryParams) throws Exception {
+        return captureConvertedV1Params(path, method, queryParams, null);
+    }
+
+    private SolrParams captureConvertedV1Params(String path, String method, 
String v2RequestBody) throws Exception {
+        return captureConvertedV1Params(path, method, new 
ModifiableSolrParams(), v2RequestBody);
+    }
+
+    private SolrParams captureConvertedV1Params(String path, String method, 
SolrParams queryParams, 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, 
queryParams) {
+            @Override
+            public List<CommandOperation> getCommands(boolean validateInput) {
+                if (v2RequestBody == null) return Collections.emptyList();
+                return ApiBag.getCommandOperations(new 
ContentStreamBase.StringStream(v2RequestBody), api.getCommandSchema(), true);
+            }
+
+            @Override
+            public Map<String, String> getPathTemplateValues() {
+                return parts;
+            }
+
+            @Override
+            public String getHttpMethod() {
+                return method;
+            }
+        };
+
+        api.call(req, rsp);
+        
verify(mockCollectionsHandler).handleRequestBody(queryRequestCaptor.capture(), 
any());
+        return queryRequestCaptor.getValue().getParams();
+    }
 }
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java
index bd9767a..3981c08 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
@@ -23,19 +23,12 @@ 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;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Stream;
 
 import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
-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.*;
+import static 
org.apache.solr.client.solrj.request.CollectionApiMapping.EndPoint.PER_COLLECTION_PER_SHARD_PER_REPLICA_DELETE;
+import static 
org.apache.solr.common.params.CollectionParams.CollectionAction.DELETEREPLICA;
 
 /**
  * Stores the mapping of v1 API parameters to v2 API parameters
@@ -44,14 +37,7 @@ import static 
org.apache.solr.common.params.CollectionParams.CollectionAction.*;
 public class CollectionApiMapping {
 
   public enum Meta implements CommandMeta {
-    DELETE_SHARD(PER_COLLECTION_PER_SHARD_DELETE, DELETE, DELETESHARD),
-    DELETE_REPLICA(PER_COLLECTION_PER_SHARD_PER_REPLICA_DELETE, DELETE, 
DELETEREPLICA),
-    SYNC_SHARD(PER_COLLECTION_PER_SHARD_COMMANDS,
-        POST,
-        CollectionAction.SYNCSHARD,
-        "synch-shard",
-        null),
-    FORCE_LEADER(PER_COLLECTION_PER_SHARD_COMMANDS, POST, 
CollectionAction.FORCELEADER, "force-leader", null);
+    DELETE_REPLICA(PER_COLLECTION_PER_SHARD_PER_REPLICA_DELETE, DELETE, 
DELETEREPLICA);
 
     public final String commandName;
     public final EndPoint endPoint;
@@ -170,8 +156,6 @@ public class CollectionApiMapping {
   }
 
   public enum EndPoint implements V2EndPoint {
-    
PER_COLLECTION_PER_SHARD_COMMANDS("collections.collection.shards.shard.Commands"),
-    
PER_COLLECTION_PER_SHARD_DELETE("collections.collection.shards.shard.delete"),
     
PER_COLLECTION_PER_SHARD_PER_REPLICA_DELETE("collections.collection.shards.shard.replica.delete");
     final String specName;
 
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/ForceLeaderPayload.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/ForceLeaderPayload.java
new file mode 100644
index 0000000..6c89e14
--- /dev/null
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/ForceLeaderPayload.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.util.ReflectMapWriter;
+
+/**
+ * Empty payload for the POST /v2/collections/collName/shards/shardName 
{"force-leader": {}} API.
+ */
+public class ForceLeaderPayload implements ReflectMapWriter { }
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SyncShardPayload.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SyncShardPayload.java
new file mode 100644
index 0000000..3e947da
--- /dev/null
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SyncShardPayload.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.util.ReflectMapWriter;
+
+/**
+ * Empty payload for the /v2/collections/collName/shards/shardName 
{"sync-shard": {}} API.
+ */
+public class SyncShardPayload implements ReflectMapWriter { }
diff --git 
a/solr/solrj/src/resources/apispec/collections.collection.shards.shard.Commands.json
 
b/solr/solrj/src/resources/apispec/collections.collection.shards.shard.Commands.json
deleted file mode 100644
index 2f8485f..0000000
--- 
a/solr/solrj/src/resources/apispec/collections.collection.shards.shard.Commands.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
-  "documentation": 
"https://lucene.apache.org/solr/guide/shard-management.html";,
-  "description": "Commands to force leader election and synchronize shards.",
-  "methods": [
-    "POST",
-    "DELETE"
-  ],
-  "url": {
-    "paths": [
-      "/collections/{collection}/shards/{shard}",
-      "/c/{collection}/shards/{shard}"
-    ]
-  },
-  "commands": {
-    "force-leader": {
-      "documentation": 
"https://lucene.apache.org/solr/guide/shard-management.html#forceleader";,
-      "description": "In the unlikely event of a shard losing its leader, this 
command can be invoked to force the election of a new leader",
-      "type": "object"
-    },
-    "sync-shard": {
-      "type": "object"
-    }
-  }
-}
diff --git 
a/solr/solrj/src/resources/apispec/collections.collection.shards.shard.delete.json
 
b/solr/solrj/src/resources/apispec/collections.collection.shards.shard.delete.json
deleted file mode 100644
index 50c1e3b..0000000
--- 
a/solr/solrj/src/resources/apispec/collections.collection.shards.shard.delete.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-  "documentation": 
"https://lucene.apache.org/solr/guide/shard-management.html#deleteshard";,
-  "description": "Deletes a shard by unloading all replicas of the shard, 
removing it from the collection's state.json, and by default deleting the 
instanceDir and dataDir. Only inactive shards or those which have no range for 
custom sharding will be deleted.",
-  "methods": [
-    "DELETE"
-  ],
-  "url": {
-    "paths": [
-      "/collections/{collection}/shards/{shard}",
-      "/c/{collection}/shards/{shard}"
-    ],
-    "params":{
-      "deleteInstanceDir":{
-        "type": "boolean",
-        "description":"By default Solr will delete the entire instanceDir of 
each replica that is deleted. Set this to false to prevent the instance 
directory from being deleted."
-      },
-      "deleteDataDir":{
-        "type":"boolean",
-        "description":"y default Solr will delete the dataDir of each replica 
that is deleted. Set this to false to prevent the data directory from being 
deleted."
-      },
-      "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."
-      }
-    }
-  }
-}
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 184ea47..73f1ef1 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
@@ -17,18 +17,17 @@
 package org.apache.solr.common.util;
 
 
+import org.apache.solr.SolrTestCaseJ4;
+
 import java.util.List;
 import java.util.Map;
 
-import org.apache.solr.SolrTestCaseJ4;
-
 import static org.apache.solr.common.util.Utils.toJSONString;
 import static org.apache.solr.common.util.ValidatingJsonMap.NOT_NULL;
 
 public class JsonValidatorTest extends SolrTestCaseJ4  {
 
   public void testSchema() {
-    checkSchema("collections.collection.shards.shard.Commands");
     checkSchema("cores.Commands");
     checkSchema("cores.core.Commands");
     checkSchema("node.Commands");

Reply via email to