diff --git a/changelog/unreleased/SOLR-16396-config-requestHandler.yml b/changelog/unreleased/SOLR-16396-config-requestHandler.yml new file mode 100644 index 000000000000..a860843da618 --- /dev/null +++ b/changelog/unreleased/SOLR-16396-config-requestHandler.yml @@ -0,0 +1,10 @@ +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 +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/api/src/java/org/apache/solr/client/api/util/Constants.java b/solr/api/src/java/org/apache/solr/client/api/util/Constants.java index 3e3c7443c18f..13f8009fbb41 100644 --- a/solr/api/src/java/org/apache/solr/client/api/util/Constants.java +++ b/solr/api/src/java/org/apache/solr/client/api/util/Constants.java @@ -24,8 +24,18 @@ private Constants() { public static final String INDEX_TYPE_PATH_PARAMETER = "indexType"; public static final String INDEX_NAME_PATH_PARAMETER = "indexName"; + + // "c" is accepted alongside "collections" because Solr internally forwards a v2 request to the + // node actually hosting a collection using the short "/c/{collection}/..." alias (see + // HttpSolrCall#sendRemoteProxy and V2HttpCall's "c"/"collections" handling); without it, any + // JAX-RS resource built on this prefix 404s with "Cannot find API for the path" whenever the + // request has to be proxied to another node. public static final String INDEX_PATH_PREFIX = - "/{" + INDEX_TYPE_PATH_PARAMETER + ":cores|collections}/{" + INDEX_NAME_PATH_PARAMETER + "}"; + "/{" + + INDEX_TYPE_PATH_PARAMETER + + ":cores|collections|c}/{" + + INDEX_NAME_PATH_PARAMETER + + "}"; public static final String CORE_NAME_PATH_PARAMETER = "coreName"; 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..3f0466d86af6 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,37 @@ 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); + } + + public ConfigInfoResponse getRequestHandler( + String sectionName, String componentName, boolean expandParams, boolean meta) { + final var response = instantiateJerseyResponse(ConfigInfoResponse.class); + + Map configMap = new LinkedHashMap<>(); + configMap.put( + sectionName, requestHandlersWithImplicits(solrConfigAsMap(), 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 +170,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 +188,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 +210,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 +254,35 @@ private NullKeyTolerantMap jsonSafeCopy(Map map) { return jsonSafeCopy; } - @SuppressWarnings({"unchecked"}) - private Map buildConfigMap( - SolrQueryRequest solrQueryRequest, boolean expandParams) { - + private Map buildConfigMap(boolean expandParams) { Map configMap = solrConfigAsMap(); + requestHandlersWithImplicits(configMap, expandParams, null); + return jsonSafeCopy(configMap); + } - Map reqHandlers = + @SuppressWarnings({"unchecked"}) + private Map requestHandlersWithImplicits( + Map configMap, boolean expandParams, String expandOnlyComponentName) { + Map reqHandlersMap = (Map) configMap.computeIfAbsent(SolrRequestHandler.TYPE, k -> new LinkedHashMap<>()); + 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..bcfe7779ca3f 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}" } } diff --git a/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java b/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java index a919b30555b7..312509746916 100644 --- a/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java +++ b/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java @@ -320,9 +320,9 @@ void assertPackageVersion( testForResponseElement( cluster.getJettySolrRunner(0).getBaseUrl().toString() + "/" + collection, - "/config/requestHandler?componentName=" + component + "&meta=true", + "/config/requestHandlers?componentName=" + component + "&meta=true", credentials, - Arrays.asList("config", "requestHandler", component, "_packageinfo_", "version"), + Arrays.asList("config", "requestHandlers", component, "_packageinfo_", "version"), componentVersion); } 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 19b29380afd8..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,11 +17,17 @@ 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; import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.common.cloud.Replica; +import org.apache.solr.embedded.JettySolrRunner; import org.apache.solr.util.ExternalPaths; import org.junit.BeforeClass; import org.junit.Test; @@ -34,7 +40,9 @@ public class GetConfigSolrCloudTest extends SolrCloudTestCase { @BeforeClass public static void setupCluster() throws Exception { System.setProperty(ALLOW_PATHS_SYSPROP, ExternalPaths.SERVER_HOME.toAbsolutePath().toString()); - configureCluster(1).addConfig("conf", configset("cloud-minimal")).configure(); + // 2 nodes so that a request can land on a node that does *not* host the collection's only + // replica, forcing Solr to internally proxy the request to the node that does. + configureCluster(2).addConfig("conf", configset("cloud-minimal")).configure(); CollectionAdminRequest.createCollection(COLLECTION_NAME, "conf", 1, 1) .process(cluster.getSolrClient()); } @@ -77,6 +85,119 @@ 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(); + JettySolrRunner hostingJetty = cluster.getReplicaJetty(replica); + JettySolrRunner otherJetty = + cluster.getJettySolrRunners().stream() + .filter(jetty -> !jetty.equals(hostingJetty)) + .findFirst() + .orElseThrow(); + + try (var client = new HttpJettySolrClient.Builder(otherJetty.getBaseUrl().toString()).build()) { + var request = new ConfigApi.GetOverlay(IndexType.COLLECTION, COLLECTION_NAME); + var response = request.process(client); + + assertNotNull(response); + assertNull(response.error); + assertNotNull(response.overlay); + assertTrue(response.overlay.containsKey("znodeVersion")); + } + } + + @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] ----