diff --git a/changelog/unreleased/SOLR-16396-config-requestHandler.yml b/changelog/unreleased/SOLR-16396-config-requestHandler.yml new file mode 100644 index 000000000000..44164b792c51 --- /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 49a4b018cc99..f10026be9249 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 @@ ConfigZnodeVersionResponse getZnodeVersion( @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 35fdc24d97de..72d76ddf8443 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.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 static String getJsonStringFromNonCollectionApi( 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 08614a0a4bc6..2b2080a06737 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.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.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.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 @@ private void handleGET() { .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 @@ private void handleGET() { : getConfig.getConfigComponentByName( parts.get(1), componentName, req.getParams().getBool("meta", false))); } else { - Map m = getConfigDetails(parts.get(1), req); + Map m = + Utils.convertToMap(req.getCore().getSolrConfig(), new LinkedHashMap<>()); Map val = new LinkedHashMap<>(); val.put(parts.get(1), m.get(parts.get(1))); String componentName = req.getParams().get("componentName"); @@ -245,79 +250,6 @@ private void handleGET() { } } - private Map getConfigDetails(String componentType, SolrQueryRequest req) { - String componentName = componentType == null ? null : req.getParams().get("componentName"); - boolean showParams = req.getParams().getBool("expandParams", false); - Map map = - Utils.convertToMap(this.req.getCore().getSolrConfig(), new LinkedHashMap<>()); - if (componentType != null && !SolrRequestHandler.TYPE.equals(componentType)) return map; - - @SuppressWarnings({"unchecked"}) - Map reqHandlers = - (Map) - map.computeIfAbsent(SolrRequestHandler.TYPE, k -> new LinkedHashMap<>()); - List 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 e : reqHandlers.entrySet()) { - if (componentName == null || e.getKey().equals(componentName)) { - Map m = expandUseParams(req, e.getValue()); - e.setValue(m); - } - } - - return map; - } - - @SuppressWarnings({"unchecked"}) - private Map expandUseParams(SolrQueryRequest req, Object plugin) { - - Map 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 m = new LinkedHashMap<>(); - pluginInfo.put("_useParamsExpanded_", m); - List 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 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 1bc7db362bd9..b8efea0a11c9 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 EXTERNAL_TO_INTERNAL_COMPONENT_NAME = @@ -99,7 +101,7 @@ public GetConfig(SolrQueryRequest solrQueryRequest) { @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 ConfigZnodeVersionResponse getZnodeVersion( 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 reqHandlersMap = + (Map) + solrConfigAsMap().computeIfAbsent(SolrRequestHandler.TYPE, k -> new LinkedHashMap<>()); + Map 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 section = new LinkedHashMap<>(); - section.put(component, solrConfigAsMap().get(toInternalComponentName(component))); - response.config = jsonSafeCopy(section); + + Map configMap = new LinkedHashMap<>(); + configMap.put(component, solrConfigAsMap().get(toInternalComponentName(component))); + + response.config = jsonSafeCopy(configMap); return response; } @@ -144,10 +174,11 @@ public ConfigInfoResponse getConfigComponent(String component) { public ConfigInfoResponse getConfigComponentByName( String component, String componentName, boolean meta) { final var response = instantiateJerseyResponse(ConfigInfoResponse.class); - Map section = new LinkedHashMap<>(); - section.put(component, solrConfigAsMap().get(toInternalComponentName(component))); - filterByComponentName(section, component, componentName, meta, solrQueryRequest); - response.config = jsonSafeCopy(section); + + Map 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 ConfigInfoResponse getConfigComponentByName( */ @SuppressWarnings({"unchecked", "rawtypes"}) public static void filterByComponentName( - Map section, + Map 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 static void filterByComponentName( Map pluginInfo = o instanceof MapWriter ? new SimpleOrderedMap<>((MapWriter) o) : (Map) o; - section.put( + configMap.put( component, pluginNameVsPluginInfo instanceof MapWriter ? pluginInfo @@ -227,29 +258,39 @@ private NullKeyTolerantMap jsonSafeCopy(Map map) { return jsonSafeCopy; } - @SuppressWarnings({"unchecked"}) - private Map buildConfigMap( - SolrQueryRequest solrQueryRequest, boolean expandParams) { - + @SuppressWarnings("unchecked") + private Map buildConfigMap(boolean expandParams) { Map configMap = solrConfigAsMap(); - - Map reqHandlers = + Map reqHandlersMap = (Map) 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 requestHandlersWithImplicits( + Map reqHandlersMap, boolean expandParams, String expandOnlyComponentName) { List 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 entry : reqHandlers.entrySet()) { - entry.setValue(expandUseParams(solrQueryRequest, entry.getValue())); + for (Map.Entry 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 808bdf545f42..6cd437911006 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 void getSingleParamSet(SolrQueryRequest req, SolrQueryResponse rsp) { @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 05fad9059784..626361d25d5b 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 2793af1641d7..825bd3b5a3c5 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 void testWrongVerificationJPathIsThrown() throws IOException { 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 6e0a2aa6e27a..e7912afa8faa 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 void populateApiBag() { // 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 dd51d21ddeaf..20dbd09d1a6b 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 void testGetZnodeVersionFromCollection() throws Exception { 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 void testGetOverlayFromCollectionViaNonHostingNode() throws Exception { } } + @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 configSection = (Map) 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 configSection = (Map) response.config.get(REQUEST_HANDLERS); + assertNotNull(configSection); + assertEquals(1, configSection.size()); + assertTrue(configSection.containsKey("/select")); + + Map selectRHSection = (Map) 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 configSection = (Map) response.config.get(REQUEST_HANDLERS); + + Map pingRHSection = (Map) configSection.get("/admin/ping"); + assertTrue(pingRHSection.containsKey("_useParamsExpanded_")); + assertNotNull(pingRHSection.get("_useParamsExpanded_")); + Map expandedPingRHSection = + (Map) 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 configSection = (Map) response.config.get(REQUEST_HANDLERS); + + Map pingRHSection = (Map) 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 504fe7ecb5e7..4407845d1554 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 void testGetZnodeVersionFromCore() throws Exception { 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 configSection = (Map) 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 configSection = (Map) response.config.get(REQUEST_HANDLERS); + assertNotNull(configSection); + assertEquals(1, configSection.size()); + assertTrue(configSection.containsKey("/select")); + + Map selectRHSection = (Map) 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 configSection = (Map) response.config.get(REQUEST_HANDLERS); + + Map pingRHSection = (Map) configSection.get("/admin/ping"); + assertTrue(pingRHSection.containsKey("_useParamsExpanded_")); + assertNotNull(pingRHSection.get("_useParamsExpanded_")); + Map expandedPingRHSection = + (Map) 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 configSection = (Map) response.config.get(REQUEST_HANDLERS); + + Map pingRHSection = (Map) 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 38981e7caf69..01c043ae6a63 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 0aad010ddc92..67948d8f1446 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] ----