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