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


The following commit(s) were added to refs/heads/branch_9x by this push:
     new e5b2b431828 SOLR-16390: Tweak v2 clusterprop APIs to be more REST-ful 
(#2788)
e5b2b431828 is described below

commit e5b2b431828af4f3308ea8ddb102521e5702037d
Author: cugarte <[email protected]>
AuthorDate: Thu Nov 21 16:18:00 2024 -0500

    SOLR-16390: Tweak v2 clusterprop APIs to be more REST-ful (#2788)
    
    This commit changes several v2 "clusterprop" APIs to be
    more in line with the REST-ful design we're targeting for Solr's
    v2 APIs.
    
    It also adds new v2 clusterprop APIs for listing-all and fetching-
    single clusterprops.
    
    ---------
    
    Co-authored-by: Jason Gerlowski <[email protected]>
---
 solr/CHANGES.txt                                   |   6 +
 .../client/api/endpoint/ClusterPropertyApis.java   |  82 ++++++++++
 .../client/api/model/ClusterPropertyDetails.java   |  31 ++++
 .../api/model/GetClusterPropertyResponse.java      |  27 ++++
 .../api/model/ListClusterPropertiesResponse.java   |  28 ++++
 .../api/model/SetClusterPropertyRequestBody.java   |  27 ++++
 .../java/org/apache/solr/handler/ClusterAPI.java   |  22 ---
 .../solr/handler/admin/CollectionsHandler.java     |  15 +-
 .../solr/handler/admin/api/ClusterProperty.java    | 156 ++++++++++++++++++
 .../apache/solr/cloud/CollectionsAPISolrJTest.java |  30 ++--
 .../apache/solr/handler/V2ApiIntegrationTest.java  |  11 +-
 .../solr/handler/V2ClusterAPIMappingTest.java      |  19 ---
 .../handler/admin/api/ClusterPropsAPITest.java     | 178 +++++++++++++++++++++
 .../pages/cluster-node-management.adoc             | 162 +++++++++++++++----
 .../solrj/src/resources/java-template/api.mustache |   8 +-
 15 files changed, 696 insertions(+), 106 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 321a61dbc9d..8530533df59 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -45,6 +45,12 @@ Improvements
 
 * SOLR-17390: EmbeddedSolrServer now considers the ResponseParser (David 
Smiley)
 
+* SOLR-16390: v2 "cluster prop" APIs have been updated to be more REST-ful. 
Cluster prop creation/update are now available
+  at `PUT /api/cluster/properties/somePropName`.  Deletion is now available at 
`DELETE /api/cluster/properties/somePropName`.
+  New APIs for listing-all and fetching-single cluster props are also now 
available at `GET /api/cluster/properties` and
+  `GET /api/cluster/properties/somePropName`, respectively. (Carlos Ugarte via 
Jason Gerlowski)
+
+
 Optimizations
 ---------------------
 * SOLR-14985: Solrj CloudSolrClient with Solr URLs had serious performance 
regressions (since the
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/ClusterPropertyApis.java
 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/ClusterPropertyApis.java
new file mode 100644
index 00000000000..5c75eec0c60
--- /dev/null
+++ 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/ClusterPropertyApis.java
@@ -0,0 +1,82 @@
+/*
+ * 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.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import java.util.Map;
+import org.apache.solr.client.api.model.ListClusterPropertiesResponse;
+import org.apache.solr.client.api.model.SetClusterPropertyRequestBody;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/** Definitions for v2 JAX-RS cluster properties APIs. */
+@Path("/cluster/properties")
+public interface ClusterPropertyApis {
+  @GET
+  @Operation(
+      summary = "List all cluster properties in this Solr cluster.",
+      tags = {"cluster-properties"})
+  ListClusterPropertiesResponse listClusterProperties();
+
+  @GET
+  @Path("/{propertyName}")
+  @Operation(
+      summary = "Get a cluster property in this Solr cluster.",
+      tags = {"cluster-properties"})
+  SolrJerseyResponse getClusterProperty(
+      @Parameter(description = "The name of the property being retrieved.", 
required = true)
+          @PathParam("propertyName")
+          String propertyName);
+
+  @PUT
+  @Path("/{propertyName}")
+  @Operation(
+      summary = "Set a single new or existing cluster property in this Solr 
cluster.",
+      tags = {"cluster-properties"})
+  SolrJerseyResponse createOrUpdateClusterProperty(
+      @Parameter(description = "The name of the property being set.", required 
= true)
+          @PathParam("propertyName")
+          String propertyName,
+      @RequestBody(description = "Value to set for the property", required = 
true)
+          SetClusterPropertyRequestBody requestBody)
+      throws Exception;
+
+  @PUT
+  @Operation(
+      summary = "Set nested cluster properties in this Solr cluster.",
+      tags = {"cluster-properties"})
+  SolrJerseyResponse createOrUpdateNestedClusterProperty(
+      @RequestBody(description = "Property/ies to be set", required = true)
+          Map<String, Object> propertyValuesByName)
+      throws Exception;
+
+  @DELETE
+  @Path("/{propertyName}")
+  @Operation(
+      summary = "Delete a cluster property in this Solr cluster.",
+      tags = {"cluster-properties"})
+  SolrJerseyResponse deleteClusterProperty(
+      @Parameter(description = "The name of the property being deleted.", 
required = true)
+          @PathParam("propertyName")
+          String propertyName);
+}
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/ClusterPropertyDetails.java
 
b/solr/api/src/java/org/apache/solr/client/api/model/ClusterPropertyDetails.java
new file mode 100644
index 00000000000..9619e96ac1e
--- /dev/null
+++ 
b/solr/api/src/java/org/apache/solr/client/api/model/ClusterPropertyDetails.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class ClusterPropertyDetails {
+  @JsonProperty("name")
+  @Schema(description = "The name of the cluster property.")
+  public String name;
+
+  @JsonProperty("value")
+  @Schema(description = "The value of the cluster property.")
+  public Object value;
+}
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/GetClusterPropertyResponse.java
 
b/solr/api/src/java/org/apache/solr/client/api/model/GetClusterPropertyResponse.java
new file mode 100644
index 00000000000..3ebdd74ef60
--- /dev/null
+++ 
b/solr/api/src/java/org/apache/solr/client/api/model/GetClusterPropertyResponse.java
@@ -0,0 +1,27 @@
+/*
+ * 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.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class GetClusterPropertyResponse extends SolrJerseyResponse {
+  @JsonProperty("clusterProperty")
+  @Schema(description = "The requested cluster property.")
+  public ClusterPropertyDetails clusterProperty;
+}
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/ListClusterPropertiesResponse.java
 
b/solr/api/src/java/org/apache/solr/client/api/model/ListClusterPropertiesResponse.java
new file mode 100644
index 00000000000..46504fb23f1
--- /dev/null
+++ 
b/solr/api/src/java/org/apache/solr/client/api/model/ListClusterPropertiesResponse.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+
+public class ListClusterPropertiesResponse extends SolrJerseyResponse {
+  @JsonProperty("clusterProperties")
+  @Schema(description = "The list of cluster properties.")
+  public List<String> clusterProperties;
+}
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/SetClusterPropertyRequestBody.java
 
b/solr/api/src/java/org/apache/solr/client/api/model/SetClusterPropertyRequestBody.java
new file mode 100644
index 00000000000..057f4bcb1d5
--- /dev/null
+++ 
b/solr/api/src/java/org/apache/solr/client/api/model/SetClusterPropertyRequestBody.java
@@ -0,0 +1,27 @@
+/*
+ * 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.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class SetClusterPropertyRequestBody {
+  @Schema(description = "The value to assign to the property.")
+  @JsonProperty("value")
+  public String value;
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java 
b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
index e161d55e5b3..4807e19aca2 100644
--- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
@@ -23,7 +23,6 @@ import static 
org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
 import static 
org.apache.solr.cloud.api.collections.CollectionHandlingUtils.REQUESTID;
 import static org.apache.solr.common.params.CollectionParams.ACTION;
 import static 
org.apache.solr.common.params.CollectionParams.CollectionAction.ADDROLE;
-import static 
org.apache.solr.common.params.CollectionParams.CollectionAction.CLUSTERPROP;
 import static 
org.apache.solr.common.params.CollectionParams.CollectionAction.DELETESTATUS;
 import static 
org.apache.solr.common.params.CollectionParams.CollectionAction.OVERSEERSTATUS;
 import static 
org.apache.solr.common.params.CollectionParams.CollectionAction.REMOVEROLE;
@@ -43,7 +42,6 @@ import org.apache.solr.api.Command;
 import org.apache.solr.api.EndPoint;
 import org.apache.solr.api.PayloadObj;
 import org.apache.solr.client.solrj.cloud.DistribStateManager;
-import org.apache.solr.client.solrj.request.beans.ClusterPropPayload;
 import org.apache.solr.client.solrj.request.beans.RateLimiterPayload;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.annotation.JsonProperty;
@@ -275,26 +273,6 @@ public class ClusterAPI {
       collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), m), 
obj.getResponse());
     }
 
-    @Command(name = "set-obj-property")
-    public void setObjProperty(PayloadObj<ClusterPropPayload> obj) {
-      // Not using the object directly here because the API differentiate 
between {name:null} and {}
-      Map<String, Object> m = obj.getDataMap();
-      ClusterProperties clusterProperties =
-          new 
ClusterProperties(getCoreContainer().getZkController().getZkClient());
-      try {
-        clusterProperties.setClusterProperties(m);
-      } catch (Exception e) {
-        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error 
in API", e);
-      }
-    }
-
-    @Command(name = "set-property")
-    public void setProperty(PayloadObj<Map<String, String>> obj) throws 
Exception {
-      Map<String, Object> m = obj.getDataMap();
-      m.put("action", CLUSTERPROP.toString());
-      collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), m), 
obj.getResponse());
-    }
-
     @Command(name = "set-ratelimiter")
     public void setRateLimiters(PayloadObj<RateLimiterPayload> payLoad) {
       RateLimiterPayload rateLimiterConfig = payLoad.get();
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 3bcc0ce9c3f..b7d2f2c9280 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
@@ -128,6 +128,7 @@ import 
org.apache.solr.client.api.model.CreateCollectionSnapshotResponse;
 import org.apache.solr.client.api.model.InstallShardDataRequestBody;
 import org.apache.solr.client.api.model.ListCollectionSnapshotsResponse;
 import org.apache.solr.client.api.model.ReplaceNodeRequestBody;
+import org.apache.solr.client.api.model.SetClusterPropertyRequestBody;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.api.model.UpdateAliasPropertiesRequestBody;
 import org.apache.solr.client.api.model.UpdateCollectionPropertyRequestBody;
@@ -144,7 +145,6 @@ import 
org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetComma
 import org.apache.solr.cloud.api.collections.ReindexCollectionCmd;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
-import org.apache.solr.common.cloud.ClusterProperties;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Replica.State;
@@ -174,6 +174,7 @@ import org.apache.solr.handler.admin.api.AdminAPIBase;
 import org.apache.solr.handler.admin.api.AliasProperty;
 import org.apache.solr.handler.admin.api.BalanceReplicas;
 import org.apache.solr.handler.admin.api.BalanceShardUnique;
+import org.apache.solr.handler.admin.api.ClusterProperty;
 import org.apache.solr.handler.admin.api.CollectionProperty;
 import org.apache.solr.handler.admin.api.CollectionStatusAPI;
 import org.apache.solr.handler.admin.api.CreateAlias;
@@ -775,11 +776,12 @@ public class CollectionsHandler extends 
RequestHandlerBase implements Permission
     CLUSTERPROP_OP(
         CLUSTERPROP,
         (req, rsp, h) -> {
+          ClusterProperty clusterProperty = new 
ClusterProperty(req.getCoreContainer(), req, rsp);
+          SetClusterPropertyRequestBody setClusterPropertyRequestBody =
+              new SetClusterPropertyRequestBody();
           String name = req.getParams().required().get(NAME);
-          String val = req.getParams().get(VALUE_LONG);
-          ClusterProperties cp =
-              new 
ClusterProperties(h.coreContainer.getZkController().getZkClient());
-          cp.setClusterProperty(name, val);
+          setClusterPropertyRequestBody.value = 
req.getParams().get(VALUE_LONG);
+          clusterProperty.createOrUpdateClusterProperty(name, 
setClusterPropertyRequestBody);
           return null;
         }),
     COLLECTIONPROP_OP(
@@ -1389,7 +1391,8 @@ public class CollectionsHandler extends 
RequestHandlerBase implements Permission
         AliasProperty.class,
         ListCollectionSnapshots.class,
         CreateCollectionSnapshot.class,
-        DeleteCollectionSnapshot.class);
+        DeleteCollectionSnapshot.class,
+        ClusterProperty.class);
   }
 
   @Override
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/ClusterProperty.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/ClusterProperty.java
new file mode 100644
index 00000000000..efce79f7e84
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ClusterProperty.java
@@ -0,0 +1,156 @@
+/*
+ * 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.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM;
+
+import jakarta.inject.Inject;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import org.apache.solr.client.api.endpoint.ClusterPropertyApis;
+import org.apache.solr.client.api.model.ClusterPropertyDetails;
+import org.apache.solr.client.api.model.GetClusterPropertyResponse;
+import org.apache.solr.client.api.model.ListClusterPropertiesResponse;
+import org.apache.solr.client.api.model.SetClusterPropertyRequestBody;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.ClusterProperties;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+public class ClusterProperty extends AdminAPIBase implements 
ClusterPropertyApis {
+  protected final ClusterProperties clusterProperties;
+
+  @Inject
+  public ClusterProperty(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+    this.clusterProperties =
+        new ClusterProperties(
+            
fetchAndValidateZooKeeperAwareCoreContainer().getZkController().getZkClient());
+  }
+
+  /**
+   * V2 API for listing cluster properties.
+   *
+   * <p>This API (GET /api/cluster/properties) has no v1 equivalent.
+   */
+  @Override
+  @PermissionName(COLL_READ_PERM)
+  public ListClusterPropertiesResponse listClusterProperties() {
+    ListClusterPropertiesResponse response =
+        instantiateJerseyResponse(ListClusterPropertiesResponse.class);
+
+    try {
+      response.clusterProperties =
+          new ArrayList<>(clusterProperties.getClusterProperties().keySet());
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    return response;
+  }
+
+  /**
+   * V2 API for returning the value of a cluster property.
+   *
+   * <p>This API (GET /api/cluster/properties/{propertyName}) has no v1 
equivalent.
+   */
+  @Override
+  @PermissionName(COLL_READ_PERM)
+  public SolrJerseyResponse getClusterProperty(String propertyName) {
+    GetClusterPropertyResponse response =
+        instantiateJerseyResponse(GetClusterPropertyResponse.class);
+
+    try {
+      Object value = 
clusterProperties.getClusterProperties().get(propertyName);
+      if (value != null) {
+        response.clusterProperty = new ClusterPropertyDetails();
+        response.clusterProperty.name = propertyName;
+        response.clusterProperty.value = value;
+      } else {
+        throw new SolrException(
+            SolrException.ErrorCode.NOT_FOUND, "No such cluster property [" + 
propertyName + "]");
+      }
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    return response;
+  }
+
+  /**
+   * V2 API for setting the value of a single new or existing cluster property.
+   *
+   * <p>This API (PUT /api/cluster/properties/{propertyName} with an object 
listing the value) is
+   * equivalent to the v1 GET
+   * 
/solr/admin/collections?action=CLUSTERPROP&amp;name={propertyName}&amp;val={propertyValue}
 API.
+   */
+  @Override
+  @PermissionName(COLL_EDIT_PERM)
+  public SolrJerseyResponse createOrUpdateClusterProperty(
+      String propertyName, SetClusterPropertyRequestBody requestBody) throws 
IOException {
+    SolrJerseyResponse response = 
instantiateJerseyResponse(SolrJerseyResponse.class);
+    clusterProperties.setClusterProperty(propertyName, requestBody.value);
+    return response;
+  }
+
+  /**
+   * V2 API for setting the value of nested cluster properties.
+   *
+   * <p>This API (PUT /api/cluster/properties with an object listing those 
properties) has no v1
+   * equivalent.
+   */
+  @Override
+  @PermissionName(COLL_EDIT_PERM)
+  public SolrJerseyResponse createOrUpdateNestedClusterProperty(
+      Map<String, Object> propertyValuesByName) {
+    SolrJerseyResponse response = 
instantiateJerseyResponse(SolrJerseyResponse.class);
+    try {
+      clusterProperties.setClusterProperties(propertyValuesByName);
+    } catch (Exception e) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error in 
API", e);
+    }
+    return response;
+  }
+
+  /**
+   * V2 API for deleting a cluster property.
+   *
+   * <p>This API (DELETE /api/cluster/properties/{propertyName}) is equivalent 
to the v1 GET
+   * /solr/admin/collections?action=CLUSTERPROP&amp;name={propertyName} API.
+   */
+  @PermissionName(COLL_EDIT_PERM)
+  @Override
+  public SolrJerseyResponse deleteClusterProperty(String propertyName) {
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+
+    try {
+      clusterProperties.setClusterProperty(propertyName, null);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    return response;
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java 
b/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java
index f49f51a2f04..dc1595a1c8e 100644
--- a/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java
@@ -134,10 +134,10 @@ public class CollectionsAPISolrJTest extends 
SolrCloudTestCase {
     String COLL_NAME = "CollWithDefaultClusterProperties";
     try {
       V2Response rsp =
-          new V2Request.Builder("/cluster")
-              .withMethod(SolrRequest.METHOD.POST)
+          new V2Request.Builder("/cluster/properties")
+              .withMethod(SolrRequest.METHOD.PUT)
               .withPayload(
-                  "{set-obj-property:{defaults : {collection:{numShards : 2 , 
nrtReplicas : 2}}}}")
+                  "{\"defaults\": {\"collection\": {\"numShards\": 2, 
\"nrtReplicas\": 2}}}")
               .build()
               .process(cluster.getSolrClient());
 
@@ -174,15 +174,13 @@ public class CollectionsAPISolrJTest extends 
SolrCloudTestCase {
 
       // unset only a single value
       rsp =
-          new V2Request.Builder("/cluster")
-              .withMethod(SolrRequest.METHOD.POST)
+          new V2Request.Builder("/cluster/properties")
+              .withMethod(SolrRequest.METHOD.PUT)
               .withPayload(
                   "{\n"
-                      + "  \"set-obj-property\": {\n"
-                      + "    \"defaults\" : {\n"
-                      + "      \"collection\": {\n"
-                      + "        \"nrtReplicas\": null\n"
-                      + "      }\n"
+                      + "  \"defaults\" : {\n"
+                      + "    \"collection\": {\n"
+                      + "      \"nrtReplicas\": null\n"
                       + "    }\n"
                       + "  }\n"
                       + "}")
@@ -201,9 +199,9 @@ public class CollectionsAPISolrJTest extends 
SolrCloudTestCase {
       assertNull(clusterProperty);
 
       rsp =
-          new V2Request.Builder("/cluster")
-              .withMethod(SolrRequest.METHOD.POST)
-              .withPayload("{set-obj-property:{defaults: {collection:null}}}")
+          new V2Request.Builder("/cluster/properties")
+              .withMethod(SolrRequest.METHOD.PUT)
+              .withPayload("{\"defaults\": {\"collection\": null}}")
               .build()
               .process(cluster.getSolrClient());
       // assert that it is really gone in both old and new paths
@@ -218,9 +216,9 @@ public class CollectionsAPISolrJTest extends 
SolrCloudTestCase {
       assertNull(clusterProperty);
     } finally {
       V2Response rsp =
-          new V2Request.Builder("/cluster")
-              .withMethod(SolrRequest.METHOD.POST)
-              .withPayload("{set-obj-property:{defaults: null}}")
+          new V2Request.Builder("/cluster/properties")
+              .withMethod(SolrRequest.METHOD.PUT)
+              .withPayload("{\"defaults\": null}")
               .build()
               .process(cluster.getSolrClient());
     }
diff --git 
a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java 
b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
index 284f268acc2..0476aed8fb3 100644
--- a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
@@ -155,18 +155,17 @@ public class V2ApiIntegrationTest extends 
SolrCloudTestCase {
         cluster
             .getSolrClient()
             .request(
-                new V2Request.Builder("/cluster")
-                    .withMethod(SolrRequest.METHOD.POST)
-                    .withPayload("{set-property: {name: maxCoresPerNode, 
val:42}}")
+                new V2Request.Builder("/cluster/properties/maxCoresPerNode")
+                    .withMethod(SolrRequest.METHOD.PUT)
+                    .withPayload("{\"value\": \"42\"}")
                     .build());
     assertTrue(resp.toString().contains("status=0"));
     resp =
         cluster
             .getSolrClient()
             .request(
-                new V2Request.Builder("/cluster")
-                    .withMethod(SolrRequest.METHOD.POST)
-                    .withPayload("{set-property: {name: maxCoresPerNode, 
val:null}}")
+                new V2Request.Builder("/cluster/properties/maxCoresPerNode")
+                    .withMethod(SolrRequest.METHOD.DELETE)
                     .build());
     assertTrue(resp.toString().contains("status=0"));
   }
diff --git 
a/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java 
b/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
index 94f091fba2a..6410dc60a47 100644
--- a/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
@@ -19,7 +19,6 @@ package org.apache.solr.handler;
 
 import static 
org.apache.solr.cloud.api.collections.CollectionHandlingUtils.REQUESTID;
 import static org.apache.solr.common.params.CommonParams.ACTION;
-import static org.apache.solr.common.params.CommonParams.NAME;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -128,29 +127,11 @@ public class V2ClusterAPIMappingTest extends 
SolrTestCaseJ4 {
     assertEquals("some_role", v1Params.get("role"));
   }
 
-  @Test
-  public void testSetPropertyAllParams() throws Exception {
-    final SolrParams v1Params =
-        captureConvertedV1Params(
-            "/cluster",
-            "POST",
-            "{'set-property': {" + "'name': 'some_prop_name', " + 
"'val':'some_value'}}");
-
-    assertEquals(CollectionParams.CollectionAction.CLUSTERPROP.toString(), 
v1Params.get(ACTION));
-    assertEquals("some_prop_name", v1Params.get(NAME));
-    assertEquals("some_value", v1Params.get("val"));
-  }
-
   private SolrParams captureConvertedV1Params(String path, String method, 
String v2RequestBody)
       throws Exception {
     return doCaptureParams(path, method, v2RequestBody, 
mockCollectionsHandler);
   }
 
-  private SolrParams captureConvertedConfigsetV1Params(
-      String path, String method, String v2RequestBody) throws Exception {
-    return doCaptureParams(path, method, v2RequestBody, mockConfigSetHandler);
-  }
-
   private SolrParams doCaptureParams(
       String path, String method, String v2RequestBody, RequestHandlerBase 
mockHandler)
       throws Exception {
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/ClusterPropsAPITest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/api/ClusterPropsAPITest.java
new file mode 100644
index 00000000000..9ae628e71eb
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/ClusterPropsAPITest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.common.util.Utils.getObjectByPath;
+
+import java.net.URL;
+import java.util.List;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.entity.StringEntity;
+import org.apache.solr.client.solrj.impl.HttpSolrClient;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.util.Utils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class ClusterPropsAPITest extends SolrCloudTestCase {
+
+  private URL baseUrl;
+  private String baseUrlV2ClusterProps;
+
+  private static final String testClusterProperty = "ext.test";
+  private static final String testClusterPropertyValue = "test value";
+  private static final String testClusterPropertyNestedKeyAndValue =
+      "  \"defaults\": {"
+          + "    \"collection\": {"
+          + "      \"numShards\": 4,"
+          + "      \"nrtReplicas\": 2,"
+          + "      \"tlogReplicas\": 2,"
+          + "      \"pullReplicas\": 2"
+          + "    }"
+          + "  }";
+  private static final String testClusterPropertyBulkAndNestedValues =
+      "{"
+          + testClusterPropertyNestedKeyAndValue
+          + ","
+          + "  \""
+          + testClusterProperty
+          + "\": "
+          + "\""
+          + testClusterPropertyValue
+          + "\""
+          + " }";
+
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    configureCluster(1).addConfig("conf", 
configset("cloud-minimal")).configure();
+  }
+
+  @Before
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    baseUrl = cluster.getJettySolrRunner(0).getBaseUrl();
+    baseUrlV2ClusterProps =
+        cluster.getJettySolrRunner(0).getBaseURLV2().toString() + 
"/cluster/properties";
+  }
+
+  @After
+  @Override
+  public void tearDown() throws Exception {
+    super.tearDown();
+  }
+
+  @Test
+  public void testClusterPropertyOpsAllGood() throws Exception {
+    try (HttpSolrClient client = new 
HttpSolrClient.Builder(baseUrl.toString()).build()) {
+      // List Properties, confirm there aren't any
+      Object o =
+          Utils.executeGET(client.getHttpClient(), baseUrlV2ClusterProps, 
Utils.JSONCONSUMER);
+      assertNotNull(o);
+      assertEquals(0, ((List<?>) getObjectByPath(o, true, 
"clusterProperties")).size());
+
+      // Create a single cluster property
+      String path = baseUrlV2ClusterProps + "/" + testClusterProperty;
+      HttpPut httpPut = new HttpPut(path);
+      httpPut.setHeader("Content-Type", "application/json");
+      httpPut.setEntity(new StringEntity("{\"value\":\"" + 
testClusterPropertyValue + "\"}"));
+      o = Utils.executeHttpMethod(client.getHttpClient(), path, 
Utils.JSONCONSUMER, httpPut);
+      assertNotNull(o);
+
+      // List Properties, this time there should be 1
+      o = Utils.executeGET(client.getHttpClient(), baseUrlV2ClusterProps, 
Utils.JSONCONSUMER);
+      assertNotNull(o);
+      assertEquals(1, ((List<?>) getObjectByPath(o, true, 
"clusterProperties")).size());
+      assertEquals(
+          testClusterProperty,
+          (String) ((List<?>) getObjectByPath(o, true, 
"clusterProperties")).get(0));
+
+      // Fetch Cluster Property
+      // Same path as used in the Create step above
+      o = Utils.executeGET(client.getHttpClient(), path, Utils.JSONCONSUMER);
+      assertNotNull(o);
+      assertEquals(testClusterProperty, (String) getObjectByPath(o, true, 
"clusterProperty/name"));
+      assertEquals(
+          testClusterPropertyValue, (String) getObjectByPath(o, true, 
"clusterProperty/value"));
+
+      // Delete Cluster Property
+      // Same path as used in the Create step above
+      HttpDelete httpDelete = new HttpDelete(path);
+      o = Utils.executeHttpMethod(client.getHttpClient(), path, 
Utils.JSONCONSUMER, httpDelete);
+      assertNotNull(o);
+
+      // List Properties, should be back to 0
+      o = Utils.executeGET(client.getHttpClient(), baseUrlV2ClusterProps, 
Utils.JSONCONSUMER);
+      assertNotNull(o);
+      assertEquals(0, ((List<?>) getObjectByPath(o, true, 
"clusterProperties")).size());
+    }
+  }
+
+  @Test
+  public void testClusterPropertyNestedBulkSet() throws Exception {
+    try (HttpSolrClient client = new 
HttpSolrClient.Builder(baseUrl.toString()).build()) {
+      // Create a single cluster property using the Bulk/Nested set 
ClusterProp API
+      HttpPut httpPut = new HttpPut(baseUrlV2ClusterProps);
+      httpPut.setHeader("Content-Type", "application/json");
+      httpPut.setEntity(new 
StringEntity(testClusterPropertyBulkAndNestedValues));
+      Object o =
+          Utils.executeHttpMethod(
+              client.getHttpClient(), baseUrlV2ClusterProps, 
Utils.JSONCONSUMER, httpPut);
+      assertNotNull(o);
+
+      // Fetch Cluster Property checking the not-nested property set above
+      String path = baseUrlV2ClusterProps + "/" + testClusterProperty;
+      o = Utils.executeGET(client.getHttpClient(), path, Utils.JSONCONSUMER);
+      assertNotNull(o);
+      assertEquals(testClusterProperty, (String) getObjectByPath(o, true, 
"clusterProperty/name"));
+      assertEquals(
+          testClusterPropertyValue, (String) getObjectByPath(o, true, 
"clusterProperty/value"));
+
+      // Fetch Cluster Property checking the nested property set above
+      path = baseUrlV2ClusterProps + "/" + "defaults";
+      o = Utils.executeGET(client.getHttpClient(), path, Utils.JSONCONSUMER);
+      assertNotNull(o);
+      assertEquals("defaults", (String) getObjectByPath(o, true, 
"clusterProperty/name"));
+      assertEquals(4L, getObjectByPath(o, true, 
"clusterProperty/value/collection/numShards"));
+
+      // Clean up to leave the state unchanged
+      HttpDelete httpDelete = new HttpDelete(path);
+      Utils.executeHttpMethod(client.getHttpClient(), path, 
Utils.JSONCONSUMER, httpDelete);
+      path = baseUrlV2ClusterProps + "/" + testClusterProperty;
+      httpDelete = new HttpDelete(path);
+      Utils.executeHttpMethod(client.getHttpClient(), path, 
Utils.JSONCONSUMER, httpDelete);
+    }
+  }
+
+  @Test
+  public void testClusterPropertyFetchNonExistentProperty() throws Exception {
+    try (HttpSolrClient client = new 
HttpSolrClient.Builder(baseUrl.toString()).build()) {
+      // Fetch Cluster Property that doesn't exist
+      String path = baseUrlV2ClusterProps + "/ext.clusterPropThatDoesNotExist";
+      HttpGet fetchClusterPropertyGet = new HttpGet(path);
+      HttpResponse httpResponse = 
client.getHttpClient().execute(fetchClusterPropertyGet);
+      assertEquals(404, httpResponse.getStatusLine().getStatusCode());
+    }
+  }
+}
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
index cc5085b6a3c..e16bee4477d 100644
--- 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
@@ -248,37 +248,41 @@ 
http://localhost:8983/solr/admin/collections?action=CLUSTERPROP&name=urlScheme&v
 V2 API::
 +
 ====
+To create or update a cluster property:
 [source,bash]
 ----
-curl -X POST http://localhost:8983/api/cluster -H 'Content-Type: 
application/json' -d '
+curl -X PUT http://localhost:8983/api/cluster/properties/urlScheme -H 
'Content-Type: application/json' -d '
   {
-    "set-property": {
-      "name": "urlScheme",
-      "val": "https"
-    }
+    "value": "https"
   }
 '
 ----
+
+To delete an existing cluster property:
+[source,bash]
+----
+curl -X DELETE http://localhost:8983/api/cluster/properties/urlScheme
+----
 ====
 ======
 
 === CLUSTERPROP Parameters
 
-`name`::
+`name` (v1)::
 +
 [%autowidth,frame=none]
 |===
 |Optional |Default: none
 |===
 +
-The name of the property.
+The name of the property.  Appears in the path of v2 requests.
 Supported properties names are `location`, `maxCoresPerNode`, `urlScheme`, and 
`defaultShardPreferences`.
 If the xref:distributed-tracing.adoc[Jaeger tracing module] has been enabled, 
the property `samplePercentage` is also available.
 +
 Other properties can be set (for example, if you need them for custom plugins) 
but they must begin with the prefix `ext.`.
 Unknown properties that don't begin with `ext.` will be rejected.
 
-`val`::
+`val` (v1), `value` (v2)::
 +
 [%autowidth,frame=none]
 |===
@@ -332,19 +336,17 @@ V2 API::
 ====
 [source,bash]
 ----
-curl -X POST -H 'Content-type:application/json' --data-binary '
+curl -X PUT -H 'Content-type:application/json' --data-binary '
 {
-  "set-obj-property": {
-    "defaults" : {
-      "collection": {
-        "numShards": 2,
-        "nrtReplicas": 1,
-        "tlogReplicas": 1,
-        "pullReplicas": 1
-      }
+  "defaults" : {
+    "collection": {
+      "numShards": 2,
+      "nrtReplicas": 1,
+      "tlogReplicas": 1,
+      "pullReplicas": 1
     }
   }
-}' http://localhost:8983/api/cluster
+}' http://localhost:8983/api/cluster/properties
 ----
 ====
 ======
@@ -353,26 +355,30 @@ curl -X POST -H 'Content-type:application/json' 
--data-binary '
 
 [source,bash]
 ----
-curl -X POST -H 'Content-type:application/json' --data-binary '
+curl -X PUT -H 'Content-type:application/json' --data-binary '
 {
-  "set-obj-property": {
-    "defaults" : {
-      "collection": {
-        "nrtReplicas": null
-      }
+  "defaults" : {
+    "collection": {
+      "nrtReplicas": null
     }
   }
-}' http://localhost:8983/api/cluster
+}' http://localhost:8983/api/cluster/properties
 ----
 
 *Unset all values in `defaults`*
 [source,bash]
 ----
-curl -X POST -H 'Content-type:application/json' --data-binary '
-{ "set-obj-property" : {
-    "defaults" : null
-}' http://localhost:8983/api/cluster
+curl -X PUT -H 'Content-type:application/json' --data-binary '
+{
+  "defaults" : null
+}' http://localhost:8983/api/cluster/properties
+----
+or
+[source,bash]
 ----
+curl -X DELETE http://localhost:8983/api/cluster/properties/defaults
+----
+
 
 === Default Shard Preferences
 
@@ -382,17 +388,103 @@ Then, set the value of `defaultShardPreferences` to 
`node.sysprop:sysprop.YOUR_P
 
 [source,bash]
 ----
-curl -X POST -H 'Content-type:application/json' --data-binary '
+curl -X PUT -H 'Content-type:application/json' --data-binary '
 {
-  "set-property" : {
-    "name" : "defaultShardPreferences",
-    "val" : "node.sysprop:sysprop.rack"
-  }
-}' http://localhost:8983/api/cluster
+  "value" : "node.sysprop:sysprop.rack"
+}' http://localhost:8983/api/cluster/properties/defaultShardPreferences
 ----
 
 At this point, if you run a query on a node having e.g., `rack=rack1`, Solr 
will try to hit only replicas from `rack1`.
 
+
+=== List Cluster Properties
+
+[tabs#setobjproperty-request]
+======
+V1 API::
++
+====
+There is no V1 equivalent of this action.
+
+====
+V2 API::
++
+====
+[source,bash]
+----
+curl -X GET http://localhost:8983/api/cluster/properties
+----
+====
+======
+
+*Input*
+
+[source,bash]
+----
+curl -X GET http://localhost:8983/api/cluster/properties
+----
+
+*Output*
+
+[source,json]
+----
+{
+  "responseHeader": {
+    "status": 0,
+    "QTime": 2
+  },
+  "clusterProperties": [
+    "urlScheme",
+    "defaultShardPreferences"
+  ]
+}
+----
+
+
+=== Fetch Cluster Property
+
+[tabs#setobjproperty-request]
+======
+V1 API::
++
+====
+There is no V1 equivalent of this action.
+
+====
+V2 API::
++
+====
+[source,bash]
+----
+curl -X GET http://localhost:8983/api/cluster/properties/urlScheme
+----
+====
+======
+
+*Input*
+
+[source,bash]
+----
+curl -X GET http://localhost:8983/api/cluster/properties/urlScheme
+----
+
+*Output*
+
+[source,json]
+----
+{
+  "responseHeader": {
+    "status": 0,
+    "QTime": 2
+  },
+  "clusterProperty": {
+    "name": "urlScheme",
+    "value": "https"
+  }
+}
+----
+
+
 [[balancereplicas]]
 == Balance Replicas
 
diff --git a/solr/solrj/src/resources/java-template/api.mustache 
b/solr/solrj/src/resources/java-template/api.mustache
index 757335a2b83..c4ef7f9c59b 100644
--- a/solr/solrj/src/resources/java-template/api.mustache
+++ b/solr/solrj/src/resources/java-template/api.mustache
@@ -133,14 +133,18 @@ public class {{classname}} {
                 {{/requiredParams}}
                 {{#bodyParam}}
                 {{^vendorExtensions.x-genericEntity}}
+                {{#isMap}}
+                    this.requestBody = new HashMap<>();
+                {{/isMap}}
                 {{#isArray}}
                     this.requestBody = new ArrayList<>();
-                    addHeader("Content-type", "application/json");
                 {{/isArray}}
+                {{^isMap}}
                 {{^isArray}}
                     this.requestBody = new {{{dataType}}}();
-                    addHeader("Content-type", "application/json");
                 {{/isArray}}
+                {{/isMap}}
+                    addHeader("Content-type", "application/json");
                 {{/vendorExtensions.x-genericEntity}}
                 {{/bodyParam}}
             }


Reply via email to