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 7e419906977 SOLR-16396: Migrate GET /config/requestHandler to JAX-RS
(#5168)
7e419906977 is described below
commit 7e419906977def36933caf0666b6e472cf921927
Author: Jalaz Kumar <[email protected]>
AuthorDate: Tue Oct 20 18:08:06 2026 +0530
SOLR-16396: Migrate GET /config/requestHandler to JAX-RS (#5168)
Co-authored-by: Eric Pugh <[email protected]>
Co-authored-by: Claude Sonnet 5 <[email protected]>
---
.../SOLR-16396-config-requestHandler.yml | 12 +++
.../apache/solr/client/api/endpoint/ConfigApi.java | 21 +++++
.../solr/cli/packagemanager/PackageUtils.java | 16 +++-
.../org/apache/solr/handler/SolrConfigHandler.java | 90 +++-----------------
.../apache/solr/handler/admin/api/GetConfig.java | 85 ++++++++++++++-----
.../solr/handler/admin/api/GetConfigAPI.java | 1 -
.../question-answer-repository/repository.json | 33 +++++++-
.../cli/packagemanager/TestPackageManager.java | 2 +-
.../solr/handler/admin/V2ConfigAPIMappingTest.java | 1 -
.../handler/admin/api/GetConfigSolrCloudTest.java | 95 ++++++++++++++++++++++
.../solr/handler/admin/api/GetConfigTest.java | 89 ++++++++++++++++++++
.../configuration-guide/pages/config-api.adoc | 8 +-
.../pages/implicit-requesthandlers.adoc | 4 +-
13 files changed, 343 insertions(+), 114 deletions(-)
diff --git a/changelog/unreleased/SOLR-16396-config-requestHandler.yml
b/changelog/unreleased/SOLR-16396-config-requestHandler.yml
new file mode 100644
index 00000000000..44164b792c5
--- /dev/null
+++ b/changelog/unreleased/SOLR-16396-config-requestHandler.yml
@@ -0,0 +1,12 @@
+title: >
+ Migrate GET /config/requestHandler to JAX-RS; V2 path and response key
+ are now plural requestHandlers (V1 unchanged)
+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 49a4b018cc9..f10026be924 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,6 +70,27 @@ public interface ConfigApi {
@Parameter(description = "Expected request-params znode version")
int expectedParamsVersion);
+ // TODO: RH names contain '/' (e.g. /select, /update/json), so they cannot
be a @PathParam in
+ // the current design; Keeping filtering via @QueryParam for now.
+ @GET
+ @Path("/requestHandlers")
+ @StoreApiParameters
+ @Operation(
+ summary = "Fetch request handlers (including implicit handler)",
+ tags = {"config"})
+ ConfigInfoResponse getRequestHandler(
+ @QueryParam("componentName")
+ @Parameter(description = "Name of the specific request handler")
+ String component,
+ @QueryParam("expandParams")
+ @DefaultValue("false")
+ @Parameter(description = "Expand request handler useParams and
effective parameters")
+ boolean expandParams,
+ @QueryParam("meta")
+ @DefaultValue("false")
+ @Parameter(description = "Include package metadata
(_packageinfo_)")
+ boolean meta);
+
// 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)
diff --git
a/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageUtils.java
b/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageUtils.java
index 35fdc24d97d..72d76ddf844 100644
--- a/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageUtils.java
+++ b/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageUtils.java
@@ -51,6 +51,7 @@ import org.apache.solr.common.util.Utils;
import org.apache.solr.filestore.ClusterFileStore;
import org.apache.solr.filestore.DistribFileStore;
import org.apache.solr.filestore.FileStoreAPI;
+import org.apache.solr.servlet.SolrRequestParsers;
import org.apache.solr.util.SolrJacksonAnnotationInspector;
import org.apache.zookeeper.server.ByteBufferInputStream;
@@ -160,10 +161,23 @@ public class PackageUtils {
return getJsonStringFromUrl(client, path, params, false);
}
- /** Returns JSON string from a given Solr URL */
+ /**
+ * repository.json's verify/setup command templates embed their query string
directly in {@code
+ * path} (e.g. {@code .../requestHandlers?componentName=x&meta=true}). The
HTTP client always
+ * appends its own query string afterward with a leading {@code ?}, and a
URL with two {@code ?}
+ * characters is parsed as a single query string on the wire -- so the last
templated parameter
+ * silently absorbs the client's params as part of its own value instead of
being read correctly.
+ */
private static String getJsonStringFromUrl(
SolrClient client, String path, SolrParams params, boolean
isCollectionApi) {
try {
+ int queryIndex = path.indexOf('?');
+ if (queryIndex >= 0) {
+ ModifiableSolrParams merged = new ModifiableSolrParams(params);
+
merged.add(SolrRequestParsers.parseQueryString(path.substring(queryIndex + 1)));
+ params = merged;
+ path = path.substring(0, queryIndex);
+ }
GenericSolrRequest request =
new GenericSolrRequest(SolrRequest.METHOD.GET, path, params)
.setRequiresCollection(isCollectionApi);
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 08614a0a4bc..2b2080a0673 100644
--- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
@@ -21,10 +21,7 @@ import static
org.apache.solr.common.util.StrUtils.formatString;
import static org.apache.solr.core.ConfigOverlay.NOT_EDITABLE;
import static org.apache.solr.core.ConfigOverlay.ZNODEVER;
import static org.apache.solr.core.ConfigSetProperties.IMMUTABLE_CONFIGSET_ARG;
-import static org.apache.solr.core.PluginInfo.APPENDS;
import static org.apache.solr.core.PluginInfo.DEFAULTS;
-import static org.apache.solr.core.PluginInfo.INVARIANTS;
-import static org.apache.solr.core.RequestParams.USEPARAM;
import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_CLASS;
import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME;
import static
org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME_IN_OVERLAY;
@@ -69,7 +66,6 @@ import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.params.CommonParams;
-import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.CommandOperation;
@@ -97,7 +93,6 @@ import org.apache.solr.schema.SchemaManager;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.PermissionNameProvider;
import org.apache.solr.util.RTimer;
-import org.apache.solr.util.SolrPluginUtils;
import org.apache.solr.util.plugin.SolrCoreAware;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -221,6 +216,15 @@ public class SolrConfigHandler extends RequestHandlerBase
.getZnodeVersion(
req.getParams().getInt(ConfigOverlay.NAME, -1),
req.getParams().getInt(RequestParams.NAME, -1)));
+ } else if (SolrRequestHandler.TYPE.equals(parts.get(1))) {
+ V2ApiUtils.squashIntoSolrResponseWithoutHeader(
+ resp,
+ new GetConfig(req)
+ .getRequestHandler(
+ parts.get(1),
+ req.getParams().get("componentName"),
+ req.getParams().getBool("expandParams", false),
+ req.getParams().getBool("meta", false)));
} else if
(GetConfig.MIGRATED_CONFIG_COMPONENTS.contains(parts.get(1))) {
String componentName = req.getParams().get("componentName");
GetConfig getConfig = new GetConfig(req);
@@ -231,7 +235,8 @@ public class SolrConfigHandler extends RequestHandlerBase
: getConfig.getConfigComponentByName(
parts.get(1), componentName,
req.getParams().getBool("meta", false)));
} else {
- Map<String, Object> m = getConfigDetails(parts.get(1), req);
+ Map<String, Object> m =
+ Utils.convertToMap(req.getCore().getSolrConfig(), new
LinkedHashMap<>());
Map<String, Object> val = new LinkedHashMap<>();
val.put(parts.get(1), m.get(parts.get(1)));
String componentName = req.getParams().get("componentName");
@@ -245,79 +250,6 @@ public class SolrConfigHandler extends RequestHandlerBase
}
}
- 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);
- Map<String, Object> map =
- Utils.convertToMap(this.req.getCore().getSolrConfig(), new
LinkedHashMap<>());
- if (componentType != null &&
!SolrRequestHandler.TYPE.equals(componentType)) return map;
-
- @SuppressWarnings({"unchecked"})
- Map<String, Object> reqHandlers =
- (Map<String, Object>)
- map.computeIfAbsent(SolrRequestHandler.TYPE, k -> new
LinkedHashMap<>());
- List<PluginInfo> plugins = this.req.getCore().getImplicitHandlers();
- for (PluginInfo plugin : plugins) {
- if (SolrRequestHandler.TYPE.equals(plugin.type)) {
- if (!reqHandlers.containsKey(plugin.name)) {
- reqHandlers.put(plugin.name, plugin);
- }
- }
- }
- if (!showParams) return map;
- for (Map.Entry<String, Object> e : reqHandlers.entrySet()) {
- if (componentName == null || e.getKey().equals(componentName)) {
- Map<String, Object> m = expandUseParams(req, e.getValue());
- e.setValue(m);
- }
- }
-
- return map;
- }
-
- @SuppressWarnings({"unchecked"})
- private Map<String, Object> expandUseParams(SolrQueryRequest req, Object
plugin) {
-
- Map<String, Object> pluginInfo = null;
- if (plugin instanceof Map) {
- pluginInfo = (Map) plugin;
- } else if (plugin instanceof PluginInfo) {
- pluginInfo = Utils.convertToMap((PluginInfo) plugin, new
LinkedHashMap<>());
- }
- String useParams = (String) pluginInfo.get(USEPARAM);
- String useParamsInReq = req.getOriginalParams().get(USEPARAM);
- if (useParams != null || useParamsInReq != null) {
- Map<String, Object> m = new LinkedHashMap<>();
- pluginInfo.put("_useParamsExpanded_", m);
- List<String> params = new ArrayList<>();
- if (useParams != null) params.addAll(StrUtils.splitSmart(useParams,
','));
- if (useParamsInReq != null)
params.addAll(StrUtils.splitSmart(useParamsInReq, ','));
- for (String param : params) {
- RequestParams.ParamSet p =
-
this.req.getCore().getSolrConfig().getRequestParams().getParams(param);
- if (p != null) {
- m.put(param, p);
- } else {
- m.put(param, "[NOT AVAILABLE]");
- }
- }
-
- SolrQueryRequest r = req.subRequest(req.getOriginalParams());
- r.getContext().put(USEPARAM, useParams);
- NamedList<?> nl = new PluginInfo(SolrRequestHandler.TYPE,
pluginInfo).initArgs;
- SolrPluginUtils.setDefaults(
- r,
- getSolrParamsFromNamedList(nl, DEFAULTS),
- getSolrParamsFromNamedList(nl, APPENDS),
- getSolrParamsFromNamedList(nl, INVARIANTS));
- // SolrParams.wrapDefaults(maskUseParams, req.getParams())
-
- MapSolrParams mask = new MapSolrParams(Map.of("componentName", "",
"expandParams", ""));
- pluginInfo.put("_effectiveParams_", SolrParams.wrapDefaults(mask,
r.getParams()));
- }
- return pluginInfo;
- }
-
private void handlePOST() throws IOException {
List<CommandOperation> ops =
ApiBag.readCommands(req.getContentStreams(), resp.getValues());
if (ops == null) return;
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 1bc7db362bd..b8efea0a11c 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
@@ -68,6 +68,8 @@ public class GetConfig extends JerseyResource implements
ConfigApi.Get {
"searchComponent",
"queryResponseWriter");
+ public static final String REQUEST_HANDLERS = "requestHandlers";
+
// 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
=
@@ -99,7 +101,7 @@ public class GetConfig extends JerseyResource implements
ConfigApi.Get {
@PermissionName(CONFIG_READ_PERM)
public ConfigInfoResponse getConfig(boolean expandParams) {
final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
- response.config = buildConfigMap(solrQueryRequest, expandParams);
+ response.config = buildConfigMap(expandParams);
return response;
}
@@ -129,13 +131,41 @@ public class GetConfig extends JerseyResource implements
ConfigApi.Get {
return response;
}
+ @Override
+ @PermissionName(CONFIG_READ_PERM)
+ public ConfigInfoResponse getRequestHandler(
+ String componentName, boolean expandParams, boolean meta) {
+ return getRequestHandler(REQUEST_HANDLERS, componentName, expandParams,
meta);
+ }
+
+ @SuppressWarnings("unchecked")
+ public ConfigInfoResponse getRequestHandler(
+ String sectionName, String componentName, boolean expandParams, boolean
meta) {
+ final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
+
+ Map<String, Object> reqHandlersMap =
+ (Map<String, Object>)
+ solrConfigAsMap().computeIfAbsent(SolrRequestHandler.TYPE, k ->
new LinkedHashMap<>());
+ Map<String, Object> configMap = new LinkedHashMap<>();
+ configMap.put(
+ sectionName, requestHandlersWithImplicits(reqHandlersMap,
expandParams, componentName));
+ if (componentName != null) {
+ filterByComponentName(configMap, sectionName, componentName, meta,
solrQueryRequest);
+ }
+
+ response.config = jsonSafeCopy(configMap);
+ return response;
+ }
+
@Override
@PermissionName(CONFIG_READ_PERM)
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);
+
+ Map<String, Object> configMap = new LinkedHashMap<>();
+ configMap.put(component,
solrConfigAsMap().get(toInternalComponentName(component)));
+
+ response.config = jsonSafeCopy(configMap);
return response;
}
@@ -144,10 +174,11 @@ public class GetConfig extends JerseyResource implements
ConfigApi.Get {
public ConfigInfoResponse getConfigComponentByName(
String component, String componentName, boolean meta) {
final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
- Map<String, Object> section = new LinkedHashMap<>();
- section.put(component,
solrConfigAsMap().get(toInternalComponentName(component)));
- filterByComponentName(section, component, componentName, meta,
solrQueryRequest);
- response.config = jsonSafeCopy(section);
+
+ Map<String, Object> configMap = new LinkedHashMap<>();
+ configMap.put(component,
solrConfigAsMap().get(toInternalComponentName(component)));
+ filterByComponentName(configMap, component, componentName, meta,
solrQueryRequest);
+ response.config = jsonSafeCopy(configMap);
return response;
}
@@ -161,12 +192,12 @@ public class GetConfig extends JerseyResource implements
ConfigApi.Get {
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static void filterByComponentName(
- Map<String, Object> section,
+ Map<String, Object> configMap,
String component,
String componentName,
boolean meta,
SolrQueryRequest req) {
- Map pluginNameVsPluginInfo = (Map) section.get(component);
+ Map pluginNameVsPluginInfo = (Map) configMap.get(component);
if (pluginNameVsPluginInfo == null) {
return;
}
@@ -183,7 +214,7 @@ public class GetConfig extends JerseyResource implements
ConfigApi.Get {
Map<String, Object> pluginInfo =
o instanceof MapWriter ? new SimpleOrderedMap<>((MapWriter) o) :
(Map<String, Object>) o;
- section.put(
+ configMap.put(
component,
pluginNameVsPluginInfo instanceof MapWriter
? pluginInfo
@@ -227,29 +258,39 @@ public class GetConfig extends JerseyResource implements
ConfigApi.Get {
return jsonSafeCopy;
}
- @SuppressWarnings({"unchecked"})
- private Map<String, Object> buildConfigMap(
- SolrQueryRequest solrQueryRequest, boolean expandParams) {
-
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> buildConfigMap(boolean expandParams) {
Map<String, Object> configMap = solrConfigAsMap();
-
- Map<String, Object> reqHandlers =
+ Map<String, Object> reqHandlersMap =
(Map<String, Object>)
configMap.computeIfAbsent(SolrRequestHandler.TYPE, k -> new
LinkedHashMap<>());
+ requestHandlersWithImplicits(reqHandlersMap, expandParams, null);
+ return jsonSafeCopy(configMap);
+ }
+
+ /**
+ * Merges implicit handlers into {@code reqHandlersMap} (mutated in place
and returned), expanding
+ * useParams for {@code expandOnlyComponentName} (or all entries if null)
when {@code
+ * expandParams}.
+ */
+ private Map<String, Object> requestHandlersWithImplicits(
+ Map<String, Object> reqHandlersMap, boolean expandParams, String
expandOnlyComponentName) {
List<PluginInfo> plugins =
solrQueryRequest.getCore().getImplicitHandlers();
for (PluginInfo plugin : plugins) {
- if (SolrRequestHandler.TYPE.equals(plugin.type) &&
!reqHandlers.containsKey(plugin.name)) {
- reqHandlers.put(plugin.name, plugin);
+ if (SolrRequestHandler.TYPE.equals(plugin.type) &&
!reqHandlersMap.containsKey(plugin.name)) {
+ reqHandlersMap.put(plugin.name, plugin);
}
}
if (expandParams) {
- for (Map.Entry<String, Object> entry : reqHandlers.entrySet()) {
- entry.setValue(expandUseParams(solrQueryRequest, entry.getValue()));
+ for (Map.Entry<String, Object> entry : reqHandlersMap.entrySet()) {
+ if (expandOnlyComponentName == null ||
expandOnlyComponentName.equals(entry.getKey())) {
+ entry.setValue(expandUseParams(solrQueryRequest, entry.getValue()));
+ }
}
}
- return jsonSafeCopy(configMap);
+ return reqHandlersMap;
}
@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 808bdf545f4..6cd43791100 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,7 +61,6 @@ public class GetConfigAPI {
@EndPoint(
path = {
"/config/luceneMatchVersion",
- "/config/requestHandler",
"/config/initParams",
"/config/listener",
"/config/requestDispatcher",
diff --git
a/solr/core/src/test-files/solr/question-answer-repository/repository.json
b/solr/core/src/test-files/solr/question-answer-repository/repository.json
index 05fad905978..626361d25d5 100644
--- a/solr/core/src/test-files/solr/question-answer-repository/repository.json
+++ b/solr/core/src/test-files/solr/question-answer-repository/repository.json
@@ -28,9 +28,9 @@
"method": "POST"
},
"verify-command": {
- "path":
"/api/collections/${collection}/config/requestHandler?componentName=${RH-HANDLER-PATH}&meta=true",
+ "path":
"/api/collections/${collection}/config/requestHandlers?componentName=${RH-HANDLER-PATH}&meta=true",
"method": "GET",
- "condition":
"$['config'].['requestHandler'].['${RH-HANDLER-PATH}'].['_packageinfo_'].['version']",
+ "condition":
"$['config'].['requestHandlers'].['${RH-HANDLER-PATH}'].['_packageinfo_'].['version']",
"expected": "${package-version}"
}
}
@@ -48,7 +48,34 @@
"url": "question-answer-request-handler-1.1.jar.tmp",
"sig":
"2PBHV2xBqd+teFpSY322lviCrr6EXB7g1Q7UAZcNsr2qzR+w+sPSyfV4YYeHKHYbMkyw1YBz11Gw/Fdgd1VIOQ=="
}
- ]
+ ],
+ "manifest": {
+ "version-constraint": ">8",
+ "plugins": [
+ {
+ "name": "request-handler",
+ "setup-command": {
+ "path": "/api/collections/${collection}/config",
+ "payload": {"add-requesthandler": {"name":
"${RH-HANDLER-PATH}", "class": "question-answer:fullstory.QARequestHandler"}},
+ "method": "POST"
+ },
+ "uninstall-command": {
+ "path": "/api/collections/${collection}/config",
+ "payload": {"delete-requesthandler": "${RH-HANDLER-PATH}"},
+ "method": "POST"
+ },
+ "verify-command": {
+ "path":
"/api/collections/${collection}/config/requestHandlers?componentName=${RH-HANDLER-PATH}&meta=true",
+ "method": "GET",
+ "condition":
"$['config'].['requestHandlers'].['${RH-HANDLER-PATH}'].['_packageinfo_'].['version']",
+ "expected": "${package-version}"
+ }
+ }
+ ],
+ "parameter-defaults": {
+ "RH-HANDLER-PATH": "/mypath"
+ }
+ }
}
]
}
diff --git
a/solr/core/src/test/org/apache/solr/cli/packagemanager/TestPackageManager.java
b/solr/core/src/test/org/apache/solr/cli/packagemanager/TestPackageManager.java
index 2793af1641d..825bd3b5a3c 100644
---
a/solr/core/src/test/org/apache/solr/cli/packagemanager/TestPackageManager.java
+++
b/solr/core/src/test/org/apache/solr/cli/packagemanager/TestPackageManager.java
@@ -60,7 +60,7 @@ public class TestPackageManager extends SolrCloudTestCase {
plugin.verifyCommand = new SolrPackage.Command();
plugin.verifyCommand.method = "GET";
plugin.verifyCommand.path = /*"/api*/
- "/collections/" + COLLECTION_NAME + "/config/requestHandler";
+ "/collections/" + COLLECTION_NAME + "/config/requestHandlers";
plugin.verifyCommand.condition = "&[no_quotes_error_jpath]";
manager.verify(
new SolrPackageInstance(
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 6e0a2aa6e27..e7912afa8fa 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,6 @@ 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/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 dd51d21ddea..20dbd09d1a6 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
@@ -17,8 +17,11 @@
package org.apache.solr.handler.admin.api;
import static org.apache.solr.core.CoreContainer.ALLOW_PATHS_SYSPROP;
+import static org.apache.solr.handler.admin.api.GetConfig.REQUEST_HANDLERS;
+import java.util.Map;
import org.apache.solr.client.api.model.IndexType;
+import org.apache.solr.client.solrj.RemoteSolrException;
import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.ConfigApi;
@@ -82,6 +85,12 @@ public class GetConfigSolrCloudTest extends
SolrCloudTestCase {
assertTrue(response.znodeVersion.params >= -1);
}
+ /**
+ * Reproduces a bug where a v2 "/config" request proxied internally to the
node actually hosting
+ * the collection fails with "Cannot find API for the path", because Solr
forwards such requests
+ * using the short "/c/{collection}/..." alias, but {@code
Constants.INDEX_PATH_PREFIX}'s JAX-RS
+ * {@code @Path} regex only recognizes "cores" or "collections", not "c".
+ */
@Test
public void testGetOverlayFromCollectionViaNonHostingNode() throws Exception
{
Replica replica =
getCollectionState(COLLECTION_NAME).replicaStream().findFirst().orElseThrow();
@@ -103,6 +112,92 @@ public class GetConfigSolrCloudTest extends
SolrCloudTestCase {
}
}
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGetRequestHandlersFromCollection() throws Exception {
+ var request = new ConfigApi.GetRequestHandler(IndexType.COLLECTION,
COLLECTION_NAME);
+ var response = request.process(cluster.getSolrClient());
+
+ assertNotNull(response);
+ assertNull(response.error);
+ assertNotNull(response.config);
+ assertEquals(1, response.config.size());
+ assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+ Map<String, Object> configSection = (Map<String, Object>)
response.config.get(REQUEST_HANDLERS);
+ assertNotNull(configSection);
+ assertTrue(configSection.containsKey("/select"));
+ assertTrue(configSection.containsKey("/admin/ping"));
+ assertTrue(configSection.containsKey("/graph"));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGetRequestHandlersFilteredByNameFromCollection() throws
Exception {
+ var request = new ConfigApi.GetRequestHandler(IndexType.COLLECTION,
COLLECTION_NAME);
+ request.setComponentName("/select");
+ var response = request.process(cluster.getSolrClient());
+
+ assertNotNull(response);
+ assertNull(response.error);
+ assertEquals(1, response.config.size());
+ assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+ Map<String, Object> configSection = (Map<String, Object>)
response.config.get(REQUEST_HANDLERS);
+ assertNotNull(configSection);
+ assertEquals(1, configSection.size());
+ assertTrue(configSection.containsKey("/select"));
+
+ Map<String, Object> selectRHSection = (Map<String, Object>)
configSection.get("/select");
+ assertEquals("/select", selectRHSection.get("name"));
+ assertEquals("solr.SearchHandler", selectRHSection.get("class"));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGetRequestHandlersExpandParamsTrue() throws Exception {
+ var request = new ConfigApi.GetRequestHandler(IndexType.COLLECTION,
COLLECTION_NAME);
+ request.setComponentName("/admin/ping");
+ request.setExpandParams(true);
+ var response = request.process(cluster.getSolrClient());
+
+ assertNotNull(response);
+ assertNull(response.error);
+ assertEquals(1, response.config.size());
+ assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+ Map<String, Object> configSection = (Map<String, Object>)
response.config.get(REQUEST_HANDLERS);
+
+ Map<String, Object> pingRHSection = (Map<String, Object>)
configSection.get("/admin/ping");
+ assertTrue(pingRHSection.containsKey("_useParamsExpanded_"));
+ assertNotNull(pingRHSection.get("_useParamsExpanded_"));
+ Map<String, Object> expandedPingRHSection =
+ (Map<String, Object>) pingRHSection.get("_useParamsExpanded_");
+ assertEquals("[NOT AVAILABLE]", expandedPingRHSection.get("_ADMIN_PING"));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGetRequestHandlersOmitsExpandedParamsByDefault() throws
Exception {
+ var request = new ConfigApi.GetRequestHandler(IndexType.COLLECTION,
COLLECTION_NAME);
+ request.setComponentName("/admin/ping");
+ var response = request.process(cluster.getSolrClient());
+
+ Map<String, Object> configSection = (Map<String, Object>)
response.config.get(REQUEST_HANDLERS);
+
+ Map<String, Object> pingRHSection = (Map<String, Object>)
configSection.get("/admin/ping");
+ assertFalse(pingRHSection.containsKey("_useParamsExpanded_"));
+ }
+
+ @Test
+ public void testGetRequestHandlersByUnknownNameReturnsNotFound() throws
Exception {
+ var request = new ConfigApi.GetRequestHandler(IndexType.COLLECTION,
COLLECTION_NAME);
+ request.setComponentName("/randomHandler");
+ final RemoteSolrException exception =
+ assertThrows(RemoteSolrException.class, () ->
request.process(cluster.getSolrClient()));
+ assertEquals(404, exception.code());
+ }
+
@Test
public void testGetConfigComponentFromCollection() throws Exception {
for (String component : GetConfig.MIGRATED_CONFIG_COMPONENTS) {
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 504fe7ecb5e..4407845d155 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
@@ -17,6 +17,7 @@
package org.apache.solr.handler.admin.api;
import static org.apache.solr.core.CoreContainer.ALLOW_PATHS_SYSPROP;
+import static org.apache.solr.handler.admin.api.GetConfig.REQUEST_HANDLERS;
import java.util.Map;
import org.apache.solr.SolrTestCase;
@@ -83,6 +84,94 @@ public class GetConfigTest extends SolrTestCase {
assertTrue(response.znodeVersion.params >= -1);
}
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGetRequestHandlersFromCore() throws Exception {
+ var request = new ConfigApi.GetRequestHandler(IndexType.CORE, CORE_NAME);
+ var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+ assertNotNull(response);
+ assertNull(response.error);
+ assertNotNull(response.config);
+ assertEquals(1, response.config.size());
+ assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+ Map<String, Object> configSection = (Map<String, Object>)
response.config.get(REQUEST_HANDLERS);
+ assertNotNull(configSection);
+ assertTrue(configSection.containsKey("/select"));
+ assertTrue(configSection.containsKey("/admin/ping"));
+ assertTrue(configSection.containsKey("/spell"));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGetRequestHandlersFilteredByNameFromCore() throws Exception {
+ var request = new ConfigApi.GetRequestHandler(IndexType.CORE, CORE_NAME);
+ request.setComponentName("/select");
+ var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+ assertNotNull(response);
+ assertNull(response.error);
+ assertEquals(1, response.config.size());
+ assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+ Map<String, Object> configSection = (Map<String, Object>)
response.config.get(REQUEST_HANDLERS);
+ assertNotNull(configSection);
+ assertEquals(1, configSection.size());
+ assertTrue(configSection.containsKey("/select"));
+
+ Map<String, Object> selectRHSection = (Map<String, Object>)
configSection.get("/select");
+ assertEquals("/select", selectRHSection.get("name"));
+ assertEquals("solr.SearchHandler", selectRHSection.get("class"));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGetRequestHandlersExpandParamsTrue() throws Exception {
+ var request = new ConfigApi.GetRequestHandler(IndexType.CORE, CORE_NAME);
+ request.setComponentName("/admin/ping");
+ request.setExpandParams(true);
+ var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+ assertNotNull(response);
+ assertNull(response.error);
+ assertEquals(1, response.config.size());
+ assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+ Map<String, Object> configSection = (Map<String, Object>)
response.config.get(REQUEST_HANDLERS);
+
+ Map<String, Object> pingRHSection = (Map<String, Object>)
configSection.get("/admin/ping");
+ assertTrue(pingRHSection.containsKey("_useParamsExpanded_"));
+ assertNotNull(pingRHSection.get("_useParamsExpanded_"));
+ Map<String, Object> expandedPingRHSection =
+ (Map<String, Object>) pingRHSection.get("_useParamsExpanded_");
+ assertEquals("[NOT AVAILABLE]", expandedPingRHSection.get("_ADMIN_PING"));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGetRequestHandlersOmitsExpandedParamsByDefault() throws
Exception {
+ var request = new ConfigApi.GetRequestHandler(IndexType.CORE, CORE_NAME);
+ request.setComponentName("/admin/ping");
+ var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+ Map<String, Object> configSection = (Map<String, Object>)
response.config.get(REQUEST_HANDLERS);
+
+ Map<String, Object> pingRHSection = (Map<String, Object>)
configSection.get("/admin/ping");
+ assertFalse(pingRHSection.containsKey("_useParamsExpanded_"));
+ }
+
+ @Test
+ public void testGetRequestHandlersByUnknownNameReturnsNotFound() throws
Exception {
+ var request = new ConfigApi.GetRequestHandler(IndexType.CORE, CORE_NAME);
+ request.setComponentName("/randomHandler");
+ final RemoteSolrException exception =
+ assertThrows(
+ RemoteSolrException.class,
+ () -> request.process(solrTestRule.getSolrClient(CORE_NAME)));
+ assertEquals(404, exception.code());
+ }
+
@Test
public void testGetConfigComponentFromCore() throws Exception {
for (String component : GetConfig.MIGRATED_CONFIG_COMPONENTS) {
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 38981e7caf6..01c043ae6a6 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
@@ -109,7 +109,7 @@ V2 API::
====
[source,bash]
----
-http://localhost:8983/api/collections/techproducts/config/requestHandler
+http://localhost:8983/api/collections/techproducts/config/requestHandlers
----
====
======
@@ -119,7 +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.
+In the V2 API, `requestHandler`, `searchComponent` and `queryResponseWriter`
are exposed under the pluralized names `requestHandlers`, `searchComponents`
and `queryResponseWriters` instead (including the response JSON key).
To further restrict the request to a single component within a top-level
section, use the `componentName` request parameter.
@@ -140,12 +140,12 @@ V2 API::
====
[source,bash]
----
-http://localhost:8983/api/collections/techproducts/config/requestHandler?componentName=/select
+http://localhost:8983/api/collections/techproducts/config/requestHandlers?componentName=/select
----
====
======
-The output of this command will look similar to:
+The output of this command will look similar to(V1 nests under
`requestHandler`; V2 under `requestHandlers`):
[source,json]
----
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 0aad010ddc9..67948d8f144 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
@@ -693,12 +693,12 @@ V2 API::
====
[source,bash]
----
-http://localhost:8983/api/collections/gettingstarted/config/requestHandler?componentName=/export&expandParams=true
+http://localhost:8983/api/collections/gettingstarted/config/requestHandlers?componentName=/export&expandParams=true
----
====
======
-The response will look similar to:
+The response will look similar to (V1 nests under `requestHandler`; V2 nests
under `requestHandlers`):
[source,json]
----