You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ds...@apache.org on 2026/10/17 22:38:44 UTC

(solr) branch branch_10x updated (0712c1f7831 -> 3a6604400fb)

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

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


    from 0712c1f7831 releaseWizard: no Jenkins jobs for release branches (#5067)
     new 8b9ac93d1c1 SOLR-18515: Preserve hyphenated picocli command names in generated CLI docs (#5068)
     new 53bf2c09997 SOLR-16396 | Migrate GET config sections updateHandler, directoryFactory, indexConfig, and codecFactory to JAX-RS. (#5023)
     new 3a6604400fb SOLR-16396 | Migrate GET config sections query, searchComponent & queryResponseWriter to JAX-RS (#5125)

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


Summary of changes:
 ...rs.yml => SOLR-16396-config-get-components.yml} |   2 +-
 ...-get-others.yml => SOLR-16396-config-query.yml} |   4 +-
 .../apache/solr/client/api/endpoint/ConfigApi.java |  35 +++++
 .../org/apache/solr/handler/SolrConfigHandler.java |  61 ++-------
 .../apache/solr/handler/admin/api/GetConfig.java   | 148 ++++++++++++++++++++-
 .../solr/handler/admin/api/GetConfigAPI.java       |  12 +-
 .../solr/handler/admin/V2ConfigAPIMappingTest.java |   2 +-
 .../handler/admin/api/GetConfigSolrCloudTest.java  |  16 +++
 .../solr/handler/admin/api/GetConfigTest.java      |  92 +++++++++++++
 solr/solr-ref-guide/build.gradle                   |  60 +++++----
 .../configuration-guide/pages/config-api.adoc      |  49 +++++++
 .../modules/deployment-guide/deployment-nav.adoc   |  10 +-
 .../modules/deployment-guide/pages/cli/index.adoc  |  10 +-
 .../pages/cli/solr-package-add-key.adoc            |   2 +-
 .../pages/cli/solr-package-add-repo.adoc           |   2 +-
 .../pages/cli/solr-package-list-available.adoc     |   2 +-
 .../pages/cli/solr-package-list-deployed.adoc      |   2 +-
 .../pages/cli/solr-package-list-installed.adoc     |   2 +-
 18 files changed, 401 insertions(+), 110 deletions(-)
 copy changelog/unreleased/{SOLR-16396-config-get-others.yml => SOLR-16396-config-get-components.yml} (60%)
 copy changelog/unreleased/{SOLR-16396-config-get-others.yml => SOLR-16396-config-query.yml} (56%)


(solr) 01/03: SOLR-18515: Preserve hyphenated picocli command names in generated CLI docs (#5068)

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

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

commit 8b9ac93d1c1f39c17c3d96683ce17de0048e0250
Author: Jalaz Kumar <ja...@gmail.com>
AuthorDate: Thu Oct 15 22:15:44 2026 +0530

    SOLR-18515: Preserve hyphenated picocli command names in generated CLI docs (#5068)
    
    (cherry picked from commit c9c87d7836856ab1df01f9de50f2edb581b7535e)
---
 solr/solr-ref-guide/build.gradle                   | 60 ++++++++++++----------
 .../modules/deployment-guide/deployment-nav.adoc   | 10 ++--
 .../modules/deployment-guide/pages/cli/index.adoc  | 10 ++--
 .../pages/cli/solr-package-add-key.adoc            |  2 +-
 .../pages/cli/solr-package-add-repo.adoc           |  2 +-
 .../pages/cli/solr-package-list-available.adoc     |  2 +-
 .../pages/cli/solr-package-list-deployed.adoc      |  2 +-
 .../pages/cli/solr-package-list-installed.adoc     |  2 +-
 8 files changed, 48 insertions(+), 42 deletions(-)

diff --git a/solr/solr-ref-guide/build.gradle b/solr/solr-ref-guide/build.gradle
index 80ee1136386..c84ce7c4b10 100644
--- a/solr/solr-ref-guide/build.gradle
+++ b/solr/solr-ref-guide/build.gradle
@@ -609,16 +609,28 @@ def DO_NOT_EDIT_NOTICE = """\
 """
 
 // Convert a generated filename stem (e.g. "solr-zk-ls") to an Antora page title
-// e.g. "solr-zk-ls" -> "bin/solr zk ls"
-String cliFileNameToTitle(String baseName) {
-  def parts = baseName.split('-')
-  return "bin/solr " + parts.drop(1).join(' ')
-}
-
-// Return the short nav label for a command file (last segment of dash-separated name)
-// e.g. "solr-zk-ls" -> "ls"
-String cliFileNameToNavLabel(String baseName) {
-  return baseName.split('-').last()
+// A dash separated commands only when that prefix is itself a generated page,
+// so a hyphen inside the leaf name is kept
+// e.g. "solr-zk-ls" -> "bin/solr zk ls". "solr-package-add-key" -> "bin/solr package add-key"
+String cliFileNameToTitle(String baseName, Set stems) {
+  return "bin/solr " + cliCommandPath(baseName, stems).join(' ')
+}
+
+// Tokens of the title after "solr". That last token is the nav label.
+// e.g. "solr-zk-ls" -> [zk, ls], "solr-package-add-key" -> [package, add-key]
+List<String> cliCommandPath(String baseName, Set stems) {
+  def cuts = [baseName.indexOf('-')]
+  for (def i = baseName.indexOf('-', cuts[0] + 1); i >= 0; i = baseName.indexOf('-', i + 1)) {
+    if (stems.contains(baseName.substring(0, i))) {
+      cuts.add(i)
+    }
+  }
+  cuts.add(baseName.length())
+  def segments = []
+  for (def c = 0; c < cuts.size() - 1; c++) {
+    segments.add(baseName.substring(cuts[c] + 1, cuts[c + 1]))
+  }
+  return segments
 }
 
 // Strip "[...]" (optional groups) and "(...)" (required groups whose members
@@ -707,10 +719,10 @@ String markRequiredOptionsAndArgs(String content) {
 // Post-process a raw ManPageGenerator AsciiDoc file for Antora compatibility.
 // The title lives inside the man-section-header block, so we replace the whole
 // header block with an Antora-compatible page title + attributes.
-String postProcessCliManPage(File rawFile, String asfHeader, String doNotEditNotice, String experimentalNotice) {
+String postProcessCliManPage(File rawFile, Set stems, String asfHeader, String doNotEditNotice, String experimentalNotice) {
   def content = rawFile.text
   def baseName = rawFile.name.replace('.adoc', '')
-  def newTitle = cliFileNameToTitle(baseName)
+  def newTitle = cliFileNameToTitle(baseName, stems)
 
   // Replace the entire man-section-header block (which contains :doctype:manpage, the page title
   // "= solr-start(1)", etc.) with our Antora-compatible title and page attributes.
@@ -776,9 +788,10 @@ String buildCliCommandTable(File cliDir) {
   def files = ((cliDir.listFiles() ?: []) as List<File>)
     .findAll { it.name.startsWith('solr-') && it.name.endsWith('.adoc') }
     .sort { it.name }
+  def stems = files.collect { it.name.replace('.adoc', '') } as Set
   def rows = files.collect { file ->
     def baseName = file.name.replace('.adoc', '')
-    "|xref:cli/${file.name}[${cliFileNameToTitle(baseName)}]"
+    "|xref:cli/${file.name}[${cliFileNameToTitle(baseName, stems)}]"
   }
   return "[cols=\"1\",options=\"header\"]\n|===\n|Command\n\n" + rows.join('\n') + "\n|==="
 }
@@ -792,18 +805,9 @@ List<String> buildCliNavEntries(File cliDir) {
     .sort { (it.name.replace('.adoc', '') + '\u0000') }
   def stems = files.collect { it.name.replace('.adoc', '') } as Set
   return files.collect { file ->
-    def baseName = file.name.replace('.adoc', '')
-    def label = cliFileNameToNavLabel(baseName)
-    // Nest under the deepest ancestor that is itself a generated page. Counting dashes instead
-    // would mis-nest a command whose own name contains one, e.g. a future "snapshot-create"
-    // would be filed under a "solr-snapshot" page that does not exist.
-    def depth = 0
-    for (def stem = baseName; stem.contains('-'); ) {
-      stem = stem.substring(0, stem.lastIndexOf('-'))
-      if (stems.contains(stem)) {
-        depth++
-      }
-    }
+    def path = cliCommandPath(file.name.replace('.adoc', ''), stems)
+    def label = path.last()
+    def depth = path.size() - 1
     // Generated pages hang off cli/index.adoc, which itself sits under the control script page,
     // so the shallowest generated page is already three levels deep.
     def stars = '*' * (depth + 3)
@@ -855,9 +859,10 @@ task generateCliDocs {
     destDir.mkdirs()
 
     def rawFiles = runManPageGenerator(rawOutputDir)
+    def stems = rawFiles.collect { it.name.replace('.adoc', '') } as Set
 
     rawFiles.each { rawFile ->
-      def processed = postProcessCliManPage(rawFile, ASF_LICENSE_HEADER, DO_NOT_EDIT_NOTICE, EXPERIMENTAL_NOTICE)
+      def processed = postProcessCliManPage(rawFile, stems, ASF_LICENSE_HEADER, DO_NOT_EDIT_NOTICE, EXPERIMENTAL_NOTICE)
       new File(destDir, rawFile.name).text = processed
     }
 
@@ -910,10 +915,11 @@ task checkCliDocsUpToDate {
     rawOutputDir.mkdirs()
 
     def rawFiles = runManPageGenerator(rawOutputDir)
+    def stems = rawFiles.collect { it.name.replace('.adoc', '') } as Set
     def issues = []
 
     rawFiles.each { rawFile ->
-      def processed = postProcessCliManPage(rawFile, ASF_LICENSE_HEADER, DO_NOT_EDIT_NOTICE, EXPERIMENTAL_NOTICE)
+      def processed = postProcessCliManPage(rawFile, stems, ASF_LICENSE_HEADER, DO_NOT_EDIT_NOTICE, EXPERIMENTAL_NOTICE)
       def committed = new File(committedDir, rawFile.name)
       if (!committed.exists()) {
         issues << "MISSING committed file (new command): ${rawFile.name}"
diff --git a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
index b001d404ba3..e22a51c7be1 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
@@ -30,13 +30,13 @@
 *** xref:cli/solr-export.adoc[export]
 *** xref:cli/solr-healthcheck.adoc[healthcheck]
 *** xref:cli/solr-package.adoc[package]
-**** xref:cli/solr-package-add-key.adoc[key]
-**** xref:cli/solr-package-add-repo.adoc[repo]
+**** xref:cli/solr-package-add-key.adoc[add-key]
+**** xref:cli/solr-package-add-repo.adoc[add-repo]
 **** xref:cli/solr-package-deploy.adoc[deploy]
 **** xref:cli/solr-package-install.adoc[install]
-**** xref:cli/solr-package-list-available.adoc[available]
-**** xref:cli/solr-package-list-deployed.adoc[deployed]
-**** xref:cli/solr-package-list-installed.adoc[installed]
+**** xref:cli/solr-package-list-available.adoc[list-available]
+**** xref:cli/solr-package-list-deployed.adoc[list-deployed]
+**** xref:cli/solr-package-list-installed.adoc[list-installed]
 **** xref:cli/solr-package-undeploy.adoc[undeploy]
 **** xref:cli/solr-package-uninstall.adoc[uninstall]
 *** xref:cli/solr-post.adoc[post]
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
index 318010391d5..fb8ea48e30e 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
@@ -68,13 +68,13 @@ Any command not listed here is unavailable when the picocli implementation is en
 |xref:cli/solr-delete.adoc[bin/solr delete]
 |xref:cli/solr-export.adoc[bin/solr export]
 |xref:cli/solr-healthcheck.adoc[bin/solr healthcheck]
-|xref:cli/solr-package-add-key.adoc[bin/solr package add key]
-|xref:cli/solr-package-add-repo.adoc[bin/solr package add repo]
+|xref:cli/solr-package-add-key.adoc[bin/solr package add-key]
+|xref:cli/solr-package-add-repo.adoc[bin/solr package add-repo]
 |xref:cli/solr-package-deploy.adoc[bin/solr package deploy]
 |xref:cli/solr-package-install.adoc[bin/solr package install]
-|xref:cli/solr-package-list-available.adoc[bin/solr package list available]
-|xref:cli/solr-package-list-deployed.adoc[bin/solr package list deployed]
-|xref:cli/solr-package-list-installed.adoc[bin/solr package list installed]
+|xref:cli/solr-package-list-available.adoc[bin/solr package list-available]
+|xref:cli/solr-package-list-deployed.adoc[bin/solr package list-deployed]
+|xref:cli/solr-package-list-installed.adoc[bin/solr package list-installed]
 |xref:cli/solr-package-undeploy.adoc[bin/solr package undeploy]
 |xref:cli/solr-package-uninstall.adoc[bin/solr package uninstall]
 |xref:cli/solr-package.adoc[bin/solr package]
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-key.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-key.adoc
index 801afac3091..d3627e3b970 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-key.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-key.adoc
@@ -19,7 +19,7 @@
 // To update: modify the @Command/@Option annotations in the Java source, then run:
 //   ./gradlew :solr:solr-ref-guide:generateCliDocs
 
-= bin/solr package add key
+= bin/solr package add-key
 :page-toclevels: 2
 
 [WARNING]
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-repo.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-repo.adoc
index 04f1eccd2fc..23fdc5e602f 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-repo.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-repo.adoc
@@ -19,7 +19,7 @@
 // To update: modify the @Command/@Option annotations in the Java source, then run:
 //   ./gradlew :solr:solr-ref-guide:generateCliDocs
 
-= bin/solr package add repo
+= bin/solr package add-repo
 :page-toclevels: 2
 
 [WARNING]
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-available.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-available.adoc
index 83110eb433d..2f3aa96e0e5 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-available.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-available.adoc
@@ -19,7 +19,7 @@
 // To update: modify the @Command/@Option annotations in the Java source, then run:
 //   ./gradlew :solr:solr-ref-guide:generateCliDocs
 
-= bin/solr package list available
+= bin/solr package list-available
 :page-toclevels: 2
 
 [WARNING]
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-deployed.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-deployed.adoc
index ecd8cdfb609..988f0e0768b 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-deployed.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-deployed.adoc
@@ -19,7 +19,7 @@
 // To update: modify the @Command/@Option annotations in the Java source, then run:
 //   ./gradlew :solr:solr-ref-guide:generateCliDocs
 
-= bin/solr package list deployed
+= bin/solr package list-deployed
 :page-toclevels: 2
 
 [WARNING]
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-installed.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-installed.adoc
index 216b3e58bfb..f57b076cc22 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-installed.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-installed.adoc
@@ -19,7 +19,7 @@
 // To update: modify the @Command/@Option annotations in the Java source, then run:
 //   ./gradlew :solr:solr-ref-guide:generateCliDocs
 
-= bin/solr package list installed
+= bin/solr package list-installed
 :page-toclevels: 2
 
 [WARNING]


(solr) 02/03: SOLR-16396 | Migrate GET config sections updateHandler, directoryFactory, indexConfig, and codecFactory to JAX-RS. (#5023)

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

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

commit 53bf2c09997001b1a729b1526bab884e482287fc
Author: Jalaz Kumar <ja...@gmail.com>
AuthorDate: Tue Oct 13 14:48:39 2026 +0530

    SOLR-16396 | Migrate GET config sections updateHandler, directoryFactory, indexConfig, and codecFactory to JAX-RS. (#5023)
    
    (cherry picked from commit b5e8ae18c5269fe53a0815c0ff10a24887d3369c)
---
 .../SOLR-16396-config-get-components.yml           |  9 ++++++
 .../apache/solr/client/api/endpoint/ConfigApi.java | 12 +++++++
 .../org/apache/solr/handler/SolrConfigHandler.java |  3 ++
 .../apache/solr/handler/admin/api/GetConfig.java   | 37 ++++++++++++++++++----
 .../solr/handler/admin/api/GetConfigAPI.java       |  9 ++----
 .../handler/admin/api/GetConfigSolrCloudTest.java  | 15 +++++++++
 .../solr/handler/admin/api/GetConfigTest.java      | 14 ++++++++
 7 files changed, 87 insertions(+), 12 deletions(-)

diff --git a/changelog/unreleased/SOLR-16396-config-get-components.yml b/changelog/unreleased/SOLR-16396-config-get-components.yml
new file mode 100644
index 00000000000..99a86733e03
--- /dev/null
+++ b/changelog/unreleased/SOLR-16396-config-get-components.yml
@@ -0,0 +1,9 @@
+title:
+  Migrate GET config sections updateHandler, directoryFactory, indexConfig, and codecFactory to JAX-RS.
+type: changed
+authors:
+  - name: Jalaz Kumar
+    nick: jaykay12
+links:
+  - name: SOLR-16396
+    url: https://issues.apache.org/jira/browse/SOLR-16396
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java
index cc9cecc1bb5..baaf4a9f785 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java
@@ -23,6 +23,7 @@ import io.swagger.v3.oas.annotations.Parameter;
 import jakarta.ws.rs.DefaultValue;
 import jakarta.ws.rs.GET;
 import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
 import jakarta.ws.rs.QueryParam;
 import org.apache.solr.client.api.model.ConfigInfoResponse;
 import org.apache.solr.client.api.model.ConfigOverlayResponse;
@@ -68,5 +69,16 @@ public interface ConfigApi {
             @DefaultValue("-1")
             @Parameter(description = "Expected request-params znode version")
             int expectedParamsVersion);
+
+    // TODO: Keep this regex in sync with GetConfig.MIGRATED_CONFIG_COMPONENTS
+    @GET
+    @Path("/{component:updateHandler|directoryFactory|indexConfig|codecFactory}")
+    @StoreApiParameters
+    @Operation(
+        summary = "Fetch a config section",
+        tags = {"config"})
+    ConfigInfoResponse getConfigComponent(
+        @PathParam("component") @Parameter(description = "Name of the config component")
+            String component);
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
index 7f79e5a55e3..c31196831f9 100644
--- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
@@ -224,6 +224,9 @@ public class SolrConfigHandler extends RequestHandlerBase
                     .getZnodeVersion(
                         req.getParams().getInt(ConfigOverlay.NAME, -1),
                         req.getParams().getInt(RequestParams.NAME, -1)));
+          } else if (GetConfig.MIGRATED_CONFIG_COMPONENTS.contains(parts.get(1))) {
+            V2ApiUtils.squashIntoSolrResponseWithoutHeader(
+                resp, new GetConfig(req).getConfigComponent(parts.get(1)));
           } else {
             Map<String, Object> m = getConfigDetails(parts.get(1), req);
             Map<String, Object> val = new LinkedHashMap<>();
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
index 80d46e7bac6..806cd1b3971 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
@@ -27,6 +27,7 @@ import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import org.apache.solr.api.JerseyResource;
 import org.apache.solr.client.api.endpoint.ConfigApi;
 import org.apache.solr.client.api.model.ConfigInfoResponse;
@@ -48,6 +49,11 @@ import org.apache.solr.util.SolrPluginUtils;
 
 public class GetConfig extends JerseyResource implements ConfigApi.Get {
 
+  // TODO: Keep this in sync with @Path regex on ConfigApi.Get#getConfigComponent.
+  // TODO: Remove this set when remaining GET /config/{component} sections also migrate.
+  public static final Set<String> MIGRATED_CONFIG_COMPONENTS =
+      Set.of("updateHandler", "directoryFactory", "indexConfig", "codecFactory");
+
   private final SolrQueryRequest solrQueryRequest;
 
   @Inject
@@ -89,15 +95,36 @@ public class GetConfig extends JerseyResource implements ConfigApi.Get {
     return response;
   }
 
+  @Override
+  @PermissionName(CONFIG_READ_PERM)
+  public ConfigInfoResponse getConfigComponent(String componentName) {
+    final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
+    Map<String, Object> section = new LinkedHashMap<>();
+    section.put(componentName, solrConfigAsMap().get(componentName));
+    response.config = jsonSafeCopy(section);
+    return response;
+  }
+
+  private Map<String, Object> solrConfigAsMap() {
+    return Utils.convertToMap(solrQueryRequest.getCore().getSolrConfig(), new LinkedHashMap<>());
+  }
+
+  @SuppressWarnings("unchecked")
+  private NullKeyTolerantMap jsonSafeCopy(Map<String, Object> map) {
+    final var jsonSafeCopy = new NullKeyTolerantMap("children");
+    jsonSafeCopy.putAll((Map<String, Object>) Utils.getDeepCopy(map, 20, true));
+    return jsonSafeCopy;
+  }
+
   @SuppressWarnings({"unchecked"})
   private Map<String, Object> buildConfigMap(
       SolrQueryRequest solrQueryRequest, boolean expandParams) {
-    Map<String, Object> map =
-        Utils.convertToMap(solrQueryRequest.getCore().getSolrConfig(), new LinkedHashMap<>());
+
+    Map<String, Object> configMap = solrConfigAsMap();
 
     Map<String, Object> reqHandlers =
         (Map<String, Object>)
-            map.computeIfAbsent(SolrRequestHandler.TYPE, k -> new LinkedHashMap<>());
+            configMap.computeIfAbsent(SolrRequestHandler.TYPE, k -> new LinkedHashMap<>());
     List<PluginInfo> plugins = solrQueryRequest.getCore().getImplicitHandlers();
     for (PluginInfo plugin : plugins) {
       if (SolrRequestHandler.TYPE.equals(plugin.type) && !reqHandlers.containsKey(plugin.name)) {
@@ -111,9 +138,7 @@ public class GetConfig extends JerseyResource implements ConfigApi.Get {
       }
     }
 
-    final var jsonSafeCopy = new NullKeyTolerantMap("children");
-    jsonSafeCopy.putAll((Map<String, Object>) Utils.getDeepCopy(map, 20, true));
-    return jsonSafeCopy;
+    return jsonSafeCopy(configMap);
   }
 
   @SuppressWarnings({"unchecked", "rawtypes"})
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java
index 6a0567c987c..1e67cb09c50 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java
@@ -28,8 +28,9 @@ import org.apache.solr.response.SolrQueryResponse;
 /**
  * V2 APIs for retrieving some or all configuration relevant to a particular collection (or core).
  *
- * <p>This class covers legacy AnnotatedApi paths under GET /config: /params, /params{paramset},
- * /overlay, /znodeVersion, etc.
+ * <p>This class covers legacy AnnotatedApi paths under GET /config: /params, /params{paramset}, and
+ * remaining {@code /config/{component}} sections not yet migrated to {@link
+ * org.apache.solr.client.api.endpoint.ConfigApi.Get}
  */
 public class GetConfigAPI {
   private final SolrConfigHandler configHandler;
@@ -63,13 +64,9 @@ public class GetConfigAPI {
         "/config/query",
         "/config/requestHandler",
         "/config/searchComponent",
-        "/config/updateHandler",
         "/config/queryResponseWriter",
         "/config/initParams",
         "/config/listener",
-        "/config/directoryFactory",
-        "/config/indexConfig",
-        "/config/codecFactory",
         "/config/requestDispatcher",
         "/config/jmx",
         "/config/queryParser",
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java
index 7ce43a5ff3d..7b34a37bbb1 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java
@@ -76,4 +76,19 @@ public class GetConfigSolrCloudTest extends SolrCloudTestCase {
     assertTrue(response.znodeVersion.overlay >= -1);
     assertTrue(response.znodeVersion.params >= -1);
   }
+
+  @Test
+  public void testGetConfigComponentFromCollection() throws Exception {
+    for (String component : GetConfig.MIGRATED_CONFIG_COMPONENTS) {
+      var request =
+          new ConfigApi.GetConfigComponent(IndexType.COLLECTION, COLLECTION_NAME, component);
+      var response = request.process(cluster.getSolrClient());
+
+      assertNotNull(component, response);
+      assertNull(component, response.error);
+      assertNotNull(component, response.config);
+      assertTrue(component, response.config.containsKey(component));
+      assertEquals(component, 1, response.config.size());
+    }
+  }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java
index a17d9258a18..b0fbbfa8035 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java
@@ -80,4 +80,18 @@ public class GetConfigTest extends SolrTestCase {
     assertTrue(response.znodeVersion.overlay >= -1);
     assertTrue(response.znodeVersion.params >= -1);
   }
+
+  @Test
+  public void testGetConfigComponentFromCore() throws Exception {
+    for (String component : GetConfig.MIGRATED_CONFIG_COMPONENTS) {
+      var request = new ConfigApi.GetConfigComponent(IndexType.CORE, CORE_NAME, component);
+      var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+      assertNotNull(component, response);
+      assertNull(component, response.error);
+      assertNotNull(component, response.config);
+      assertTrue(component, response.config.containsKey(component));
+      assertEquals(component, 1, response.config.size());
+    }
+  }
 }


(solr) 03/03: SOLR-16396 | Migrate GET config sections query, searchComponent & queryResponseWriter to JAX-RS (#5125)

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

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

commit 3a6604400fb9d7b939644790f2677cec75845a78
Author: Jalaz Kumar <ja...@gmail.com>
AuthorDate: Thu Oct 15 23:03:39 2026 +0530

    SOLR-16396 | Migrate GET config sections query, searchComponent & queryResponseWriter to JAX-RS (#5125)
    
    Co-authored-by: Eric Pugh <ep...@opensourceconnections.com>
    (cherry picked from commit 714d6ceff852e6dc743ef51ae094e4eaf8f3b3f9)
---
 changelog/unreleased/SOLR-16396-config-query.yml   |  11 ++
 .../apache/solr/client/api/endpoint/ConfigApi.java |  27 ++++-
 .../org/apache/solr/handler/SolrConfigHandler.java |  60 ++---------
 .../apache/solr/handler/admin/api/GetConfig.java   | 119 ++++++++++++++++++++-
 .../solr/handler/admin/api/GetConfigAPI.java       |   3 -
 .../solr/handler/admin/V2ConfigAPIMappingTest.java |   2 +-
 .../handler/admin/api/GetConfigSolrCloudTest.java  |   5 +-
 .../solr/handler/admin/api/GetConfigTest.java      |  82 +++++++++++++-
 .../configuration-guide/pages/config-api.adoc      |  49 +++++++++
 9 files changed, 293 insertions(+), 65 deletions(-)

diff --git a/changelog/unreleased/SOLR-16396-config-query.yml b/changelog/unreleased/SOLR-16396-config-query.yml
new file mode 100644
index 00000000000..b51ffafa9a2
--- /dev/null
+++ b/changelog/unreleased/SOLR-16396-config-query.yml
@@ -0,0 +1,11 @@
+title:
+  Migrate GET config sections query, searchComponent & queryResponseWriter to JAX-RS
+type: changed
+authors:
+  - name: Jalaz Kumar
+    nick: jaykay12
+  - name: Eric Pugh
+    nick: epugh
+links:
+  - name: SOLR-16396
+    url: https://issues.apache.org/jira/browse/SOLR-16396
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java
index baaf4a9f785..49a4b018cc9 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java
@@ -70,9 +70,12 @@ public interface ConfigApi {
             @Parameter(description = "Expected request-params znode version")
             int expectedParamsVersion);
 
-    // TODO: Keep this regex in sync with GetConfig.MIGRATED_CONFIG_COMPONENTS
+    // TODO: Keep this regex in sync with GetConfig.MIGRATED_CONFIG_COMPONENTS ("searchComponents"
+    // and "queryResponseWriters" here are the external, pluralized names for the internal
+    // "searchComponent"/"queryResponseWriter" sections; see GetConfig#toExternalComponentName)
     @GET
-    @Path("/{component:updateHandler|directoryFactory|indexConfig|codecFactory}")
+    @Path(
+        "/{component:updateHandler|directoryFactory|indexConfig|codecFactory|query|searchComponents|queryResponseWriters}")
     @StoreApiParameters
     @Operation(
         summary = "Fetch a config section",
@@ -80,5 +83,25 @@ public interface ConfigApi {
     ConfigInfoResponse getConfigComponent(
         @PathParam("component") @Parameter(description = "Name of the config component")
             String component);
+
+    // TODO: Keep this regex in sync with GetConfig.MIGRATED_CONFIG_COMPONENTS ("searchComponents"
+    // and "queryResponseWriters" here are the external, pluralized names for the internal
+    // "searchComponent"/"queryResponseWriter" sections; see GetConfig#toExternalComponentName)
+    @GET
+    @Path(
+        "/{component:updateHandler|directoryFactory|indexConfig|codecFactory|query|searchComponents|queryResponseWriters}/{componentName}")
+    @StoreApiParameters
+    @Operation(
+        summary = "Fetch a single named entry within a config section",
+        tags = {"config"})
+    ConfigInfoResponse getConfigComponentByName(
+        @PathParam("component") @Parameter(description = "Name of the config component")
+            String component,
+        @PathParam("componentName") @Parameter(description = "Name of the entry within the section")
+            String componentName,
+        @QueryParam("meta")
+            @DefaultValue("false")
+            @Parameter(description = "Include package metadata (_packageinfo_)")
+            boolean meta);
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
index c31196831f9..b4d8a5eee78 100644
--- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
@@ -61,7 +61,6 @@ import org.apache.solr.client.solrj.request.CollectionRequiringSolrRequest;
 import org.apache.solr.client.solrj.response.SimpleSolrResponse;
 import org.apache.solr.cloud.ZkController;
 import org.apache.solr.cloud.ZkSolrResourceLoader;
-import org.apache.solr.common.MapWriter;
 import org.apache.solr.common.SolrErrorWrappingException;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ClusterState;
@@ -76,7 +75,6 @@ import org.apache.solr.common.util.CommandOperation;
 import org.apache.solr.common.util.EnvUtils;
 import org.apache.solr.common.util.ExecutorUtil;
 import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.common.util.SolrNamedThreadFactory;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.common.util.Utils;
@@ -91,8 +89,6 @@ import org.apache.solr.handler.admin.api.GetConfigAPI;
 import org.apache.solr.handler.admin.api.ModifyConfigComponentAPI;
 import org.apache.solr.handler.admin.api.ModifyParamSetAPI;
 import org.apache.solr.handler.api.V2ApiUtils;
-import org.apache.solr.pkg.PackageAPI;
-import org.apache.solr.pkg.PackageListeners;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrRequestHandler;
 import org.apache.solr.response.SolrQueryResponse;
@@ -225,50 +221,22 @@ public class SolrConfigHandler extends RequestHandlerBase
                         req.getParams().getInt(ConfigOverlay.NAME, -1),
                         req.getParams().getInt(RequestParams.NAME, -1)));
           } else if (GetConfig.MIGRATED_CONFIG_COMPONENTS.contains(parts.get(1))) {
+            String componentName = req.getParams().get("componentName");
+            GetConfig getConfig = new GetConfig(req);
             V2ApiUtils.squashIntoSolrResponseWithoutHeader(
-                resp, new GetConfig(req).getConfigComponent(parts.get(1)));
+                resp,
+                componentName == null
+                    ? getConfig.getConfigComponent(parts.get(1))
+                    : getConfig.getConfigComponentByName(
+                        parts.get(1), componentName, req.getParams().getBool("meta", false)));
           } else {
             Map<String, Object> m = getConfigDetails(parts.get(1), req);
             Map<String, Object> val = new LinkedHashMap<>();
             val.put(parts.get(1), m.get(parts.get(1)));
             String componentName = req.getParams().get("componentName");
             if (componentName != null) {
-              @SuppressWarnings({"rawtypes"})
-              Map pluginNameVsPluginInfo = (Map) val.get(parts.get(1));
-              if (pluginNameVsPluginInfo != null) {
-                Object o =
-                    pluginNameVsPluginInfo instanceof MapWriter
-                        ? pluginNameVsPluginInfo
-                        : pluginNameVsPluginInfo.get(componentName);
-                Map<String, Object> pluginInfo =
-                    o instanceof MapWriter
-                        ? new SimpleOrderedMap<>((MapWriter) o)
-                        : (Map<String, Object>) o;
-                val.put(
-                    parts.get(1),
-                    pluginNameVsPluginInfo instanceof MapWriter
-                        ? pluginInfo
-                        : Map.of(componentName, pluginInfo));
-                if (req.getParams().getBool("meta", false)) {
-                  // meta=true is asking for the package info of the plugin
-                  // We go through all the listeners and see if there is one registered for this
-                  // plugin
-                  List<PackageListeners.Listener> listeners =
-                      req.getCore().getPackageListeners().getListeners();
-                  for (PackageListeners.Listener listener : listeners) {
-                    Map<String, PackageAPI.PkgVersion> infos = listener.packageDetails();
-                    if (infos == null || infos.isEmpty()) continue;
-                    infos.forEach(
-                        (s, mapWriter) -> {
-                          Map<String, Object> componentInfo =
-                              getComponentInfo(pluginInfo, componentName);
-                          if (s.equals(componentInfo.get("class"))) {
-                            (componentInfo).put("_packageinfo_", mapWriter);
-                          }
-                        });
-                  }
-                }
-              }
+              GetConfig.filterByComponentName(
+                  val, parts.get(1), componentName, req.getParams().getBool("meta", false), req);
             }
             resp.add("config", val);
           }
@@ -276,16 +244,6 @@ public class SolrConfigHandler extends RequestHandlerBase
       }
     }
 
-    @SuppressWarnings({"unchecked"})
-    private Map<String, Object> getComponentInfo(
-        Map<String, Object> pluginInfo, String componentName) {
-      if (pluginInfo.containsKey("class")) {
-        return pluginInfo;
-      }
-      return (Map<String, Object>)
-          pluginInfo.computeIfAbsent(componentName, k -> new LinkedHashMap<>());
-    }
-
     private Map<String, Object> getConfigDetails(String componentType, SolrQueryRequest req) {
       String componentName = componentType == null ? null : req.getParams().get("componentName");
       boolean showParams = req.getParams().getBool("expandParams", false);
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
index 806cd1b3971..1bc7db362bd 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
@@ -33,9 +33,12 @@ import org.apache.solr.client.api.endpoint.ConfigApi;
 import org.apache.solr.client.api.model.ConfigInfoResponse;
 import org.apache.solr.client.api.model.ConfigOverlayResponse;
 import org.apache.solr.client.api.model.ConfigZnodeVersionResponse;
+import org.apache.solr.common.MapWriter;
+import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.MapSolrParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.PluginInfo;
@@ -43,16 +46,47 @@ import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.handler.SolrConfigHandler;
 import org.apache.solr.jersey.NullKeyTolerantMap;
 import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.pkg.PackageAPI;
+import org.apache.solr.pkg.PackageListeners;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrRequestHandler;
 import org.apache.solr.util.SolrPluginUtils;
 
 public class GetConfig extends JerseyResource implements ConfigApi.Get {
 
-  // TODO: Keep this in sync with @Path regex on ConfigApi.Get#getConfigComponent.
+  // TODO: Keep this in sync with @Path regex on ConfigApi.Get#getConfigComponent. These are the
+  // internal SolrConfig section names (also used by the legacy v1 GET /config/{component} path);
+  // see EXTERNAL_TO_INTERNAL_COMPONENT_NAME for the v2-only pluralized renames.
   // TODO: Remove this set when remaining GET /config/{component} sections also migrate.
   public static final Set<String> MIGRATED_CONFIG_COMPONENTS =
-      Set.of("updateHandler", "directoryFactory", "indexConfig", "codecFactory");
+      Set.of(
+          "updateHandler",
+          "directoryFactory",
+          "indexConfig",
+          "codecFactory",
+          "query",
+          "searchComponent",
+          "queryResponseWriter");
+
+  // v2-only renames: external (URL/response-key) component name -> internal SolrConfig section
+  // name. v1's GET /config/{component} keeps using the internal names unchanged.
+  private static final Map<String, String> EXTERNAL_TO_INTERNAL_COMPONENT_NAME =
+      Map.of(
+          "searchComponents", "searchComponent",
+          "queryResponseWriters", "queryResponseWriter");
+
+  private static String toInternalComponentName(String externalName) {
+    return EXTERNAL_TO_INTERNAL_COMPONENT_NAME.getOrDefault(externalName, externalName);
+  }
+
+  public static String toExternalComponentName(String internalName) {
+    for (Map.Entry<String, String> e : EXTERNAL_TO_INTERNAL_COMPONENT_NAME.entrySet()) {
+      if (e.getValue().equals(internalName)) {
+        return e.getKey();
+      }
+    }
+    return internalName;
+  }
 
   private final SolrQueryRequest solrQueryRequest;
 
@@ -97,14 +131,91 @@ public class GetConfig extends JerseyResource implements ConfigApi.Get {
 
   @Override
   @PermissionName(CONFIG_READ_PERM)
-  public ConfigInfoResponse getConfigComponent(String componentName) {
+  public ConfigInfoResponse getConfigComponent(String component) {
+    final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
+    Map<String, Object> section = new LinkedHashMap<>();
+    section.put(component, solrConfigAsMap().get(toInternalComponentName(component)));
+    response.config = jsonSafeCopy(section);
+    return response;
+  }
+
+  @Override
+  @PermissionName(CONFIG_READ_PERM)
+  public ConfigInfoResponse getConfigComponentByName(
+      String component, String componentName, boolean meta) {
     final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
     Map<String, Object> section = new LinkedHashMap<>();
-    section.put(componentName, solrConfigAsMap().get(componentName));
+    section.put(component, solrConfigAsMap().get(toInternalComponentName(component)));
+    filterByComponentName(section, component, componentName, meta, solrQueryRequest);
     response.config = jsonSafeCopy(section);
     return response;
   }
 
+  /**
+   * Shared by the v2 {@code getConfigComponentByName} API and {@link
+   * org.apache.solr.handler.SolrConfigHandler}'s legacy v1 handling for components not yet migrated
+   * to v2, so the two don't carry separate copies of this logic.
+   *
+   * @throws SolrException NOT_FOUND if {@code componentName} does not exist within {@code
+   *     component}
+   */
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  public static void filterByComponentName(
+      Map<String, Object> section,
+      String component,
+      String componentName,
+      boolean meta,
+      SolrQueryRequest req) {
+    Map pluginNameVsPluginInfo = (Map) section.get(component);
+    if (pluginNameVsPluginInfo == null) {
+      return;
+    }
+
+    Object o =
+        pluginNameVsPluginInfo instanceof MapWriter
+            ? pluginNameVsPluginInfo
+            : pluginNameVsPluginInfo.get(componentName);
+    if (o == null) {
+      throw new SolrException(
+          SolrException.ErrorCode.NOT_FOUND,
+          componentName + " not found in config section " + component);
+    }
+
+    Map<String, Object> pluginInfo =
+        o instanceof MapWriter ? new SimpleOrderedMap<>((MapWriter) o) : (Map<String, Object>) o;
+    section.put(
+        component,
+        pluginNameVsPluginInfo instanceof MapWriter
+            ? pluginInfo
+            : Map.of(componentName, pluginInfo));
+
+    if (meta) {
+      List<PackageListeners.Listener> listeners =
+          req.getCore().getPackageListeners().getListeners();
+      for (PackageListeners.Listener listener : listeners) {
+        Map<String, PackageAPI.PkgVersion> infos = listener.packageDetails();
+        if (infos == null || infos.isEmpty()) continue;
+        infos.forEach(
+            (s, mapWriter) -> {
+              Map<String, Object> componentInfo = getComponentInfo(pluginInfo, componentName);
+              if (s.equals(componentInfo.get("class"))) {
+                (componentInfo).put("_packageinfo_", mapWriter);
+              }
+            });
+      }
+    }
+  }
+
+  @SuppressWarnings({"unchecked"})
+  private static Map<String, Object> getComponentInfo(
+      Map<String, Object> pluginInfo, String componentName) {
+    if (pluginInfo.containsKey("class")) {
+      return pluginInfo;
+    }
+    return (Map<String, Object>)
+        pluginInfo.computeIfAbsent(componentName, k -> new LinkedHashMap<>());
+  }
+
   private Map<String, Object> solrConfigAsMap() {
     return Utils.convertToMap(solrQueryRequest.getCore().getSolrConfig(), new LinkedHashMap<>());
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java
index 1e67cb09c50..808bdf545f4 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java
@@ -61,10 +61,7 @@ public class GetConfigAPI {
   @EndPoint(
       path = {
         "/config/luceneMatchVersion",
-        "/config/query",
         "/config/requestHandler",
-        "/config/searchComponent",
-        "/config/queryResponseWriter",
         "/config/initParams",
         "/config/listener",
         "/config/requestDispatcher",
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/V2ConfigAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/V2ConfigAPIMappingTest.java
index 252946a5971..6e0a2aa6e27 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/V2ConfigAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/V2ConfigAPIMappingTest.java
@@ -49,7 +49,7 @@ public class V2ConfigAPIMappingTest extends V2ApiMappingTest<SolrConfigHandler>
   // the API lookup works for a handful of the valid config "components".
   @Test
   public void testGetSingleComponentConfig() {
-    assertAnnotatedApiExistsFor("GET", "/config/query");
+    assertAnnotatedApiExistsFor("GET", "/config/requestHandler");
     assertAnnotatedApiExistsFor("GET", "/config/jmx");
     assertAnnotatedApiExistsFor("GET", "/config/requestDispatcher");
     assertAnnotatedApiExistsFor("GET", "/config/luceneMatchVersion");
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java
index 7b34a37bbb1..19b29380afd 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java
@@ -80,14 +80,15 @@ public class GetConfigSolrCloudTest extends SolrCloudTestCase {
   @Test
   public void testGetConfigComponentFromCollection() throws Exception {
     for (String component : GetConfig.MIGRATED_CONFIG_COMPONENTS) {
+      String pathComponent = GetConfig.toExternalComponentName(component);
       var request =
-          new ConfigApi.GetConfigComponent(IndexType.COLLECTION, COLLECTION_NAME, component);
+          new ConfigApi.GetConfigComponent(IndexType.COLLECTION, COLLECTION_NAME, pathComponent);
       var response = request.process(cluster.getSolrClient());
 
       assertNotNull(component, response);
       assertNull(component, response.error);
       assertNotNull(component, response.config);
-      assertTrue(component, response.config.containsKey(component));
+      assertTrue(component, response.config.containsKey(pathComponent));
       assertEquals(component, 1, response.config.size());
     }
   }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java
index b0fbbfa8035..504fe7ecb5e 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java
@@ -18,8 +18,10 @@ package org.apache.solr.handler.admin.api;
 
 import static org.apache.solr.core.CoreContainer.ALLOW_PATHS_SYSPROP;
 
+import java.util.Map;
 import org.apache.solr.SolrTestCase;
 import org.apache.solr.client.api.model.IndexType;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.request.ConfigApi;
 import org.apache.solr.util.ExternalPaths;
 import org.apache.solr.util.SolrJettyTestRule;
@@ -84,14 +86,90 @@ public class GetConfigTest extends SolrTestCase {
   @Test
   public void testGetConfigComponentFromCore() throws Exception {
     for (String component : GetConfig.MIGRATED_CONFIG_COMPONENTS) {
-      var request = new ConfigApi.GetConfigComponent(IndexType.CORE, CORE_NAME, component);
+      String pathComponent = GetConfig.toExternalComponentName(component);
+      var request = new ConfigApi.GetConfigComponent(IndexType.CORE, CORE_NAME, pathComponent);
       var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
 
       assertNotNull(component, response);
       assertNull(component, response.error);
       assertNotNull(component, response.config);
-      assertTrue(component, response.config.containsKey(component));
+      assertTrue(component, response.config.containsKey(pathComponent));
       assertEquals(component, 1, response.config.size());
     }
   }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testGetConfigComponentFilteredByNameFromCore() throws Exception {
+    var request =
+        new ConfigApi.GetConfigComponentByName(
+            IndexType.CORE, CORE_NAME, "searchComponents", "spellcheck");
+    request.setMeta(true);
+    var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+    assertNotNull(response);
+    assertNull(response.error);
+    assertNotNull(response.config);
+    assertEquals(1, response.config.size());
+
+    Map<String, Object> section = (Map<String, Object>) response.config.get("searchComponents");
+    assertNotNull(section);
+    assertTrue(section.containsKey("spellcheck"));
+
+    Map<String, Object> spellcheck = (Map<String, Object>) section.get("spellcheck");
+    assertNotNull(spellcheck);
+    assertEquals("solr.SpellCheckComponent", spellcheck.get("class"));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testGetConfigComponentFilteredByNameOmitsPackageInfoByDefault() throws Exception {
+    var request =
+        new ConfigApi.GetConfigComponentByName(
+            IndexType.CORE, CORE_NAME, "searchComponents", "spellcheck");
+    // meta is left unset, so it defaults to false and no package lookup should happen.
+    var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+    Map<String, Object> section = (Map<String, Object>) response.config.get("searchComponents");
+    Map<String, Object> spellcheck = (Map<String, Object>) section.get("spellcheck");
+    assertFalse(spellcheck.containsKey("_packageinfo_"));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testGetConfigComponentFilteredByNameForOtherMigratedComponents() throws Exception {
+    // filterCache is one of the few <query> entries the default configset doesn't comment out.
+    var queryRequest =
+        new ConfigApi.GetConfigComponentByName(IndexType.CORE, CORE_NAME, "query", "filterCache");
+    var queryResponse = queryRequest.process(solrTestRule.getSolrClient(CORE_NAME));
+    assertNotNull(queryResponse);
+    assertNull(queryResponse.error);
+    Map<String, Object> querySection = (Map<String, Object>) queryResponse.config.get("query");
+    assertNotNull(querySection);
+    assertTrue(querySection.containsKey("filterCache"));
+  }
+
+  @Test
+  public void testGetConfigComponentByUnknownNameReturnsNotFound() {
+    var request =
+        new ConfigApi.GetConfigComponentByName(
+            IndexType.CORE, CORE_NAME, "searchComponents", "doesNotExist");
+    final RemoteSolrException ex =
+        expectThrows(
+            RemoteSolrException.class,
+            () -> request.process(solrTestRule.getSolrClient(CORE_NAME)));
+    assertEquals(404, ex.code());
+  }
+
+  @Test
+  public void testGetConfigComponentRejectsOldSingularSearchComponentPath() {
+    // v2 intentionally renamed this one section to the plural "searchComponents" in the URL;
+    // the old singular form (still used by v1) shouldn't match the v2 route at all.
+    var request = new ConfigApi.GetConfigComponent(IndexType.CORE, CORE_NAME, "searchComponent");
+    final RemoteSolrException ex =
+        expectThrows(
+            RemoteSolrException.class,
+            () -> request.process(solrTestRule.getSolrClient(CORE_NAME)));
+    assertEquals(404, ex.code());
+  }
 }
diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/config-api.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/config-api.adoc
index f01f882a0d0..38981e7caf6 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/config-api.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/config-api.adoc
@@ -119,6 +119,7 @@ To see the configuration for implicit request handlers, add `expandParams=true`
 See the documentation for implicit request handlers linked above for examples using this command.
 
 The available top-level sections that can be added as path parameters are: `query`, `requestHandler`, `searchComponent`, `updateHandler`, `queryResponseWriter`, `initParams`, `znodeVersion`, `listener`, `directoryFactory`, `indexConfig`, and `codecFactory`.
+In the V2 API, `searchComponent` and `queryResponseWriter` are exposed under the pluralized names `searchComponents` and `queryResponseWriters` instead.
 
 To further restrict the request to a single component within a top-level section, use the `componentName` request parameter.
 
@@ -161,6 +162,54 @@ The output of this command will look similar to:
 
 The ability to restrict to objects within a top-level section is limited to request handlers (`requestHandler`), search components (`searchComponent`), and response writers (`queryResponseWriter`).
 
+For search components and response writers, the V2 API uses a path segment instead of the `componentName` request parameter, under the pluralized section names.
+For example, to return configuration for the `elevator` search component:
+
+[tabs#getnamedcomponent]
+======
+V1 API::
++
+====
+[source,bash]
+----
+http://localhost:8983/solr/techproducts/config/searchComponent?componentName=elevator
+----
+====
+V2 API::
++
+====
+[source,bash]
+----
+http://localhost:8983/api/collections/techproducts/config/searchComponents/elevator
+----
+====
+======
+
+The same applies to response writers.
+For example, to return configuration for the `xslt` response writer:
+
+[tabs#getnamedwriter]
+======
+V1 API::
++
+====
+[source,bash]
+----
+http://localhost:8983/solr/techproducts/config/queryResponseWriter?componentName=xslt
+----
+====
+V2 API::
++
+====
+[source,bash]
+----
+http://localhost:8983/api/collections/techproducts/config/queryResponseWriters/xslt
+----
+====
+======
+
+Request handlers are unaffected and continue to use `componentName` in both APIs, as shown above.
+
 == Commands to Modify the Config
 
 This API uses specific commands with POST requests to tell Solr what property or type of property to add to or modify in `configoverlay.json`.