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

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


The following commit(s) were added to refs/heads/branch_10x by this push:
     new 7b3de52afe3 SOLR-12224: Add APIs to read collection properties (#4071)
7b3de52afe3 is described below

commit 7b3de52afe33750c4131b35da76f1f06d9b10990
Author: Jason Gerlowski <[email protected]>
AuthorDate: Mon Jan 26 11:25:27 2026 -0500

    SOLR-12224: Add APIs to read collection properties (#4071)
    
    Prior to this PR Solr allowed users to write collection properties but never
    read them.  This commit adds two new APIs to serve this need: the first for
    listing all properties (`GET /api/collections/someCollName/properties`) and 
the
    second for fetching a single property by name (`GET
    /api/collections/someCollName/properties/somePropName`).
    
    Corresponding SolrJ "SolrRequest" and "SolrResponse" classes are also
    generated based on the OAS definition for these new APIs.
---
 .../SOLR-12224-add-collprop-read-apis.yml          |  8 +++
 .../client/api/endpoint/CollectionPropertyApi.java | 23 ++++++++-
 .../api/model/GetCollectionPropertyResponse.java   | 28 ++++++++++
 .../model/ListCollectionPropertiesResponse.java    | 29 +++++++++++
 .../solr/handler/admin/api/CollectionProperty.java | 57 ++++++++++++++++++--
 .../apache/solr/cloud/CollectionsAPISolrJTest.java | 38 +++++++++++---
 .../pages/collection-management.adoc               | 60 +++++++++++++++++++---
 7 files changed, 227 insertions(+), 16 deletions(-)

diff --git a/changelog/unreleased/SOLR-12224-add-collprop-read-apis.yml 
b/changelog/unreleased/SOLR-12224-add-collprop-read-apis.yml
new file mode 100644
index 00000000000..3372502b7a4
--- /dev/null
+++ b/changelog/unreleased/SOLR-12224-add-collprop-read-apis.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Create new v2 APIs for listing and reading collection properties 
("collprops")
+type: added # added, changed, fixed, deprecated, removed, dependency_update, 
security, other
+authors:
+  - name: Jason Gerlowski
+links:
+  - name: SOLR-12224
+    url: https://issues.apache.org/jira/browse/SOLR-12224
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/CollectionPropertyApi.java
 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/CollectionPropertyApi.java
index 8c69aa3ce4c..a4fd54de8db 100644
--- 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/CollectionPropertyApi.java
+++ 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/CollectionPropertyApi.java
@@ -18,16 +18,36 @@ package org.apache.solr.client.api.endpoint;
 
 import io.swagger.v3.oas.annotations.Operation;
 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 org.apache.solr.client.api.model.GetCollectionPropertyResponse;
+import org.apache.solr.client.api.model.ListCollectionPropertiesResponse;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.api.model.UpdateCollectionPropertyRequestBody;
 
 /** V2 API definitions for modifying collection-level properties. */
-@Path("/collections/{collName}/properties/{propName}")
+@Path("/collections/{collName}/properties")
 public interface CollectionPropertyApi {
+  @GET
+  @Operation(
+      summary = "List all properties for the specified collection",
+      tags = {"collection-properties"})
+  ListCollectionPropertiesResponse 
listCollectionProperties(@PathParam("collName") String collName)
+      throws Exception;
+
+  @GET
+  @Path("/{propName}")
+  @Operation(
+      summary = "Get the value of a specific collection property",
+      tags = {"collection-properties"})
+  GetCollectionPropertyResponse getCollectionProperty(
+      @PathParam("collName") String collName, @PathParam("propName") String 
propName)
+      throws Exception;
+
   @PUT
+  @Path("/{propName}")
   @Operation(
       summary = "Create or update a collection property",
       tags = {"collection-properties"})
@@ -38,6 +58,7 @@ public interface CollectionPropertyApi {
       throws Exception;
 
   @DELETE
+  @Path("/{propName}")
   @Operation(
       summary = "Delete the specified collection property from the collection",
       tags = {"collection-properties"})
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/GetCollectionPropertyResponse.java
 
b/solr/api/src/java/org/apache/solr/client/api/model/GetCollectionPropertyResponse.java
new file mode 100644
index 00000000000..0da15ee34bb
--- /dev/null
+++ 
b/solr/api/src/java/org/apache/solr/client/api/model/GetCollectionPropertyResponse.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;
+
+/** The Response for the v2 "get collection property" API */
+public class GetCollectionPropertyResponse extends SolrJerseyResponse {
+
+  @Schema(description = "The value of the collection property.")
+  @JsonProperty("value")
+  public String value;
+}
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionPropertiesResponse.java
 
b/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionPropertiesResponse.java
new file mode 100644
index 00000000000..74479ebf914
--- /dev/null
+++ 
b/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionPropertiesResponse.java
@@ -0,0 +1,29 @@
+/*
+ * 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.Map;
+
+/** The Response for the v2 "list collection properties" API */
+public class ListCollectionPropertiesResponse extends SolrJerseyResponse {
+
+  @Schema(description = "The properties for the collection.")
+  @JsonProperty("properties")
+  public Map<String, String> properties;
+}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionProperty.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionProperty.java
index 55da1ea0d53..c68e6ce21f2 100644
--- 
a/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionProperty.java
+++ 
b/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionProperty.java
@@ -18,9 +18,14 @@
 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.Map;
 import org.apache.solr.client.api.endpoint.CollectionPropertyApi;
+import org.apache.solr.client.api.model.GetCollectionPropertyResponse;
+import org.apache.solr.client.api.model.ListCollectionPropertiesResponse;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.api.model.UpdateCollectionPropertyRequestBody;
 import org.apache.solr.common.SolrException;
@@ -31,13 +36,13 @@ import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 
 /**
- * V2 API implementations for modifying collection-level properties.
+ * V2 API implementations for managing collection-level properties.
  *
- * <p>These APIs (PUT and DELETE 
/api/collections/collName/properties/propName) are analogous to the
- * v1 /admin/collections?action=COLLECTIONPROP command.
+ * <p>These APIs are analogous to the v1 
/admin/collections?action=COLLECTIONPROP command.
  */
 public class CollectionProperty extends AdminAPIBase implements 
CollectionPropertyApi {
 
+  @Inject
   public CollectionProperty(
       CoreContainer coreContainer,
       SolrQueryRequest solrQueryRequest,
@@ -45,6 +50,52 @@ public class CollectionProperty extends AdminAPIBase 
implements CollectionProper
     super(coreContainer, solrQueryRequest, solrQueryResponse);
   }
 
+  @Override
+  @PermissionName(COLL_READ_PERM)
+  public ListCollectionPropertiesResponse listCollectionProperties(String 
collName)
+      throws Exception {
+    final var response = 
instantiateJerseyResponse(ListCollectionPropertiesResponse.class);
+    ensureRequiredParameterProvided("collName", collName);
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    recordCollectionForLogAndTracing(collName, solrQueryRequest);
+
+    String resolvedCollection = 
coreContainer.getAliases().resolveSimpleAlias(collName);
+    CollectionProperties cp =
+        new 
CollectionProperties(coreContainer.getZkController().getZkClient());
+    Map<String, String> properties = 
cp.getCollectionProperties(resolvedCollection);
+
+    // Handle null case - return empty map instead of null
+    response.properties = (properties != null) ? properties : Map.of();
+
+    return response;
+  }
+
+  @Override
+  @PermissionName(COLL_READ_PERM)
+  public GetCollectionPropertyResponse getCollectionProperty(String collName, 
String propName)
+      throws Exception {
+    final var response = 
instantiateJerseyResponse(GetCollectionPropertyResponse.class);
+    ensureRequiredParameterProvided("collName", collName);
+    ensureRequiredParameterProvided("propName", propName);
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    recordCollectionForLogAndTracing(collName, solrQueryRequest);
+
+    String resolvedCollection = 
coreContainer.getAliases().resolveSimpleAlias(collName);
+    CollectionProperties cp =
+        new 
CollectionProperties(coreContainer.getZkController().getZkClient());
+    Map<String, String> properties = 
cp.getCollectionProperties(resolvedCollection);
+
+    if (properties != null && properties.containsKey(propName)) {
+      response.value = properties.get(propName);
+    } else {
+      throw new SolrException(
+          SolrException.ErrorCode.NOT_FOUND,
+          "Property '" + propName + "' not found for collection '" + collName 
+ "'");
+    }
+
+    return response;
+  }
+
   @Override
   @PermissionName(COLL_EDIT_PERM)
   public SolrJerseyResponse createOrUpdateCollectionProperty(
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 992ec12f450..60636d30c92 100644
--- a/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java
@@ -49,6 +49,7 @@ import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.CollectionPropertiesApi;
 import org.apache.solr.client.solrj.request.CollectionsApi;
 import org.apache.solr.client.solrj.request.CoreAdminRequest;
 import org.apache.solr.client.solrj.request.V2Request;
@@ -536,7 +537,7 @@ public class CollectionsAPISolrJTest extends 
SolrCloudTestCase {
   }
 
   @Test
-  public void testCollectionProp() throws InterruptedException, IOException, 
SolrServerException {
+  public void testCollectionProp() throws Exception {
     String collectionName = getSaferTestName();
     final String propName = "testProperty";
 
@@ -554,18 +555,43 @@ public class CollectionsAPISolrJTest extends 
SolrCloudTestCase {
     CollectionAdminRequest.setCollectionProperty(collectionName, propName, 
null)
         .process(cluster.getSolrClient());
     checkCollectionProperty(collectionName, propName, null);
+
+    // Test that "list-properties" returns all properties
+    CollectionAdminRequest.setCollectionProperty(collectionName, propName + 
"1", "prop1Val")
+        .process(cluster.getSolrClient());
+    CollectionAdminRequest.setCollectionProperty(collectionName, propName + 
"2", "prop2Val")
+        .process(cluster.getSolrClient());
+    final var allProperties =
+        new CollectionPropertiesApi.ListCollectionProperties(collectionName)
+            .process(cluster.getSolrClient())
+            .properties;
+    assertEquals(2, allProperties.size());
+    assertEquals("prop1Val", allProperties.get(propName + "1"));
+    assertEquals("prop2Val", allProperties.get(propName + "2"));
+
+    // Test GET single property API
+    final var prop1Response =
+        new CollectionPropertiesApi.GetCollectionProperty(collectionName, 
propName + "1")
+            .process(cluster.getSolrClient());
+    assertEquals("prop1Val", prop1Response.value);
+
+    final var prop2Response =
+        new CollectionPropertiesApi.GetCollectionProperty(collectionName, 
propName + "2")
+            .process(cluster.getSolrClient());
+    assertEquals("prop2Val", prop2Response.value);
   }
 
   private void checkCollectionProperty(String collection, String propertyName, 
String propertyValue)
-      throws InterruptedException {
+      throws Exception {
     TimeOut timeout = new TimeOut(TIMEOUT, TimeUnit.MILLISECONDS, 
TimeSource.NANO_TIME);
     while (!timeout.hasTimedOut()) {
-      Thread.sleep(10);
-      if (Objects.equals(
-          
cluster.getZkStateReader().getCollectionProperties(collection).get(propertyName),
-          propertyValue)) {
+      final var listCollPropRsp =
+          new CollectionPropertiesApi.ListCollectionProperties(collection)
+              .process(cluster.getSolrClient());
+      if (Objects.equals(listCollPropRsp.properties.get(propertyName), 
propertyValue)) {
         return;
       }
+      Thread.sleep(10);
     }
 
     fail("Timed out waiting for cluster property value");
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
index 90006a03e94..f34f700e814 100644
--- 
a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
@@ -673,7 +673,8 @@ If the status is anything other than "success", an error 
message will explain wh
 [[collectionprop]]
 == COLLECTIONPROP: Collection Properties
 
-Add, edit or delete a collection property.
+Add, update, delete, or retrieve collection properties.
+(Listing all collection properties, or fetching an individual property by name 
are only supported in Solr's v2 API)
 
 [tabs#collectionproperty-request]
 ======
@@ -682,7 +683,7 @@ V1 API::
 ====
 [source,bash]
 ----
-http://localhost:8983/solr/admin/collections?action=COLLECTIONPROP&name=techproducts_v2&propertyName=propertyName&propertyValue=propertyValue
+http://localhost:8983/solr/admin/collections?action=COLLECTIONPROP&name=techproducts&propertyName=propertyName&propertyValue=propertyValue
 ----
 ====
 
@@ -690,20 +691,36 @@ V2 API::
 +
 ====
 To create or update a collection property:
+
 [source,bash]
 ----
-curl -X PUT 
http://localhost:8983/api/collections/techproducts_v2/properties/foo -H 
'Content-Type: application/json' -d '
+curl -X PUT 
http://localhost:8983/api/collections/techproducts_v2/properties/propertyName 
-H 'Content-Type: application/json' -d '
   {
-    "value": "bar"
+    "value": "propertyValue"
   }
 '
 ----
 
+To list all properties for a collection:
+
+[source,bash]
+----
+curl http://localhost:8983/api/collections/techproducts/properties
+----
+
+To get a specific collection property by name:
+
+[source,bash]
+----
+curl http://localhost:8983/api/collections/techproducts/properties/propertyName
+----
+
+
 To delete an existing collection property:
 
 [source,bash]
 ----
-curl -X DELETE 
http://localhost:8983/api/collections/techproducts_v2/properties/foo 
+curl -X DELETE 
http://localhost:8983/api/collections/techproducts/properties/propertyName
 ----
 ====
 ======
@@ -742,9 +759,40 @@ When not provided in v1 requests, the property is deleted.
 
 === COLLECTIONPROP Response
 
-The response will include the status of the request and the properties that 
were updated or removed.
+The response will include the status of the request.
 If the status is anything other than "0", an error message will explain why 
the request failed.
 
+For GET requests to list all properties, the response includes a `properties` 
object containing all property name-value pairs:
+
+[source,json]
+----
+{
+  "responseHeader": {
+    "status": 0,
+    "QTime": 5
+  },
+  "properties": {
+    "foo": "bar",
+    "myProperty": "myValue"
+  }
+}
+----
+
+For GET requests to retrieve a single property, the response includes a 
`value` field with the property value:
+
+[source,json]
+----
+{
+  "responseHeader": {
+    "status": 0,
+    "QTime": 3
+  },
+  "value": "bar"
+}
+----
+
+If a requested property does not exist, the API will return a 404 error with 
an appropriate error message.
+
 [[migrate]]
 == MIGRATE: Migrate Documents to Another Collection
 

Reply via email to