Skip to content
11 changes: 11 additions & 0 deletions changelog/unreleased/SOLR-16396-config-query.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,38 @@ 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",
tags = {"config"})
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);
}
}
60 changes: 9 additions & 51 deletions solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -226,67 +222,29 @@ 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<String, Object> m = getConfigDetails(parts.get(1), req);
Map<String, Object> val = new LinkedHashMap<>();
val.put(parts.get(1), m.get(parts.get(1)));
String componentName = req.getParams().get("componentName");
if (componentName != null) {
@SuppressWarnings({"rawtypes"})
Map pluginNameVsPluginInfo = (Map) val.get(parts.get(1));
if (pluginNameVsPluginInfo != null) {
Object o =
pluginNameVsPluginInfo instanceof MapWriter
? pluginNameVsPluginInfo
: pluginNameVsPluginInfo.get(componentName);
Map<String, Object> pluginInfo =
o instanceof MapWriter
? new SimpleOrderedMap<>((MapWriter) o)
: (Map<String, Object>) o;
val.put(
parts.get(1),
pluginNameVsPluginInfo instanceof MapWriter
? pluginInfo
: Map.of(componentName, pluginInfo));
if (req.getParams().getBool("meta", false)) {
// meta=true is asking for the package info of the plugin
// We go through all the listeners and see if there is one registered for this
// plugin
List<PackageListeners.Listener> listeners =
req.getCore().getPackageListeners().getListeners();
for (PackageListeners.Listener listener : listeners) {
Map<String, PackageAPI.PkgVersion> infos = listener.packageDetails();
if (infos == null || infos.isEmpty()) continue;
infos.forEach(
(s, mapWriter) -> {
Map<String, Object> componentInfo =
getComponentInfo(pluginInfo, componentName);
if (s.equals(componentInfo.get("class"))) {
(componentInfo).put("_packageinfo_", mapWriter);
}
});
}
}
}
GetConfig.filterByComponentName(
val, parts.get(1), componentName, req.getParams().getBool("meta", false), req);
}
resp.add("config", val);
}
}
}
}

@SuppressWarnings({"unchecked"})
private Map<String, Object> getComponentInfo(
Map<String, Object> pluginInfo, String componentName) {
if (pluginInfo.containsKey("class")) {
return pluginInfo;
}
return (Map<String, Object>)
pluginInfo.computeIfAbsent(componentName, k -> new LinkedHashMap<>());
}

private Map<String, Object> getConfigDetails(String componentType, SolrQueryRequest req) {
String componentName = componentType == null ? null : req.getParams().get("componentName");
boolean showParams = req.getParams().getBool("expandParams", false);
Expand Down
119 changes: 115 additions & 4 deletions solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,60 @@
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;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.handler.SolrConfigHandler;
import org.apache.solr.jersey.NullKeyTolerantMap;
import org.apache.solr.jersey.PermissionName;
import org.apache.solr.pkg.PackageAPI;
import org.apache.solr.pkg.PackageListeners;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.util.SolrPluginUtils;

public class GetConfig extends JerseyResource implements ConfigApi.Get {

// TODO: Keep this in sync with @Path regex on ConfigApi.Get#getConfigComponent.
// TODO: Keep this in sync with @Path regex on ConfigApi.Get#getConfigComponent. These are the
// internal SolrConfig section names (also used by the legacy v1 GET /config/{component} path);
// see EXTERNAL_TO_INTERNAL_COMPONENT_NAME for the v2-only pluralized renames.
// TODO: Remove this set when remaining GET /config/{component} sections also migrate.
public static final Set<String> MIGRATED_CONFIG_COMPONENTS =
Set.of("updateHandler", "directoryFactory", "indexConfig", "codecFactory");
Set.of(
"updateHandler",
"directoryFactory",
"indexConfig",
"codecFactory",
"query",
"searchComponent",
"queryResponseWriter");

// v2-only renames: external (URL/response-key) component name -> internal SolrConfig section
// name. v1's GET /config/{component} keeps using the internal names unchanged.
private static final Map<String, String> EXTERNAL_TO_INTERNAL_COMPONENT_NAME =
Map.of(
"searchComponents", "searchComponent",
"queryResponseWriters", "queryResponseWriter");

private static String toInternalComponentName(String externalName) {
return EXTERNAL_TO_INTERNAL_COMPONENT_NAME.getOrDefault(externalName, externalName);
}

public static String toExternalComponentName(String internalName) {
for (Map.Entry<String, String> e : EXTERNAL_TO_INTERNAL_COMPONENT_NAME.entrySet()) {
if (e.getValue().equals(internalName)) {
return e.getKey();
}
}
return internalName;
}

private final SolrQueryRequest solrQueryRequest;

Expand Down Expand Up @@ -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<String, Object> section = new LinkedHashMap<>();
section.put(component, solrConfigAsMap().get(toInternalComponentName(component)));
response.config = jsonSafeCopy(section);
return response;
}

@Override
@PermissionName(CONFIG_READ_PERM)
public ConfigInfoResponse getConfigComponentByName(
String component, String componentName, boolean meta) {
final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
Map<String, Object> section = new LinkedHashMap<>();
section.put(componentName, solrConfigAsMap().get(componentName));
section.put(component, solrConfigAsMap().get(toInternalComponentName(component)));
filterByComponentName(section, component, componentName, meta, solrQueryRequest);
response.config = jsonSafeCopy(section);
return response;
}

/**
* Shared by the v2 {@code getConfigComponentByName} API and {@link
* org.apache.solr.handler.SolrConfigHandler}'s legacy v1 handling for components not yet migrated
* to v2, so the two don't carry separate copies of this logic.
*
* @throws SolrException NOT_FOUND if {@code componentName} does not exist within {@code
* component}
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static void filterByComponentName(
Map<String, Object> section,
String component,
String componentName,
boolean meta,
SolrQueryRequest req) {
Map pluginNameVsPluginInfo = (Map) section.get(component);
if (pluginNameVsPluginInfo == null) {
return;
}

Object o =
pluginNameVsPluginInfo instanceof MapWriter
? pluginNameVsPluginInfo
: pluginNameVsPluginInfo.get(componentName);
if (o == null) {
throw new SolrException(
SolrException.ErrorCode.NOT_FOUND,
componentName + " not found in config section " + component);
}

Map<String, Object> pluginInfo =
o instanceof MapWriter ? new SimpleOrderedMap<>((MapWriter) o) : (Map<String, Object>) o;
section.put(
component,
pluginNameVsPluginInfo instanceof MapWriter
? pluginInfo
: Map.of(componentName, pluginInfo));

if (meta) {
List<PackageListeners.Listener> listeners =
req.getCore().getPackageListeners().getListeners();
for (PackageListeners.Listener listener : listeners) {
Map<String, PackageAPI.PkgVersion> infos = listener.packageDetails();
if (infos == null || infos.isEmpty()) continue;
infos.forEach(
(s, mapWriter) -> {
Map<String, Object> componentInfo = getComponentInfo(pluginInfo, componentName);
if (s.equals(componentInfo.get("class"))) {
(componentInfo).put("_packageinfo_", mapWriter);
}
});
}
}
}

@SuppressWarnings({"unchecked"})
private static Map<String, Object> getComponentInfo(
Map<String, Object> pluginInfo, String componentName) {
if (pluginInfo.containsKey("class")) {
return pluginInfo;
}
return (Map<String, Object>)
pluginInfo.computeIfAbsent(componentName, k -> new LinkedHashMap<>());
}

private Map<String, Object> solrConfigAsMap() {
return Utils.convertToMap(solrQueryRequest.getCore().getSolrConfig(), new LinkedHashMap<>());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Expand Down
Loading
Loading