You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2023/06/06 14:20:42 UTC

[solr] branch branch_9x updated (95f2ea967ca -> a0cd45394f3)

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

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


    from 95f2ea967ca SOLR-16812: Support CBOR format for update/query (#1655)
     new 64b89918fcf SOLR-16395 JAX-RS conversion for /schema, uniqueKey, similarity and version (#1649)
     new a0cd45394f3 SOLR-16392: Tweak v2 CREATESHARD to be more REST-ful (#1671)

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


Summary of changes:
 solr/CHANGES.txt                                   |   9 +
 .../org/apache/solr/handler/SchemaHandler.java     |  41 ++--
 .../solr/handler/admin/CollectionsHandler.java     |  41 +---
 .../solr/handler/admin/api/CreateShardAPI.java     | 220 +++++++++++++++++----
 .../solr/handler/admin/api/GetSchemaAPI.java       | 120 +++++++++++
 .../solr/handler/admin/api/SchemaInfoAPI.java      |  48 -----
 .../handler/admin/api/SchemaSimilarityAPI.java     |  48 -----
 .../solr/handler/admin/api/SchemaUniqueKeyAPI.java |  48 -----
 .../solr/handler/admin/api/SchemaVersionAPI.java   |  48 -----
 .../org/apache/solr/jersey/InjectionFactories.java |  19 ++
 .../org/apache/solr/jersey/JerseyApplications.java |   4 +
 .../solr/handler/admin/TestApiFramework.java       |   1 -
 .../solr/handler/admin/api/CreateShardAPITest.java | 172 ++++++++++++++++
 .../solr/handler/admin/api/GetSchemaAPITest.java   |  93 +++++++++
 .../handler/admin/api/V2SchemaAPIMappingTest.java  |   8 -
 .../handler/admin/api/V2ShardsAPIMappingTest.java  |  41 ----
 .../deployment-guide/pages/shard-management.adoc   |   6 +-
 17 files changed, 630 insertions(+), 337 deletions(-)
 create mode 100644 solr/core/src/java/org/apache/solr/handler/admin/api/GetSchemaAPI.java
 delete mode 100644 solr/core/src/java/org/apache/solr/handler/admin/api/SchemaInfoAPI.java
 delete mode 100644 solr/core/src/java/org/apache/solr/handler/admin/api/SchemaSimilarityAPI.java
 delete mode 100644 solr/core/src/java/org/apache/solr/handler/admin/api/SchemaUniqueKeyAPI.java
 delete mode 100644 solr/core/src/java/org/apache/solr/handler/admin/api/SchemaVersionAPI.java
 create mode 100644 solr/core/src/test/org/apache/solr/handler/admin/api/CreateShardAPITest.java
 create mode 100644 solr/core/src/test/org/apache/solr/handler/admin/api/GetSchemaAPITest.java


[solr] 01/02: SOLR-16395 JAX-RS conversion for /schema, uniqueKey, similarity and version (#1649)

Posted by ge...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 64b89918fcfe6f0eb28af077054f9cc84bfc3d85
Author: bszabo97 <79...@users.noreply.github.com>
AuthorDate: Tue May 30 17:28:42 2023 +0200

    SOLR-16395 JAX-RS conversion for /schema, uniqueKey, similarity and version (#1649)
    
    Migrates several v2 `GET /schema` APIs from the legacy annotation framework
    to JAX-RS, including:
    
    - `/schema` - fetch the whole schema
    - `/schema/uniquekey` - fetch the name of the schemas unique key
    - `/schema/similarity` - fetch the similarity implementation
    - `/schema/version` - fetch the schema version
    
    This change doesn't modify the APIs themselves, so users should remain unaffected.
    
    ---------
    
    Co-authored-by: Jason Gerlowski <ge...@apache.org>
---
 .../org/apache/solr/handler/SchemaHandler.java     |  41 +++----
 .../solr/handler/admin/api/GetSchemaAPI.java       | 120 +++++++++++++++++++++
 .../solr/handler/admin/api/SchemaInfoAPI.java      |  48 ---------
 .../handler/admin/api/SchemaSimilarityAPI.java     |  48 ---------
 .../solr/handler/admin/api/SchemaUniqueKeyAPI.java |  48 ---------
 .../solr/handler/admin/api/SchemaVersionAPI.java   |  48 ---------
 .../org/apache/solr/jersey/InjectionFactories.java |  19 ++++
 .../org/apache/solr/jersey/JerseyApplications.java |   4 +
 .../solr/handler/admin/TestApiFramework.java       |   1 -
 .../solr/handler/admin/api/GetSchemaAPITest.java   |  93 ++++++++++++++++
 .../handler/admin/api/V2SchemaAPIMappingTest.java  |   8 --
 11 files changed, 258 insertions(+), 220 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java b/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java
index da216ed6510..89dc0ffd3de 100644
--- a/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java
@@ -47,19 +47,16 @@ import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.core.PluginInfo;
 import org.apache.solr.core.SolrCore;
+import org.apache.solr.handler.admin.api.GetSchemaAPI;
 import org.apache.solr.handler.admin.api.SchemaBulkModifyAPI;
 import org.apache.solr.handler.admin.api.SchemaGetDynamicFieldAPI;
 import org.apache.solr.handler.admin.api.SchemaGetFieldAPI;
 import org.apache.solr.handler.admin.api.SchemaGetFieldTypeAPI;
-import org.apache.solr.handler.admin.api.SchemaInfoAPI;
 import org.apache.solr.handler.admin.api.SchemaListAllCopyFieldsAPI;
 import org.apache.solr.handler.admin.api.SchemaListAllDynamicFieldsAPI;
 import org.apache.solr.handler.admin.api.SchemaListAllFieldTypesAPI;
 import org.apache.solr.handler.admin.api.SchemaListAllFieldsAPI;
 import org.apache.solr.handler.admin.api.SchemaNameAPI;
-import org.apache.solr.handler.admin.api.SchemaSimilarityAPI;
-import org.apache.solr.handler.admin.api.SchemaUniqueKeyAPI;
-import org.apache.solr.handler.admin.api.SchemaVersionAPI;
 import org.apache.solr.handler.admin.api.SchemaZkVersionAPI;
 import org.apache.solr.handler.api.V2ApiUtils;
 import org.apache.solr.pkg.PackageListeningClassLoader;
@@ -140,19 +137,29 @@ public class SchemaHandler extends RequestHandlerBase
       String path = (String) req.getContext().get("path");
       switch (path) {
         case "/schema":
-          rsp.add(IndexSchema.SCHEMA, req.getSchema().getNamedPropertyValues());
-          break;
+          {
+            V2ApiUtils.squashIntoSolrResponseWithoutHeader(
+                rsp, new GetSchemaAPI(req.getCore().getLatestSchema()).getSchemaInfo());
+            break;
+          }
         case "/schema/version":
-          rsp.add(IndexSchema.VERSION, req.getSchema().getVersion());
-          break;
+          {
+            V2ApiUtils.squashIntoSolrResponseWithoutHeader(
+                rsp, new GetSchemaAPI(req.getCore().getLatestSchema()).getSchemaVersion());
+            break;
+          }
         case "/schema/uniquekey":
-          rsp.add(IndexSchema.UNIQUE_KEY, req.getSchema().getUniqueKeyField().getName());
-          break;
+          {
+            V2ApiUtils.squashIntoSolrResponseWithoutHeader(
+                rsp, new GetSchemaAPI(req.getCore().getLatestSchema()).getSchemaUniqueKey());
+            break;
+          }
         case "/schema/similarity":
-          rsp.add(
-              IndexSchema.SIMILARITY,
-              req.getSchema().getSimilarityFactory().getNamedPropertyValues());
-          break;
+          {
+            V2ApiUtils.squashIntoSolrResponseWithoutHeader(
+                rsp, new GetSchemaAPI(req.getCore().getLatestSchema()).getSchemaSimilarity());
+            break;
+          }
         case "/schema/name":
           {
             V2ApiUtils.squashIntoSolrResponseWithoutHeader(
@@ -313,10 +320,6 @@ public class SchemaHandler extends RequestHandlerBase
   public Collection<Api> getApis() {
 
     final List<Api> apis = new ArrayList<>();
-    apis.addAll(AnnotatedApi.getApis(new SchemaInfoAPI(this)));
-    apis.addAll(AnnotatedApi.getApis(new SchemaUniqueKeyAPI(this)));
-    apis.addAll(AnnotatedApi.getApis(new SchemaVersionAPI(this)));
-    apis.addAll(AnnotatedApi.getApis(new SchemaSimilarityAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new SchemaZkVersionAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new SchemaListAllFieldsAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new SchemaGetFieldAPI(this)));
@@ -332,7 +335,7 @@ public class SchemaHandler extends RequestHandlerBase
 
   @Override
   public Collection<Class<? extends JerseyResource>> getJerseyResources() {
-    return List.of(SchemaNameAPI.class);
+    return List.of(SchemaNameAPI.class, GetSchemaAPI.class);
   }
 
   @Override
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/GetSchemaAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/GetSchemaAPI.java
new file mode 100644
index 00000000000..461120aab15
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetSchemaAPI.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.handler.admin.api;
+
+import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.security.PermissionNameProvider;
+
+@Path("/{a:cores|collections}/{collectionName}/schema")
+public class GetSchemaAPI extends JerseyResource {
+
+  private IndexSchema indexSchema;
+
+  @Inject
+  public GetSchemaAPI(IndexSchema indexSchema) {
+    this.indexSchema = indexSchema;
+  }
+
+  @GET
+  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @PermissionName(PermissionNameProvider.Name.SCHEMA_READ_PERM)
+  public SchemaInfoResponse getSchemaInfo() {
+    final var response = instantiateJerseyResponse(SchemaInfoResponse.class);
+
+    response.schema = indexSchema.getNamedPropertyValues();
+
+    return response;
+  }
+
+  public static class SchemaInfoResponse extends SolrJerseyResponse {
+    // TODO The schema response is quite complicated, so for the moment it's sufficient to record it
+    // here only as a Map.  However, if SOLR-16825 is tackled then there will be a lot of value in
+    // describing this response format more accurately so that clients can navigate the contents
+    // without lots of map fetching and casting.
+    @JsonProperty("schema")
+    public Map<String, Object> schema;
+  }
+
+  @GET
+  @Path("/similarity")
+  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @PermissionName(PermissionNameProvider.Name.SCHEMA_READ_PERM)
+  public SchemaSimilarityResponse getSchemaSimilarity() {
+    final var response = instantiateJerseyResponse(SchemaSimilarityResponse.class);
+
+    response.similarity = indexSchema.getSimilarityFactory().getNamedPropertyValues();
+
+    return response;
+  }
+
+  public static class SchemaSimilarityResponse extends SolrJerseyResponse {
+    // TODO The schema response is quite complicated, so for the moment it's sufficient to record it
+    // here only as a Map.  However, if SOLR-16825 is tackled then there will be a lot of value in
+    // describing this response format more accurately so that clients can navigate the contents
+    // without lots of map fetching and casting.
+    @JsonProperty("similarity")
+    public SimpleOrderedMap<Object> similarity;
+  }
+
+  @GET
+  @Path("/uniquekey")
+  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @PermissionName(PermissionNameProvider.Name.SCHEMA_READ_PERM)
+  public SchemaUniqueKeyResponse getSchemaUniqueKey() {
+    final var response = instantiateJerseyResponse(SchemaUniqueKeyResponse.class);
+
+    response.uniqueKey = indexSchema.getUniqueKeyField().getName();
+
+    return response;
+  }
+
+  public static class SchemaUniqueKeyResponse extends SolrJerseyResponse {
+    @JsonProperty("uniqueKey")
+    public String uniqueKey;
+  }
+
+  @GET
+  @Path("/version")
+  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @PermissionName(PermissionNameProvider.Name.SCHEMA_READ_PERM)
+  public SchemaVersionResponse getSchemaVersion() {
+    final var response = instantiateJerseyResponse(SchemaVersionResponse.class);
+
+    response.version = indexSchema.getVersion();
+
+    return response;
+  }
+
+  public static class SchemaVersionResponse extends SolrJerseyResponse {
+    @JsonProperty("version")
+    public float version;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaInfoAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaInfoAPI.java
deleted file mode 100644
index 9ac6e3a52d9..00000000000
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaInfoAPI.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.solr.handler.admin.api;
-
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
-
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.handler.SchemaHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.security.PermissionNameProvider;
-
-/**
- * V2 API for getting basic information about an in-use schema
- *
- * <p>This API (GET /v2/collections/collectionName/schema) is analogous to the v1
- * /solr/collectionName/schema API.
- */
-public class SchemaInfoAPI {
-  private final SchemaHandler schemaHandler;
-
-  public SchemaInfoAPI(SchemaHandler schemaHandler) {
-    this.schemaHandler = schemaHandler;
-  }
-
-  @EndPoint(
-      path = {"/schema"},
-      method = GET,
-      permission = PermissionNameProvider.Name.SCHEMA_READ_PERM)
-  public void getSchemaInfo(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    schemaHandler.handleRequestBody(req, rsp);
-  }
-}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaSimilarityAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaSimilarityAPI.java
deleted file mode 100644
index 5e492646d84..00000000000
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaSimilarityAPI.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.solr.handler.admin.api;
-
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
-
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.handler.SchemaHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.security.PermissionNameProvider;
-
-/**
- * V2 API for getting information about the 'similarity' settings for an in-use schema.
- *
- * <p>This API (GET /v2/collections/collectionName/schema/similarity) is analogous to the v1
- * /solr/collectionName/schema/similarity API.
- */
-public class SchemaSimilarityAPI {
-  private final SchemaHandler schemaHandler;
-
-  public SchemaSimilarityAPI(SchemaHandler schemaHandler) {
-    this.schemaHandler = schemaHandler;
-  }
-
-  @EndPoint(
-      path = {"/schema/similarity"},
-      method = GET,
-      permission = PermissionNameProvider.Name.SCHEMA_READ_PERM)
-  public void getSchemaSimilarity(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    schemaHandler.handleRequestBody(req, rsp);
-  }
-}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaUniqueKeyAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaUniqueKeyAPI.java
deleted file mode 100644
index 13cb8490f5a..00000000000
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaUniqueKeyAPI.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.solr.handler.admin.api;
-
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
-
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.handler.SchemaHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.security.PermissionNameProvider;
-
-/**
- * V2 API for getting the name of the unique-key field for an in-use schema.
- *
- * <p>This API (GET /v2/collections/collectionName/schema/uniquekey) is analogous to the v1
- * /solr/collectionName/schema/uniquekey API.
- */
-public class SchemaUniqueKeyAPI {
-  private final SchemaHandler schemaHandler;
-
-  public SchemaUniqueKeyAPI(SchemaHandler schemaHandler) {
-    this.schemaHandler = schemaHandler;
-  }
-
-  @EndPoint(
-      path = {"/schema/uniquekey"},
-      method = GET,
-      permission = PermissionNameProvider.Name.SCHEMA_READ_PERM)
-  public void getSchemaUniqueKey(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    schemaHandler.handleRequestBody(req, rsp);
-  }
-}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaVersionAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaVersionAPI.java
deleted file mode 100644
index 6d3538f5c77..00000000000
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/SchemaVersionAPI.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.solr.handler.admin.api;
-
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
-
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.handler.SchemaHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.security.PermissionNameProvider;
-
-/**
- * V2 API for getting the version of an in-use schema.
- *
- * <p>This API (GET /v2/collections/collectionName/schema/version) is analogous to the v1
- * /solr/collectionName/schema/version API.
- */
-public class SchemaVersionAPI {
-  private final SchemaHandler schemaHandler;
-
-  public SchemaVersionAPI(SchemaHandler schemaHandler) {
-    this.schemaHandler = schemaHandler;
-  }
-
-  @EndPoint(
-      path = {"/schema/version"},
-      method = GET,
-      permission = PermissionNameProvider.Name.SCHEMA_READ_PERM)
-  public void getSchemaVersion(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    schemaHandler.handleRequestBody(req, rsp);
-  }
-}
diff --git a/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java b/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
index 2cbc5f99cfd..d6111b15d19 100644
--- a/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
+++ b/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
@@ -24,6 +24,7 @@ import javax.ws.rs.container.ContainerRequestContext;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.schema.IndexSchema;
 import org.glassfish.hk2.api.Factory;
 
 public class InjectionFactories {
@@ -85,6 +86,24 @@ public class InjectionFactories {
     public void dispose(SolrCore instance) {}
   }
 
+  public static class ReuseFromContextIndexSchemaFactory implements Factory<IndexSchema> {
+
+    private final SolrCore solrCore;
+
+    @Inject
+    public ReuseFromContextIndexSchemaFactory(SolrCore solrCore) {
+      this.solrCore = solrCore;
+    }
+
+    @Override
+    public IndexSchema provide() {
+      return solrCore.getLatestSchema();
+    }
+
+    @Override
+    public void dispose(IndexSchema instance) {}
+  }
+
   public static class SingletonFactory<T> implements Factory<T> {
 
     private final T singletonVal;
diff --git a/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java b/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
index ef9587ceb90..d8e3568e540 100644
--- a/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
+++ b/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
@@ -24,6 +24,7 @@ import java.util.Map;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.schema.IndexSchema;
 import org.apache.solr.util.SolrVersion;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
 import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider;
@@ -116,6 +117,9 @@ public class JerseyApplications {
               bindFactory(InjectionFactories.ReuseFromContextSolrCoreFactory.class)
                   .to(SolrCore.class)
                   .in(RequestScoped.class);
+              bindFactory(InjectionFactories.ReuseFromContextIndexSchemaFactory.class)
+                  .to(IndexSchema.class)
+                  .in(RequestScoped.class);
             }
           });
     }
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 a04e4bc4aef..07601cc624d 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
@@ -138,7 +138,6 @@ public class TestApiFramework extends SolrTestCaseJ4 {
     methodNames.add(rsp.getValues()._getStr("/spec[0]/methods[0]", null));
     methodNames.add(rsp.getValues()._getStr("/spec[1]/methods[0]", null));
     assertTrue(methodNames.contains("POST"));
-    assertTrue(methodNames.contains("GET"));
   }
 
   public void testPayload() {
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/GetSchemaAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/GetSchemaAPITest.java
new file mode 100644
index 00000000000..5aa3e3b8890
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/GetSchemaAPITest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.admin.api;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.schema.SimilarityFactory;
+import org.apache.solr.schema.StrField;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Unit tests for {@link GetSchemaAPI} */
+public class GetSchemaAPITest extends SolrTestCaseJ4 {
+
+  private IndexSchema mockSchema;
+  private GetSchemaAPI api;
+
+  @Before
+  public void setUpMocks() {
+    assumeWorkingMockito();
+
+    mockSchema = mock(IndexSchema.class);
+    api = new GetSchemaAPI(mockSchema);
+  }
+
+  @Test
+  public void testReliesOnIndexSchemaWhenFetchingWholeSchema() {
+    when(mockSchema.getNamedPropertyValues()).thenReturn(Map.of("flagKey", "flagValue"));
+
+    final var response = api.getSchemaInfo();
+
+    assertNotNull(response);
+    assertNotNull(response.schema);
+    assertEquals(1, response.schema.size());
+    assertEquals("flagValue", response.schema.get("flagKey"));
+  }
+
+  @Test
+  public void testReliesOnIndexSchemaWhenFetchingSimilarity() {
+    final var map = new SimpleOrderedMap<Object>();
+    map.add("flagKey", "flagValue");
+    final SimilarityFactory mockSimFactory = mock(SimilarityFactory.class);
+    when(mockSimFactory.getNamedPropertyValues()).thenReturn(map);
+    when(mockSchema.getSimilarityFactory()).thenReturn(mockSimFactory);
+
+    final var response = api.getSchemaSimilarity();
+
+    assertNotNull(response);
+    assertNotNull(response.similarity);
+    assertEquals(1, response.similarity.size());
+    assertEquals("flagValue", response.similarity.get("flagKey"));
+  }
+
+  @Test
+  public void testReliesOnIndexSchemaWhenFetchingUniqueKey() {
+    when(mockSchema.getUniqueKeyField()).thenReturn(new SchemaField("myUniqueKey", new StrField()));
+
+    final var response = api.getSchemaUniqueKey();
+
+    assertNotNull(response);
+    assertEquals("myUniqueKey", response.uniqueKey);
+  }
+
+  @Test
+  public void testReliesOnIndexSchemaWhenFetchingVersion() {
+    when(mockSchema.getVersion()).thenReturn(123.456f);
+
+    final var response = api.getSchemaVersion();
+
+    assertNotNull(response);
+    assertEquals(123.456f, response.version, 0.1f);
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2SchemaAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2SchemaAPIMappingTest.java
index 7de25940707..cb34f8a482e 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2SchemaAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2SchemaAPIMappingTest.java
@@ -26,10 +26,6 @@ public class V2SchemaAPIMappingTest extends V2ApiMappingTest<SchemaHandler> {
 
   @Override
   public void populateApiBag() {
-    apiBag.registerObject(new SchemaInfoAPI(getRequestHandler()));
-    apiBag.registerObject(new SchemaUniqueKeyAPI(getRequestHandler()));
-    apiBag.registerObject(new SchemaVersionAPI(getRequestHandler()));
-    apiBag.registerObject(new SchemaSimilarityAPI(getRequestHandler()));
     apiBag.registerObject(new SchemaZkVersionAPI(getRequestHandler()));
     apiBag.registerObject(new SchemaListAllFieldsAPI(getRequestHandler()));
     apiBag.registerObject(new SchemaGetFieldAPI(getRequestHandler()));
@@ -53,7 +49,6 @@ public class V2SchemaAPIMappingTest extends V2ApiMappingTest<SchemaHandler> {
 
   @Test
   public void testGetSchemaInfoApis() {
-    assertAnnotatedApiExistsFor("GET", "/schema");
     assertAnnotatedApiExistsFor("GET", "/schema/dynamicfields");
     assertAnnotatedApiExistsFor("GET", "/schema/dynamicfields/someDynamicField");
     assertAnnotatedApiExistsFor("GET", "/schema/fieldtypes");
@@ -61,9 +56,6 @@ public class V2SchemaAPIMappingTest extends V2ApiMappingTest<SchemaHandler> {
     assertAnnotatedApiExistsFor("GET", "/schema/fields");
     assertAnnotatedApiExistsFor("GET", "/schema/fields/someField");
     assertAnnotatedApiExistsFor("GET", "/schema/copyfields");
-    assertAnnotatedApiExistsFor("GET", "/schema/similarity");
-    assertAnnotatedApiExistsFor("GET", "/schema/uniquekey");
-    assertAnnotatedApiExistsFor("GET", "/schema/version");
     assertAnnotatedApiExistsFor("GET", "/schema/zkversion");
   }
 


[solr] 02/02: SOLR-16392: Tweak v2 CREATESHARD to be more REST-ful (#1671)

Posted by ge...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit a0cd45394f3c99e4c3918d007e7b963819019c7b
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Thu Jun 1 09:09:58 2023 -0400

    SOLR-16392: Tweak v2 CREATESHARD to be more REST-ful (#1671)
    
    The "create" command-specifier has been removed, but the API otherwise
    remains unchanged.
    
    This commit also converts the API over to the JAX-RS framework.
---
 solr/CHANGES.txt                                   |   9 +
 .../solr/handler/admin/CollectionsHandler.java     |  41 +---
 .../solr/handler/admin/api/CreateShardAPI.java     | 220 +++++++++++++++++----
 .../solr/handler/admin/api/CreateShardAPITest.java | 172 ++++++++++++++++
 .../handler/admin/api/V2ShardsAPIMappingTest.java  |  41 ----
 .../deployment-guide/pages/shard-management.adoc   |   6 +-
 6 files changed, 372 insertions(+), 117 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 7e8cd9cf1a5..1a99ef812b6 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -109,6 +109,15 @@ Improvements
 
 * SOLR-16687: Add support of SolrClassLoader to SolrZkClient (Lamine Idjeraoui via Jason Gerlowski & Houston Putman)
 
+* SOLR-9378: Internal shard requests no longer include the wasteful shard.url param.  [shard] transformer now defaults to returning 
+  only the shard id (based on luceneMatchVersion), but can be configured to return the legacy list of replicas. (hossman)
+
+* SOLR-16816: Update node metrics while making affinityPlacement selections. Therefore selections can be made given the expected cluster
+  information after the previous selections are implemented. (Houston Putman)
+
+* SOLR-16392: The v2 "create shard" API has been tweaked to be more intuitive, by removing the top-level "create"
+  command specifier.  The rest of the API remains unchanged. (Jason Gerlowski)
+
 Optimizations
 ---------------------
 
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index f222fa2e801..c1ca2a1df69 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -136,7 +136,6 @@ import org.apache.solr.client.solrj.impl.HttpSolrClient.Builder;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.CoreAdminRequest.RequestSyncShard;
 import org.apache.solr.client.solrj.response.RequestStatusState;
-import org.apache.solr.client.solrj.util.SolrIdentifierValidator;
 import org.apache.solr.cloud.OverseerSolrResponse;
 import org.apache.solr.cloud.OverseerSolrResponseSerializer;
 import org.apache.solr.cloud.OverseerTaskQueue;
@@ -152,8 +151,6 @@ import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.cloud.ClusterProperties;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
-import org.apache.solr.common.cloud.DocCollection.CollectionStateProps;
-import org.apache.solr.common.cloud.ImplicitDocRouter;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Replica.State;
 import org.apache.solr.common.cloud.Slice;
@@ -768,40 +765,8 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     CREATESHARD_OP(
         CREATESHARD,
         (req, rsp, h) -> {
-          Map<String, Object> map =
-              copy(req.getParams().required(), null, COLLECTION_PROP, SHARD_ID_PROP);
-          ClusterState clusterState = h.coreContainer.getZkController().getClusterState();
-          final String newShardName =
-              SolrIdentifierValidator.validateShardName(req.getParams().get(SHARD_ID_PROP));
-          boolean followAliases = req.getParams().getBool(FOLLOW_ALIASES, false);
-          String extCollectionName = req.getParams().get(COLLECTION_PROP);
-          String collectionName =
-              followAliases
-                  ? h.coreContainer
-                      .getZkController()
-                      .getZkStateReader()
-                      .getAliases()
-                      .resolveSimpleAlias(extCollectionName)
-                  : extCollectionName;
-          if (!ImplicitDocRouter.NAME.equals(
-              ((Map<?, ?>)
-                      clusterState
-                          .getCollection(collectionName)
-                          .get(CollectionStateProps.DOC_ROUTER))
-                  .get(NAME)))
-            throw new SolrException(
-                ErrorCode.BAD_REQUEST, "shards can be added only to 'implicit' collections");
-          copy(
-              req.getParams(),
-              map,
-              REPLICATION_FACTOR,
-              NRT_REPLICAS,
-              TLOG_REPLICAS,
-              PULL_REPLICAS,
-              CREATE_NODE_SET,
-              WAIT_FOR_FINAL_STATE,
-              FOLLOW_ALIASES);
-          return copyPropertiesWithPrefix(req.getParams(), map, PROPERTY_PREFIX);
+          CreateShardAPI.invokeFromV1Params(h.coreContainer, req, rsp);
+          return null;
         }),
     DELETEREPLICA_OP(
         DELETEREPLICA,
@@ -1558,6 +1523,7 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
         CreateAliasAPI.class,
         CreateCollectionAPI.class,
         CreateCollectionBackupAPI.class,
+        CreateShardAPI.class,
         DeleteAliasAPI.class,
         DeleteCollectionBackupAPI.class,
         DeleteCollectionAPI.class,
@@ -1582,7 +1548,6 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
   public Collection<Api> getApis() {
     final List<Api> apis = new ArrayList<>();
     apis.addAll(AnnotatedApi.getApis(new SplitShardAPI(this)));
-    apis.addAll(AnnotatedApi.getApis(new CreateShardAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new AddReplicaAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new SyncShardAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new ForceLeaderAPI(this)));
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShardAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShardAPI.java
index 080068c4d69..3f39404f45a 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShardAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShardAPI.java
@@ -17,65 +17,215 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
+import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.CREATE_NODE_SET;
+import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
+import static org.apache.solr.common.cloud.ZkStateReader.PULL_REPLICAS;
+import static org.apache.solr.common.cloud.ZkStateReader.REPLICATION_FACTOR;
+import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.TLOG_REPLICAS;
 import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
 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.PROPERTY_PREFIX;
-import static org.apache.solr.common.params.CommonParams.ACTION;
-import static org.apache.solr.handler.ClusterAPI.wrapParams;
-import static org.apache.solr.handler.api.V2ApiUtils.flattenMapWithPrefix;
+import static org.apache.solr.common.params.CollectionAdminParams.SHARD;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.handler.admin.api.CreateCollectionAPI.copyPrefixedPropertiesWithoutPrefix;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import org.apache.solr.api.Command;
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.api.PayloadObj;
-import org.apache.solr.client.solrj.request.beans.CreateShardPayload;
+import javax.inject.Inject;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import org.apache.solr.client.solrj.util.SolrIdentifierValidator;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.ClusterState;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.cloud.ImplicitDocRouter;
+import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SubResponseAccumulatingJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
 
 /**
  * V2 API for creating a new shard in a collection.
  *
- * <p>This API (POST /v2/collections/collectionName/shards {'create': {...}}) is analogous to the v1
+ * <p>This API (POST /v2/collections/collectionName/shards {...}) is analogous to the v1
  * /admin/collections?action=CREATESHARD command.
- *
- * @see CreateShardAPI
  */
-@EndPoint(
-    path = {"/c/{collection}/shards", "/collections/{collection}/shards"},
-    method = POST,
-    permission = COLL_EDIT_PERM)
-public class CreateShardAPI {
-  private static final String V2_CREATE_CMD = "create";
+@Path("/collections/{collectionName}/shards")
+public class CreateShardAPI extends AdminAPIBase {
+
+  @Inject
+  public CreateShardAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
 
-  private final CollectionsHandler collectionsHandler;
+  @POST
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(COLL_EDIT_PERM)
+  public SubResponseAccumulatingJerseyResponse createShard(
+      @PathParam("collectionName") String collectionName, CreateShardRequestBody requestBody)
+      throws Exception {
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    if (requestBody == null) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST, "Required request-body is missing");
+    }
+    ensureRequiredParameterProvided(COLLECTION_PROP, collectionName);
+    ensureRequiredParameterProvided(SHARD_ID_PROP, requestBody.shardName);
+    SolrIdentifierValidator.validateShardName(requestBody.shardName);
+    final String resolvedCollectionName =
+        resolveAndValidateAliasIfEnabled(
+            collectionName, Boolean.TRUE.equals(requestBody.followAliases));
+    ensureCollectionUsesImplicitRouter(resolvedCollectionName);
 
-  public CreateShardAPI(CollectionsHandler collectionsHandler) {
-    this.collectionsHandler = collectionsHandler;
+    final ZkNodeProps remoteMessage = createRemoteMessage(resolvedCollectionName, requestBody);
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionParams.CollectionAction.CREATESHARD,
+        remoteMessage,
+        requestBody.asyncId);
+    return response;
   }
 
-  @Command(name = V2_CREATE_CMD)
-  public void createShard(PayloadObj<CreateShardPayload> obj) throws Exception {
-    final CreateShardPayload v2Body = obj.get();
-    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
-    v1Params.put(ACTION, CollectionParams.CollectionAction.CREATESHARD.toLower());
-    v1Params.put(COLLECTION, obj.getRequest().getPathTemplateValues().get(COLLECTION));
+  public static class CreateShardRequestBody implements JacksonReflectMapWriter {
+    @JsonProperty(NAME)
+    public String shardName;
+
+    @JsonProperty(REPLICATION_FACTOR)
+    public Integer replicationFactor;
+
+    @JsonProperty(NRT_REPLICAS)
+    public Integer nrtReplicas;
+
+    @JsonProperty(TLOG_REPLICAS)
+    public Integer tlogReplicas;
+
+    @JsonProperty(PULL_REPLICAS)
+    public Integer pullReplicas;
+
+    @JsonProperty("createReplicas")
+    public Boolean createReplicas;
+
+    @JsonProperty("nodeSet")
+    public List<String> nodeSet;
+
+    @JsonProperty(WAIT_FOR_FINAL_STATE)
+    public Boolean waitForFinalState;
 
-    if (v2Body.nodeSet != null) {
-      v1Params.put(CREATE_NODE_SET_PARAM, buildV1CreateNodeSetValue(v2Body.nodeSet));
+    @JsonProperty(FOLLOW_ALIASES)
+    public Boolean followAliases;
+
+    @JsonProperty(ASYNC)
+    public String asyncId;
+
+    @JsonProperty public Map<String, String> properties;
+
+    public static CreateShardRequestBody fromV1Params(SolrParams params) {
+      params.required().check(COLLECTION, SHARD);
+
+      final var requestBody = new CreateShardRequestBody();
+      requestBody.shardName = params.get(SHARD);
+      requestBody.replicationFactor = params.getInt(REPLICATION_FACTOR);
+      requestBody.nrtReplicas = params.getInt(NRT_REPLICAS);
+      requestBody.tlogReplicas = params.getInt(TLOG_REPLICAS);
+      requestBody.pullReplicas = params.getInt(PULL_REPLICAS);
+      if (params.get(CREATE_NODE_SET_PARAM) != null) {
+        final String nodeSetStr = params.get(CREATE_NODE_SET_PARAM);
+        if ("EMPTY".equals(nodeSetStr)) {
+          requestBody.createReplicas = false;
+        } else {
+          requestBody.nodeSet = Arrays.asList(nodeSetStr.split(","));
+        }
+      }
+      requestBody.waitForFinalState = params.getBool(WAIT_FOR_FINAL_STATE);
+      requestBody.followAliases = params.getBool(FOLLOW_ALIASES);
+      requestBody.asyncId = params.get(ASYNC);
+      requestBody.properties =
+          copyPrefixedPropertiesWithoutPrefix(params, new HashMap<>(), PROPERTY_PREFIX);
+
+      return requestBody;
     }
+  }
 
-    flattenMapWithPrefix(v2Body.coreProperties, v1Params, PROPERTY_PREFIX);
-    collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
+  public static void invokeFromV1Params(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse)
+      throws Exception {
+    final var requestBody = CreateShardRequestBody.fromV1Params(solrQueryRequest.getParams());
+    final var createShardApi =
+        new CreateShardAPI(coreContainer, solrQueryRequest, solrQueryResponse);
+    final var response =
+        createShardApi.createShard(solrQueryRequest.getParams().get(COLLECTION), requestBody);
+    V2ApiUtils.squashIntoSolrResponseWithoutHeader(solrQueryResponse, response);
   }
 
-  private String buildV1CreateNodeSetValue(List<String> nodeSet) {
-    if (nodeSet.size() > 0) {
-      return String.join(",", nodeSet);
+  public static ZkNodeProps createRemoteMessage(
+      String collectionName, CreateShardRequestBody requestBody) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.CREATESHARD.toLower());
+    remoteMessage.put(COLLECTION_PROP, collectionName);
+    remoteMessage.put(SHARD_ID_PROP, requestBody.shardName);
+    if (requestBody.createReplicas == null || requestBody.createReplicas) {
+      // The remote message expects a single comma-delimited string, so nodeSet requires flattening
+      if (requestBody.nodeSet != null) {
+        remoteMessage.put(CREATE_NODE_SET_PARAM, String.join(",", requestBody.nodeSet));
+      }
+    } else {
+      remoteMessage.put(CREATE_NODE_SET, "EMPTY");
     }
-    return "EMPTY";
+    insertIfNotNull(remoteMessage, REPLICATION_FACTOR, requestBody.replicationFactor);
+    insertIfNotNull(remoteMessage, NRT_REPLICAS, requestBody.nrtReplicas);
+    insertIfNotNull(remoteMessage, TLOG_REPLICAS, requestBody.tlogReplicas);
+    insertIfNotNull(remoteMessage, PULL_REPLICAS, requestBody.pullReplicas);
+    insertIfNotNull(remoteMessage, WAIT_FOR_FINAL_STATE, requestBody.waitForFinalState);
+    insertIfNotNull(remoteMessage, FOLLOW_ALIASES, requestBody.followAliases);
+    insertIfNotNull(remoteMessage, ASYNC, requestBody.asyncId);
+
+    if (requestBody.properties != null) {
+      requestBody
+          .properties
+          .entrySet()
+          .forEach(
+              entry -> {
+                remoteMessage.put(PROPERTY_PREFIX + entry.getKey(), entry.getValue());
+              });
+    }
+
+    return new ZkNodeProps(remoteMessage);
+  }
+
+  private void ensureCollectionUsesImplicitRouter(String collectionName) {
+    final ClusterState clusterState = coreContainer.getZkController().getClusterState();
+    if (!ImplicitDocRouter.NAME.equals(
+        ((Map<?, ?>)
+                clusterState
+                    .getCollection(collectionName)
+                    .get(DocCollection.CollectionStateProps.DOC_ROUTER))
+            .get(NAME)))
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "shards can be added only to 'implicit' collections");
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateShardAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateShardAPITest.java
new file mode 100644
index 00000000000..07b71d8a805
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateShardAPITest.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.handler.admin.api;
+
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
+import static org.apache.solr.common.cloud.ZkStateReader.PULL_REPLICAS;
+import static org.apache.solr.common.cloud.ZkStateReader.REPLICATION_FACTOR;
+import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.TLOG_REPLICAS;
+import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
+import static org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
+import static org.hamcrest.Matchers.containsString;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.hamcrest.MatcherAssert;
+import org.junit.Test;
+
+/** Unit tests for {@link CreateShardAPI} */
+public class CreateShardAPITest extends SolrTestCaseJ4 {
+
+  @Test
+  public void testReportsErrorIfRequestBodyMissing() {
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new CreateShardAPI(null, null, null);
+              api.createShard("someCollName", null);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Required request-body is missing", thrown.getMessage());
+  }
+
+  @Test
+  public void testReportsErrorIfCollectionNameMissing() {
+    final var requestBody = new CreateShardAPI.CreateShardRequestBody();
+    requestBody.shardName = "someShardName";
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new CreateShardAPI(null, null, null);
+              api.createShard(null, requestBody);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Missing required parameter: collection", thrown.getMessage());
+  }
+
+  @Test
+  public void testReportsErrorIfShardNameMissing() {
+    final var requestBody = new CreateShardAPI.CreateShardRequestBody();
+    requestBody.shardName = null;
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new CreateShardAPI(null, null, null);
+              api.createShard("someCollectionName", requestBody);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Missing required parameter: shard", thrown.getMessage());
+  }
+
+  @Test
+  public void testReportsErrorIfShardNameIsInvalid() {
+    final var requestBody = new CreateShardAPI.CreateShardRequestBody();
+    requestBody.shardName = "invalid$shard@name";
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new CreateShardAPI(null, null, null);
+              api.createShard("someCollectionName", requestBody);
+            });
+
+    assertEquals(400, thrown.code());
+    MatcherAssert.assertThat(
+        thrown.getMessage(), containsString("Invalid shard: [invalid$shard@name]"));
+  }
+
+  @Test
+  public void testCreateRemoteMessageAllProperties() {
+    final var requestBody = new CreateShardAPI.CreateShardRequestBody();
+    requestBody.shardName = "someShardName";
+    requestBody.replicationFactor = 123;
+    requestBody.nrtReplicas = 123;
+    requestBody.tlogReplicas = 456;
+    requestBody.pullReplicas = 789;
+    requestBody.createReplicas = true;
+    requestBody.nodeSet = List.of("node1", "node2");
+    requestBody.waitForFinalState = true;
+    requestBody.followAliases = true;
+    requestBody.asyncId = "someAsyncId";
+    requestBody.properties = Map.of("propName1", "propVal1", "propName2", "propVal2");
+
+    final var remoteMessage =
+        CreateShardAPI.createRemoteMessage("someCollectionName", requestBody).getProperties();
+
+    assertEquals(13, remoteMessage.size());
+    assertEquals("createshard", remoteMessage.get(QUEUE_OPERATION));
+    assertEquals("someCollectionName", remoteMessage.get(COLLECTION));
+    assertEquals("someShardName", remoteMessage.get(SHARD_ID_PROP));
+    assertEquals(123, remoteMessage.get(REPLICATION_FACTOR));
+    assertEquals(123, remoteMessage.get(NRT_REPLICAS));
+    assertEquals(456, remoteMessage.get(TLOG_REPLICAS));
+    assertEquals(789, remoteMessage.get(PULL_REPLICAS));
+    assertEquals("node1,node2", remoteMessage.get(CREATE_NODE_SET_PARAM));
+    assertEquals(true, remoteMessage.get(WAIT_FOR_FINAL_STATE));
+    assertEquals(true, remoteMessage.get(FOLLOW_ALIASES));
+    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
+    assertEquals("propVal1", remoteMessage.get("property.propName1"));
+    assertEquals("propVal2", remoteMessage.get("property.propName2"));
+  }
+
+  @Test
+  public void testCanConvertV1ParamsToV2RequestBody() {
+    final var v1Params = new ModifiableSolrParams();
+    v1Params.add(COLLECTION, "someCollectionName");
+    v1Params.add(SHARD_ID_PROP, "someShardName");
+    v1Params.set(REPLICATION_FACTOR, 123);
+    v1Params.set(NRT_REPLICAS, 123);
+    v1Params.set(TLOG_REPLICAS, 456);
+    v1Params.set(PULL_REPLICAS, 789);
+    v1Params.add(CREATE_NODE_SET_PARAM, "node1,node2");
+    v1Params.set(WAIT_FOR_FINAL_STATE, true);
+    v1Params.set(FOLLOW_ALIASES, true);
+    v1Params.add(ASYNC, "someAsyncId");
+    v1Params.add("property.propName1", "propVal1");
+    v1Params.add("property.propName2", "propVal2");
+
+    final var requestBody = CreateShardAPI.CreateShardRequestBody.fromV1Params(v1Params);
+
+    assertEquals("someShardName", requestBody.shardName);
+    assertEquals(Integer.valueOf(123), requestBody.replicationFactor);
+    assertEquals(Integer.valueOf(123), requestBody.nrtReplicas);
+    assertEquals(Integer.valueOf(456), requestBody.tlogReplicas);
+    assertEquals(Integer.valueOf(789), requestBody.pullReplicas);
+    assertNull(requestBody.createReplicas);
+    assertEquals(List.of("node1", "node2"), requestBody.nodeSet);
+    assertEquals(Boolean.TRUE, requestBody.waitForFinalState);
+    assertEquals(Boolean.TRUE, requestBody.followAliases);
+    assertEquals("someAsyncId", requestBody.asyncId);
+    assertEquals("propVal1", requestBody.properties.get("propName1"));
+    assertEquals("propVal2", requestBody.properties.get("propName2"));
+  }
+}
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 af4fda1cd43..3d8d1448329 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
@@ -17,11 +17,7 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
-import static org.apache.solr.common.cloud.ZkStateReader.PULL_REPLICAS;
-import static org.apache.solr.common.cloud.ZkStateReader.REPLICATION_FACTOR;
 import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
-import static org.apache.solr.common.cloud.ZkStateReader.TLOG_REPLICAS;
 import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
 import static org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
 import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
@@ -59,7 +55,6 @@ public class V2ShardsAPIMappingTest extends V2ApiMappingTest<CollectionsHandler>
   public void populateApiBag() {
     final CollectionsHandler collectionsHandler = getRequestHandler();
     apiBag.registerObject(new SplitShardAPI(collectionsHandler));
-    apiBag.registerObject(new CreateShardAPI(collectionsHandler));
     apiBag.registerObject(new AddReplicaAPI(collectionsHandler));
     apiBag.registerObject(new SyncShardAPI(collectionsHandler));
     apiBag.registerObject(new ForceLeaderAPI(collectionsHandler));
@@ -137,42 +132,6 @@ public class V2ShardsAPIMappingTest extends V2ApiMappingTest<CollectionsHandler>
     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(COLLECTION));
-    assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
-    assertEquals("foo,bar,baz", v1Params.get(CREATE_NODE_SET_PARAM));
-    assertTrue(v1Params.getPrimitiveBool(FOLLOW_ALIASES));
-    assertEquals("some_async_id", v1Params.get(ASYNC));
-    assertTrue(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 =
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc
index a691743c1e5..c8548b573b9 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc
@@ -324,9 +324,7 @@ http://localhost:8983/solr/admin/collections?action=CREATESHARD&shard=newShardNa
 ----
 curl -X POST http://localhost:8983/api/collections/techproducts/shards -H 'Content-Type: application/json' -d '
   {
-    "create":{
-      "shard":"newShardName"
-    }
+    "shard":"newShardName"
   }
 '
 ----
@@ -358,6 +356,8 @@ s|Required |Default: none
 +
 
 The name of the collection that includes the shard to be split.
+Provided as a query parameter in v1 requests, and as a path parameter for v2 requests.
+
 
 `shard`::
 +