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

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


The following commit(s) were added to refs/heads/main by this push:
     new 340e604d0ed SOLR-16458: Migrate node properties API to JAX-RS (#4775)
340e604d0ed is described below

commit 340e604d0eda6dabebf572bd4656f4b9b273d27e
Author: Prithvi S <[email protected]>
AuthorDate: Fri Aug 28 17:20:08 2026 +0530

    SOLR-16458: Migrate node properties API to JAX-RS (#4775)
    
    Signed-off-by: prithvi <[email protected]>
    Co-authored-by: Eric Pugh <[email protected]>
---
 .../SOLR-16458-migrate-node-properties-api.yml     |   8 ++
 .../client/api/endpoint/NodePropertiesApi.java     |  44 ++++++++
 .../client/api/model/NodePropertiesResponse.java   |  34 ++++++
 .../handler/admin/PropertiesRequestHandler.java    |  40 ++++---
 .../solr/handler/admin/api/GetNodeProperties.java  |  92 ++++++++++++++++
 .../solr/handler/admin/api/NodePropertiesAPI.java  |  47 --------
 .../admin/PropertiesRequestHandlerTest.java        |  40 ++++++-
 .../handler/admin/api/GetNodePropertiesTest.java   | 120 +++++++++++++++++++++
 .../handler/admin/api/V2NodeAPIMappingTest.java    |  24 -----
 .../pages/implicit-requesthandlers.adoc            |   9 +-
 .../deployment-guide/pages/jvm-settings.adoc       |   4 +
 .../solr/client/solrj/impl/NodeValueFetcher.java   |  16 ++-
 12 files changed, 378 insertions(+), 100 deletions(-)

diff --git a/changelog/unreleased/SOLR-16458-migrate-node-properties-api.yml 
b/changelog/unreleased/SOLR-16458-migrate-node-properties-api.yml
new file mode 100644
index 00000000000..0b45680bdec
--- /dev/null
+++ b/changelog/unreleased/SOLR-16458-migrate-node-properties-api.yml
@@ -0,0 +1,8 @@
+title: "A single node system property can be fetched at GET 
/api/node/properties/{propertyName} (v1 still uses ?name=). SolrJ now provides 
NodeApi.GetNodeProperties and NodeApi.GetNodeProperty."
+type: added
+authors:
+  - name: Prithvi S
+    nick: iprithv
+links:
+  - name: SOLR-16458
+    url: https://issues.apache.org/jira/browse/SOLR-16458
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/NodePropertiesApi.java 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/NodePropertiesApi.java
new file mode 100644
index 00000000000..7c986e5ad82
--- /dev/null
+++ 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/NodePropertiesApi.java
@@ -0,0 +1,44 @@
+/*
+ * 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 jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.NodePropertiesResponse;
+
+/** V2 API definition for listing JRE system properties on a Solr node. */
+@Path("/node/properties")
+public interface NodePropertiesApi {
+
+  @GET
+  @Operation(
+      summary = "List system properties for the target Solr node.",
+      tags = {"node"})
+  NodePropertiesResponse getNodeProperties();
+
+  @GET
+  @Path("/{propertyName}")
+  @Operation(
+      summary = "Get a single system property for the target Solr node.",
+      tags = {"node"})
+  NodePropertiesResponse getNodeProperty(
+      @Parameter(description = "Name of the system property to return.") 
@PathParam("propertyName")
+          String propertyName);
+}
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/NodePropertiesResponse.java
 
b/solr/api/src/java/org/apache/solr/client/api/model/NodePropertiesResponse.java
new file mode 100644
index 00000000000..81ca0331fdd
--- /dev/null
+++ 
b/solr/api/src/java/org/apache/solr/client/api/model/NodePropertiesResponse.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * Response body for {@code GET /api/node/properties} and {@code GET
+ * /api/node/properties/{propertyName}}.
+ */
+public class NodePropertiesResponse extends SolrJerseyResponse {
+
+  public static final String SYSTEM_PROPERTIES = "system.properties";
+
+  @Schema(description = "JRE system properties for the Solr node. Secret 
values are redacted.")
+  @JsonProperty(SYSTEM_PROPERTIES)
+  public Map<String, String> systemProperties;
+}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/PropertiesRequestHandler.java
 
b/solr/core/src/java/org/apache/solr/handler/admin/PropertiesRequestHandler.java
index 8658adf3c52..fb290bdd1a2 100644
--- 
a/solr/core/src/java/org/apache/solr/handler/admin/PropertiesRequestHandler.java
+++ 
b/solr/core/src/java/org/apache/solr/handler/admin/PropertiesRequestHandler.java
@@ -20,20 +20,22 @@ import static 
org.apache.solr.common.params.CommonParams.NAME;
 
 import java.io.IOException;
 import java.util.Collection;
-import java.util.Enumeration;
-import org.apache.solr.api.AnnotatedApi;
+import java.util.List;
 import org.apache.solr.api.Api;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.model.NodePropertiesResponse;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.core.NodeConfig;
 import org.apache.solr.handler.RequestHandlerBase;
-import org.apache.solr.handler.admin.api.NodePropertiesAPI;
+import org.apache.solr.handler.admin.api.GetNodeProperties;
+import org.apache.solr.handler.api.V2ApiUtils;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.security.AuthorizationContext;
 
 /**
+ * v1 implementation of {@code GET /admin/info/properties}. Business logic 
lives in {@link
+ * GetNodeProperties}.
+ *
  * @since solr 1.2
  */
 public class PropertiesRequestHandler extends RequestHandlerBase {
@@ -51,21 +53,12 @@ public class PropertiesRequestHandler extends 
RequestHandlerBase {
 
   @Override
   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) 
throws IOException {
-    NamedList<String> props = new SimpleOrderedMap<>();
-    String name = req.getParams().get(NAME);
-    NodeConfig nodeConfig = getCoreContainer(req).getNodeConfig();
-    if (name != null) {
-      String property = nodeConfig.getRedactedSysPropValue(name);
-      props.add(name, property);
-    } else {
-      Enumeration<?> enumeration = System.getProperties().propertyNames();
-      while (enumeration.hasMoreElements()) {
-        name = (String) enumeration.nextElement();
-        props.add(name, nodeConfig.getRedactedSysPropValue(name));
-      }
-    }
-    rsp.add("system.properties", props);
     rsp.setHttpCaching(false);
+    final GetNodeProperties api = new GetNodeProperties(getCoreContainer(req));
+    final NodePropertiesResponse response = new NodePropertiesResponse();
+    // v1 ?name= returns the key even if unset; the v2 path form 404s for 
unknown names.
+    response.systemProperties = 
api.collectProperties(req.getParams().get(NAME));
+    V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, response);
   }
 
   //////////////////////// SolrInfoMBeans methods //////////////////////
@@ -82,7 +75,12 @@ public class PropertiesRequestHandler extends 
RequestHandlerBase {
 
   @Override
   public Collection<Api> getApis() {
-    return AnnotatedApi.getApis(new NodePropertiesAPI(this));
+    return List.of();
+  }
+
+  @Override
+  public Collection<Class<? extends JerseyResource>> getJerseyResources() {
+    return List.of(GetNodeProperties.class);
   }
 
   @Override
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/GetNodeProperties.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/GetNodeProperties.java
new file mode 100644
index 00000000000..464ec22c687
--- /dev/null
+++ 
b/solr/core/src/java/org/apache/solr/handler/admin/api/GetNodeProperties.java
@@ -0,0 +1,92 @@
+/*
+ * 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 jakarta.inject.Inject;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.endpoint.NodePropertiesApi;
+import org.apache.solr.client.api.model.NodePropertiesResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.NodeConfig;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.security.PermissionNameProvider;
+
+/**
+ * V2 API for listing system properties on the receiving node.
+ *
+ * <p>GET /api/node/properties lists all properties. GET 
/api/node/properties/{propertyName} returns
+ * a single property. Both are analogous to v1 /admin/info/properties, which 
still uses a {@code
+ * name} query parameter for the single-property form.
+ *
+ * <p>The v1 {@link org.apache.solr.handler.admin.PropertiesRequestHandler} 
delegates to this class.
+ */
+public class GetNodeProperties extends JerseyResource implements 
NodePropertiesApi {
+
+  private final CoreContainer coreContainer;
+
+  @Inject
+  public GetNodeProperties(CoreContainer coreContainer) {
+    this.coreContainer = coreContainer;
+  }
+
+  @Override
+  @PermissionName(PermissionNameProvider.Name.CONFIG_READ_PERM)
+  public NodePropertiesResponse getNodeProperties() {
+    return buildResponse(null);
+  }
+
+  @Override
+  @PermissionName(PermissionNameProvider.Name.CONFIG_READ_PERM)
+  public NodePropertiesResponse getNodeProperty(String propertyName) {
+    final NodeConfig nodeConfig = coreContainer.getNodeConfig();
+    // Hidden names always return 200 + a redacted value, even if unset, so 
callers cannot
+    // probe whether a secret is configured.
+    if (!System.getProperties().containsKey(propertyName)
+        && !nodeConfig.isSysPropHidden(propertyName)) {
+      throw new SolrException(
+          SolrException.ErrorCode.NOT_FOUND,
+          "No system property named '" + propertyName + "' exists on this 
node.");
+    }
+    return buildResponse(propertyName);
+  }
+
+  private NodePropertiesResponse buildResponse(String name) {
+    final NodePropertiesResponse response = 
instantiateJerseyResponse(NodePropertiesResponse.class);
+    response.systemProperties = collectProperties(name);
+    return response;
+  }
+
+  /** Collect redacted system properties, optionally limited to a single named 
property. */
+  public Map<String, String> collectProperties(String name) {
+    final NodeConfig nodeConfig = coreContainer.getNodeConfig();
+    final Map<String, String> props = new LinkedHashMap<>();
+    if (name != null) {
+      props.put(name, nodeConfig.getRedactedSysPropValue(name));
+    } else {
+      Enumeration<?> enumeration = System.getProperties().propertyNames();
+      while (enumeration.hasMoreElements()) {
+        String propertyName = (String) enumeration.nextElement();
+        props.put(propertyName, 
nodeConfig.getRedactedSysPropValue(propertyName));
+      }
+    }
+    return props;
+  }
+}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/NodePropertiesAPI.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/NodePropertiesAPI.java
deleted file mode 100644
index d9cf81f8a8b..00000000000
--- 
a/solr/core/src/java/org/apache/solr/handler/admin/api/NodePropertiesAPI.java
+++ /dev/null
@@ -1,47 +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 static 
org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM;
-
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.handler.admin.PropertiesRequestHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-
-/**
- * V2 API for listing system properties for each node.
- *
- * <p>This API (GET /v2/node/properties) is analogous to the v1 
/admin/info/properties.
- */
-public class NodePropertiesAPI {
-  private final PropertiesRequestHandler handler;
-
-  public NodePropertiesAPI(PropertiesRequestHandler handler) {
-    this.handler = handler;
-  }
-
-  @EndPoint(
-      path = {"/node/properties"},
-      method = GET,
-      permission = CONFIG_READ_PERM)
-  public void getRequestedProperties(SolrQueryRequest req, SolrQueryResponse 
rsp) throws Exception {
-    handler.handleRequestBody(req, rsp);
-  }
-}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/PropertiesRequestHandlerTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/PropertiesRequestHandlerTest.java
index f64b96ae311..7202a6b5a54 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/PropertiesRequestHandlerTest.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/PropertiesRequestHandlerTest.java
@@ -16,11 +16,13 @@
  */
 package org.apache.solr.handler.admin;
 
+import java.util.Map;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
 import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.core.NodeConfig;
 import org.junit.BeforeClass;
@@ -45,7 +47,7 @@ public class PropertiesRequestHandlerTest extends 
SolrTestCaseJ4 {
           "some.Secret"
         }) {
       System.setProperty(propName, PASSWORD);
-      NamedList<Object> properties = readProperties();
+      Map<String, Object> properties = readProperties();
 
       assertEquals(
           "Failed to redact " + propName,
@@ -54,13 +56,41 @@ public class PropertiesRequestHandlerTest extends 
SolrTestCaseJ4 {
     }
   }
 
+  @Test
+  public void testSingleProperty() throws Exception {
+    System.setProperty("GetNodeProperties.v1.visible", "hello");
+    try {
+      Map<String, Object> properties = 
readProperties("GetNodeProperties.v1.visible");
+      assertEquals(1, properties.size());
+      assertEquals("hello", properties.get("GetNodeProperties.v1.visible"));
+    } finally {
+      System.clearProperty("GetNodeProperties.v1.visible");
+    }
+  }
+
+  @Test
+  public void testMissingPropertyStillReturned() throws Exception {
+    Map<String, Object> properties = 
readProperties("GetNodeProperties.v1.doesNotExist");
+    assertEquals(1, properties.size());
+    assertTrue(properties.containsKey("GetNodeProperties.v1.doesNotExist"));
+    assertNull(properties.get("GetNodeProperties.v1.doesNotExist"));
+  }
+
+  private Map<String, Object> readProperties() throws Exception {
+    return readProperties(null);
+  }
+
   @SuppressWarnings({"unchecked"})
-  private NamedList<Object> readProperties() throws Exception {
+  private Map<String, Object> readProperties(String name) throws Exception {
     SolrClient client = new EmbeddedSolrServer(h.getCore());
-
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    if (name != null) {
+      params.set("name", name);
+    }
     NamedList<Object> properties =
-        client.request(new GenericSolrRequest(SolrRequest.METHOD.GET, 
"/admin/info/properties"));
+        client.request(
+            new GenericSolrRequest(SolrRequest.METHOD.GET, 
"/admin/info/properties", params));
 
-    return (NamedList<Object>) properties.get("system.properties");
+    return (Map<String, Object>) properties.get("system.properties");
   }
 }
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/GetNodePropertiesTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/api/GetNodePropertiesTest.java
new file mode 100644
index 00000000000..c00a8822310
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/GetNodePropertiesTest.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 java.util.Collections;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.client.api.model.NodePropertiesResponse;
+import org.apache.solr.client.solrj.RemoteSolrException;
+import org.apache.solr.client.solrj.request.NodeApi;
+import org.apache.solr.core.NodeConfig;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/**
+ * HTTP tests for {@code GET /api/node/properties} and {@code GET
+ * /api/node/properties/{propertyName}} via the generated SolrJ client classes.
+ */
+public class GetNodePropertiesTest extends SolrTestCase {
+
+  private static final String VISIBLE_PROP = "GetNodePropertiesTest.visible";
+  private static final String SECRET_PROP = "GetNodePropertiesTest.password";
+  private static final String PASSWORD = "secret123";
+
+  @ClassRule public static final SolrJettyTestRule solrTestRule = new 
SolrJettyTestRule();
+
+  @BeforeClass
+  public static void setupSolr() throws Exception {
+    solrTestRule.startSolr(createTempDir());
+  }
+
+  @After
+  public void clearTestProperties() {
+    System.clearProperty(VISIBLE_PROP);
+    System.clearProperty(SECRET_PROP);
+  }
+
+  @Test
+  public void testNamedProperty() throws Exception {
+    var req = new NodeApi.GetNodeProperty("java.version");
+    var rsp = req.process(solrTestRule.getAdminClient());
+
+    assertNotNull(rsp);
+    assertNull(rsp.error);
+    assertEquals(1, rsp.systemProperties.size());
+    assertEquals(System.getProperty("java.version"), 
rsp.systemProperties.get("java.version"));
+  }
+
+  @Test
+  public void testAllProperties() throws Exception {
+    System.setProperty(VISIBLE_PROP, "hello");
+
+    NodePropertiesResponse rsp = fetchProperties(null);
+
+    assertEquals(
+        Collections.list(System.getProperties().propertyNames()).size(),
+        rsp.systemProperties.size());
+    assertEquals(System.getProperty("java.version"), 
rsp.systemProperties.get("java.version"));
+    assertEquals("hello", rsp.systemProperties.get(VISIBLE_PROP));
+  }
+
+  @Test
+  public void testRedactsHiddenProperties() throws Exception {
+    System.setProperty(SECRET_PROP, PASSWORD);
+
+    NodePropertiesResponse named = fetchProperties(SECRET_PROP);
+    assertEquals(1, named.systemProperties.size());
+    assertEquals(NodeConfig.REDACTED_SYS_PROP_VALUE, 
named.systemProperties.get(SECRET_PROP));
+    assertFalse(named.systemProperties.containsValue(PASSWORD));
+
+    NodePropertiesResponse all = fetchProperties(null);
+    assertEquals(NodeConfig.REDACTED_SYS_PROP_VALUE, 
all.systemProperties.get(SECRET_PROP));
+    assertFalse(all.systemProperties.containsValue(PASSWORD));
+  }
+
+  @Test
+  public void testUnknownPropertyReturns404() {
+    var req = new 
NodeApi.GetNodeProperty("GetNodePropertiesTest.doesNotExist");
+    final RemoteSolrException ex =
+        expectThrows(RemoteSolrException.class, () -> 
req.process(solrTestRule.getAdminClient()));
+    assertEquals(404, ex.code());
+  }
+
+  @Test
+  public void testUnknownHiddenPropertyDoesNotRevealExistence() throws 
Exception {
+    final String hiddenUnset = "GetNodePropertiesTest.doesNotExist.password";
+    assertFalse(System.getProperties().containsKey(hiddenUnset));
+
+    NodePropertiesResponse rsp = fetchProperties(hiddenUnset);
+    assertEquals(1, rsp.systemProperties.size());
+    assertEquals(NodeConfig.REDACTED_SYS_PROP_VALUE, 
rsp.systemProperties.get(hiddenUnset));
+  }
+
+  private NodePropertiesResponse fetchProperties(String name) throws Exception 
{
+    NodePropertiesResponse rsp =
+        name == null
+            ? new 
NodeApi.GetNodeProperties().process(solrTestRule.getAdminClient())
+            : new 
NodeApi.GetNodeProperty(name).process(solrTestRule.getAdminClient());
+    assertNotNull(rsp);
+    assertNull(rsp.error);
+    assertNotNull(rsp.systemProperties);
+    return rsp;
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
index 6b3c63de45b..ac2b660bc88 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
@@ -35,8 +35,6 @@ import org.apache.solr.common.util.ContentStreamBase;
 import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.handler.admin.CoreAdminHandler;
 import org.apache.solr.handler.admin.InfoHandler;
-import org.apache.solr.handler.admin.LoggingHandler;
-import org.apache.solr.handler.admin.PropertiesRequestHandler;
 import org.apache.solr.handler.admin.ThreadDumpHandler;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrQueryRequestBase;
@@ -52,8 +50,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
   private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
   private CoreAdminHandler mockCoresHandler;
   private InfoHandler infoHandler;
-  private LoggingHandler mockLoggingHandler;
-  private PropertiesRequestHandler mockPropertiesHandler;
   private ThreadDumpHandler mockThreadDumpHandler;
 
   @BeforeClass
@@ -65,13 +61,9 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
   public void setupApiBag() {
     mockCoresHandler = mock(CoreAdminHandler.class);
     infoHandler = mock(InfoHandler.class);
-    mockLoggingHandler = mock(LoggingHandler.class);
-    mockPropertiesHandler = mock(PropertiesRequestHandler.class);
     mockThreadDumpHandler = mock(ThreadDumpHandler.class);
     queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
 
-    when(infoHandler.getLoggingHandler()).thenReturn(mockLoggingHandler);
-    when(infoHandler.getPropertiesHandler()).thenReturn(mockPropertiesHandler);
     when(infoHandler.getThreadDumpHandler()).thenReturn(mockThreadDumpHandler);
 
     apiBag = new ApiBag(false);
@@ -116,16 +108,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
     assertEquals("true", v1Params.get("rejoinAtHead"));
   }
 
-  @Test
-  public void testSystemPropsApiAllProperties() throws Exception {
-    final ModifiableSolrParams solrParams = new ModifiableSolrParams();
-    solrParams.add("name", "specificPropertyName");
-    final SolrParams v1Params =
-        captureConvertedPropertiesV1Params("/node/properties", "GET", 
solrParams);
-
-    assertEquals("specificPropertyName", v1Params.get("name"));
-  }
-
   @Test
   public void testThreadDumpApiAllProperties() throws Exception {
     final ModifiableSolrParams solrParams = new ModifiableSolrParams();
@@ -143,11 +125,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
         path, method, new ModifiableSolrParams(), v2RequestBody, 
mockCoresHandler);
   }
 
-  private SolrParams captureConvertedPropertiesV1Params(
-      String path, String method, SolrParams inputParams) throws Exception {
-    return doCaptureParams(path, method, inputParams, null, 
mockPropertiesHandler);
-  }
-
   private SolrParams captureConvertedThreadDumpV1Params(
       String path, String method, SolrParams inputParams) throws Exception {
     return doCaptureParams(path, method, inputParams, null, 
mockThreadDumpHandler);
@@ -188,7 +165,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
       ApiBag apiBag, CoreAdminHandler coreHandler, InfoHandler infoHandler) {
     apiBag.registerObject(new OverseerOperationAPI(coreHandler));
     apiBag.registerObject(new RejoinLeaderElectionAPI(coreHandler));
-    apiBag.registerObject(new 
NodePropertiesAPI(infoHandler.getPropertiesHandler()));
     apiBag.registerObject(new 
NodeThreadsAPI(infoHandler.getThreadDumpHandler()));
   }
 }
diff --git 
a/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc
 
b/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc
index 4380337752c..005742a7cec 100644
--- 
a/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc
+++ 
b/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc
@@ -92,14 +92,21 @@ This handler must have a collection name in the path to the 
endpoint.
 |===
 
 System Properties:: Return JRE system properties.
+Secret values are redacted.
++
+*Documentation*: 
xref:deployment-guide:jvm-settings.adoc#java-properties-screen[Java Properties 
Screen]
 +
 [cols="3*.",frame=none,grid=cols,options="header"]
 |===
 |API Endpoints |Class & Javadocs |Paramset
 |v1: `solr/admin/info/properties`
 
-v2: `api/node/properties` 
|{solr-javadocs}/core/org/apache/solr/handler/admin/PropertiesRequestHandler.html[PropertiesRequestHandler]
 |`_ADMIN_PROPERTIES`
+v2: `api/node/properties` |v1: 
{solr-javadocs}/core/org/apache/solr/handler/admin/PropertiesRequestHandler.html[PropertiesRequestHandler]
+
+v2: 
{solr-javadocs}/core/org/apache/solr/handler/admin/api/GetNodeProperties.html[GetNodeProperties]
 |`_ADMIN_PROPERTIES`
 |===
++
+To fetch a single property, v1 uses a `name` query parameter 
(`solr/admin/info/properties?name=java.version`) and v2 uses a path segment 
(`api/node/properties/java.version`).
 
 Segments:: Return info on last commit generation Lucene index segments.
 +
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/jvm-settings.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/jvm-settings.adoc
index a40cdc4073e..5c9dc071163 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/jvm-settings.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/jvm-settings.adoc
@@ -72,6 +72,10 @@ Test this by running `java -help` and look for `-server` as 
an available option
 A great way to see what JVM settings your server is using, along with other 
useful information, is to use the `admin` request handler, 
`/solr/admin/info/system`.
 This request handler will display a wealth of server statistics and settings.
 
+The JVM's system properties are available from `GET 
/solr/admin/info/properties` (v1) or `GET /api/node/properties` (v2).
+A single property can be requested with v1 `?name=java.version` or v2 `GET 
/api/node/properties/java.version`.
+Secret values are redacted.
+
 === Java Properties Screen
 
 Many of the system environment variables include Java settings, and these can 
be seen on the main Dashboard of the Admin UI.
diff --git 
a/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/NodeValueFetcher.java
 
b/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/NodeValueFetcher.java
index abd31c5d86c..917eb59ceea 100644
--- 
a/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/NodeValueFetcher.java
+++ 
b/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/NodeValueFetcher.java
@@ -26,6 +26,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -243,9 +244,10 @@ public class NodeValueFetcher {
     ModifiableSolrParams params = new ModifiableSolrParams();
     try {
       SimpleSolrResponse rsp = ctx.invokeWithRetry(ctx.getNode(), 
"/admin/info/properties", params);
-      NamedList<?> systemPropsRsp = (NamedList<?>) 
rsp.getResponse().get("system.properties");
+      Object systemPropsRsp = rsp.getResponse().get("system.properties");
       for (String requestedProperty : requestedTagNames) {
-        Object property = 
systemPropsRsp.get(requestedProperty.substring(SYSPROP_PREFIX.length()));
+        String key = requestedProperty.substring(SYSPROP_PREFIX.length());
+        Object property = getSysProp(systemPropsRsp, key);
         if (property != null) ctx.tags.put(requestedProperty, 
property.toString());
       }
     } catch (Exception e) {
@@ -253,6 +255,16 @@ public class NodeValueFetcher {
     }
   }
 
+  private static Object getSysProp(Object systemProperties, String key) {
+    if (systemProperties instanceof NamedList<?> namedList) {
+      return namedList.get(key);
+    }
+    if (systemProperties instanceof Map<?, ?> map) {
+      return map.get(key);
+    }
+    return null;
+  }
+
   /**
    * Retrieve values that match metrics. Metrics names are structured like 
below:
    *

Reply via email to