This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_10x in repository https://gitbox.apache.org/repos/asf/solr.git
commit 31fc827b9b73d04b3d3ad57862548a63ea60c4a6 Author: Jalaz Kumar <[email protected]> AuthorDate: Fri Oct 30 04:07:18 2026 +0530 SOLR-16396: Migrate GET paramsets v2 API to JAX-RS. (#5218) Co-authored-by: Eric Pugh <[email protected]> Co-authored-by: Claude Sonnet 5 <[email protected]> (cherry picked from commit 69444fcbb9ba33d98c919b5f3c69316f9879ee17) --- .../unreleased/SOLR-16396-config-get-params.yml | 14 +++ .../solr/client/api/endpoint/ParamSetsApi.java | 50 ++++++++++ .../solr/client/api/model/ParamSetsResponse.java | 29 ++++++ .../solr/cli/packagemanager/PackageManager.java | 56 +++++++---- .../solr/cli/packagemanager/PackageUtils.java | 6 ++ .../org/apache/solr/handler/SolrConfigHandler.java | 32 ++++--- .../solr/handler/admin/api/GetConfigAPI.java | 56 ----------- .../solr/handler/admin/api/GetParamSets.java | 76 +++++++++++++++ .../apache/solr/core/TestSolrConfigHandler.java | 40 ++++++-- .../solr/handler/admin/V2ConfigAPIMappingTest.java | 21 ----- .../solr/handler/admin/api/GetParamSetsTest.java | 105 +++++++++++++++++++++ .../configuration-guide/pages/config-api.adoc | 3 +- .../pages/request-parameters-api.adoc | 27 +++++- .../solr/webapp/AdminUiParamsetsScreenTest.java | 2 + .../webapp/web/js/angular/controllers/paramsets.js | 77 ++++++--------- solr/webapp/web/js/angular/controllers/query.js | 30 ++---- solr/webapp/web/js/angular/services.js | 17 ++-- 17 files changed, 448 insertions(+), 193 deletions(-) diff --git a/changelog/unreleased/SOLR-16396-config-get-params.yml b/changelog/unreleased/SOLR-16396-config-get-params.yml new file mode 100644 index 00000000000..ec229e1b94b --- /dev/null +++ b/changelog/unreleased/SOLR-16396-config-get-params.yml @@ -0,0 +1,14 @@ +title: + Migrate GET paramsets v2 API to JAX-RS. v2 GET /config/params is now GET /config/paramsets, and + returns a 404 for an unknown paramset name; v1's GET /config/params keeps its existing 200 + response shape. The Admin UI's paramsets and query screens now read paramsets through the + generated v2 client too. +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/ParamSetsApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/ParamSetsApi.java new file mode 100644 index 00000000000..04efd8c17a0 --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ParamSetsApi.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.client.api.endpoint; + +import static org.apache.solr.client.api.util.Constants.INDEX_PATH_PREFIX; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import org.apache.solr.client.api.model.ParamSetsResponse; +import org.apache.solr.client.api.util.StoreApiParameters; + +public interface ParamSetsApi { + + @Path(INDEX_PATH_PREFIX + "/config/paramsets") + interface Get { + @GET + @StoreApiParameters + @Operation( + summary = "Fetch all request paramsets", + tags = {"param-sets"}) + ParamSetsResponse listParamSets(); + + @GET + @Path("/{paramSetName}") + @StoreApiParameters + @Operation( + summary = "Fetch a single request paramset", + tags = {"param-sets"}) + ParamSetsResponse getParamSet( + @PathParam("paramSetName") @Parameter(description = "Name of the paramset") + String paramSetName); + } +} diff --git a/solr/api/src/java/org/apache/solr/client/api/model/ParamSetsResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/ParamSetsResponse.java new file mode 100644 index 00000000000..a3aab5f004d --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/model/ParamSetsResponse.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.client.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; + +public class ParamSetsResponse extends SolrJerseyResponse { + + @JsonProperty("znodeVersion") + public int znodeVersion; + + @JsonProperty("params") + public Map<String, Object> params; +} diff --git a/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageManager.java b/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageManager.java index 7db659bbca1..ee1b5fc0ed7 100644 --- a/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageManager.java +++ b/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageManager.java @@ -244,15 +244,20 @@ public class PackageManager implements Closeable { solrClient.request( new GenericV2SolrRequest( SolrRequest.METHOD.GET, - PackageUtils.getCollectionParamsPath(collection) + "/PKG_VERSIONS") + PackageUtils.getCollectionParamSetsPath(collection) + "/PKG_VERSIONS") .setRequiresCollection( false) /* Making a collection request, but already baked into path */); packages = (Map<String, String>) - Objects.requireNonNullElse(result._get("/response/params/PKG_VERSIONS"), Map.of()); + Objects.requireNonNullElse(result._get("/params/PKG_VERSIONS"), Map.of()); } catch (PathNotFoundException ex) { // Don't worry if PKG_VERSION wasn't found. It just means this collection was never touched by // the package manager. + } catch (RemoteSolrException ex) { + if (ex.code() != 404) { + throw new SolrException(ErrorCode.SERVER_ERROR, ex); + } + // No PKG_VERSIONS paramset defined yet for this collection. } catch (SolrServerException | IOException ex) { throw new SolrException(ErrorCode.SERVER_ERROR, ex); } @@ -421,15 +426,26 @@ public class PackageManager implements Closeable { getCollectionParameterOverrides(packageInstance, isUpdate, overrides, collection); // Get package params + boolean packageParamsExist; try { NamedList<Object> collectionParams = solrClient.request( new GenericV2SolrRequest( SolrRequest.METHOD.GET, - PackageUtils.getCollectionParamsPath(collection) + "/packages") + PackageUtils.getCollectionParamSetsPath(collection) + "/packages") .setRequiresCollection( false) /* Making a collection-request, but already baked into path */); - boolean packageParamsExist = collectionParams.get("params") != null; + packageParamsExist = collectionParams.get("params") != null; + } catch (RemoteSolrException ex) { + if (ex.code() != 404) { + throw new SolrException(ErrorCode.SERVER_ERROR, ex); + } + // No "packages" paramset defined yet for this collection. + packageParamsExist = false; + } catch (SolrServerException | IOException ex) { + throw new SolrException(ErrorCode.SERVER_ERROR, ex); + } + try { SolrCLI.postJsonToSolr( solrClient, PackageUtils.getCollectionParamsPath(collection), @@ -728,12 +744,11 @@ public class PackageManager implements Closeable { solrClient.request( new GenericV2SolrRequest( SolrRequest.METHOD.GET, - PackageUtils.getCollectionParamsPath(collection) + "/packages") + PackageUtils.getCollectionParamSetsPath(collection) + "/packages") .setRequiresCollection( false) /* Making a collection-request, but already baked into path */); return (Map<String, String>) - Objects.requireNonNullElse( - response._get("/response/params/packages/" + packageName), Map.of()); + Objects.requireNonNullElse(response._get("/params/packages/" + packageName), Map.of()); } catch (Exception ex) { // This should be because there are no parameters. Be tolerant here. @@ -1103,19 +1118,24 @@ public class PackageManager implements Closeable { Map<String, String> deployed = new HashMap<>(); for (String collection : allCollections) { // Check package version installed - String paramsJson = - PackageUtils.getJsonStringFromCollectionApi( - solrClient, - PackageUtils.getCollectionParamsPath(collection) + "/PKG_VERSIONS", - new ModifiableSolrParams().add("omitHeader", "true")); String version = null; try { - version = - jsonPathRead( - paramsJson, "$['response'].['params'].['PKG_VERSIONS'].['" + packageName + "']"); - } catch (PathNotFoundException ex) { - // Don't worry if PKG_VERSION wasn't found. It just means this collection was never touched - // by the package manager. + NamedList<Object> response = + solrClient.request( + new GenericV2SolrRequest( + SolrRequest.METHOD.GET, + PackageUtils.getCollectionParamSetsPath(collection) + "/PKG_VERSIONS") + .setRequiresCollection( + false) /* Making a collection-request, but already baked into path */); + version = (String) response._get("/params/PKG_VERSIONS/" + packageName); + } catch (RemoteSolrException ex) { + if (ex.code() != 404) { + throw new SolrException(ErrorCode.SERVER_ERROR, ex); + } + // No PKG_VERSIONS paramset defined yet. It just means this collection was never touched by + // the package manager. + } catch (SolrServerException | IOException ex) { + throw new SolrException(ErrorCode.SERVER_ERROR, ex); } if (version != null) { deployed.put(collection, version); 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 72d76ddf844..2cab7b99a18 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 @@ -275,10 +275,16 @@ public class PackageUtils { return collections; } + /** Path for setting/unsetting request param sets (POST), per {@code ModifyParamSetAPI}. */ public static String getCollectionParamsPath(String collection) { return "/api/collections/" + collection + "/config/params"; } + /** Path for reading request param sets (GET), per {@code ParamSetsApi.Get}. */ + public static String getCollectionParamSetsPath(String collection) { + return "/api/collections/" + collection + "/config/paramsets"; + } + public static void uploadKey(byte[] bytes, String path, Path home) throws IOException { FileStoreAPI.MetaData meta = ClusterFileStore._createJsonMetaData(bytes, null); DistribFileStore._persistToFile( 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 d44e7fe9c53..72c1eefe665 100644 --- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java @@ -35,7 +35,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -72,6 +71,7 @@ import org.apache.solr.common.util.CommandOperation; 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; @@ -82,7 +82,7 @@ import org.apache.solr.core.SolrConfig; import org.apache.solr.core.SolrCore; import org.apache.solr.core.SolrResourceLoader; import org.apache.solr.handler.admin.api.GetConfig; -import org.apache.solr.handler.admin.api.GetConfigAPI; +import org.apache.solr.handler.admin.api.GetParamSets; import org.apache.solr.handler.admin.api.ModifyConfigComponentAPI; import org.apache.solr.handler.admin.api.ModifyParamSetAPI; import org.apache.solr.handler.api.V2ApiUtils; @@ -197,19 +197,26 @@ public class SolrConfigHandler extends RequestHandlerBase if (ConfigOverlay.NAME.equals(parts.get(1))) { V2ApiUtils.squashIntoSolrResponseWithoutHeader(resp, new GetConfig(req).getOverlay()); } else if (RequestParams.NAME.equals(parts.get(1))) { + NamedList<Object> paramSets = new SimpleOrderedMap<>(); if (parts.size() == 3) { - RequestParams params = req.getCore().getSolrConfig().getRequestParams(); - RequestParams.ParamSet p = params.getParams(parts.get(2)); - Map<String, Object> m = new LinkedHashMap<>(); - m.put(ZNODEVER, params.getZnodeVersion()); - if (p != null) { - m.put(RequestParams.NAME, Map.of(parts.get(2), p)); + try { + V2ApiUtils.squashIntoNamedListWithoutHeader( + paramSets, new GetParamSets(req).getParamSet(parts.get(2))); + } catch (SolrException e) { + if (e.code() != SolrException.ErrorCode.NOT_FOUND.code) { + throw e; + } + // v1 has always returned 200 here with no "params" entry for an unknown paramset + // name, rather than an error - preserve that for existing v1 callers. v2's + // GetParamSets keeps its own (more correct) 404 for its own callers. + paramSets.add( + ZNODEVER, req.getCore().getSolrConfig().getRequestParams().getZnodeVersion()); } - resp.add(SolrQueryResponse.NAME, m); } else { - resp.add(SolrQueryResponse.NAME, req.getCore().getSolrConfig().getRequestParams()); + V2ApiUtils.squashIntoNamedListWithoutHeader( + paramSets, new GetParamSets(req).listParamSets()); } - + resp.add(SolrQueryResponse.NAME, paramSets); } else { if (ZNODEVER.equals(parts.get(1))) { V2ApiUtils.squashIntoSolrResponseWithoutHeader( @@ -950,7 +957,6 @@ public class SolrConfigHandler extends RequestHandlerBase @Override public Collection<Api> getApis() { final List<Api> apis = new ArrayList<>(); - apis.addAll(AnnotatedApi.getApis(new GetConfigAPI(this))); apis.addAll(AnnotatedApi.getApis(new ModifyConfigComponentAPI(this))); apis.addAll(AnnotatedApi.getApis(new ModifyParamSetAPI(this))); return apis; @@ -958,7 +964,7 @@ public class SolrConfigHandler extends RequestHandlerBase @Override public Collection<Class<? extends JerseyResource>> getJerseyResources() { - return List.of(GetConfig.class); + return List.of(GetConfig.class, GetParamSets.class); } @Override 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 deleted file mode 100644 index 110974eec68..00000000000 --- a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.solr.handler.admin.api; - -import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET; -import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM; - -import org.apache.solr.api.EndPoint; -import org.apache.solr.handler.SolrConfigHandler; -import org.apache.solr.request.SolrQueryRequest; -import org.apache.solr.response.SolrQueryResponse; - -/** - * V2 APIs for retrieving some or all configuration relevant to a particular collection (or core). - * - * <p>This class only covers legacy AnnotatedApi paths under GET /config: /params and - * /params{paramset} - */ -public class GetConfigAPI { - private final SolrConfigHandler configHandler; - - public GetConfigAPI(SolrConfigHandler configHandler) { - this.configHandler = configHandler; - } - - @EndPoint( - path = {"/config/params"}, - method = GET, - permission = CONFIG_READ_PERM) - public void getAllParamSets(SolrQueryRequest req, SolrQueryResponse rsp) { - configHandler.handleRequest(req, rsp); - } - - @EndPoint( - path = {"/config/params/{paramset}"}, - method = GET, - permission = CONFIG_READ_PERM) - public void getSingleParamSet(SolrQueryRequest req, SolrQueryResponse rsp) { - configHandler.handleRequest(req, rsp); - } -} diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/GetParamSets.java b/solr/core/src/java/org/apache/solr/handler/admin/api/GetParamSets.java new file mode 100644 index 00000000000..a4ed0924ac0 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetParamSets.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.handler.admin.api; + +import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM; + +import jakarta.inject.Inject; +import java.util.LinkedHashMap; +import java.util.Map; +import org.apache.solr.api.JerseyResource; +import org.apache.solr.client.api.endpoint.ParamSetsApi; +import org.apache.solr.client.api.model.ParamSetsResponse; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.util.Utils; +import org.apache.solr.core.RequestParams; +import org.apache.solr.jersey.PermissionName; +import org.apache.solr.request.SolrQueryRequest; + +public class GetParamSets extends JerseyResource implements ParamSetsApi.Get { + + private final SolrQueryRequest solrQueryRequest; + + @Inject + public GetParamSets(SolrQueryRequest solrQueryRequest) { + this.solrQueryRequest = solrQueryRequest; + } + + @Override + @PermissionName(CONFIG_READ_PERM) + public ParamSetsResponse listParamSets() { + return buildResponse(null); + } + + @Override + @PermissionName(CONFIG_READ_PERM) + public ParamSetsResponse getParamSet(String paramSetName) { + return buildResponse(paramSetName); + } + + @SuppressWarnings("unchecked") + private ParamSetsResponse buildResponse(String paramSetName) { + final var response = instantiateJerseyResponse(ParamSetsResponse.class); + final RequestParams requestParams = + solrQueryRequest.getCore().getSolrConfig().getRequestParams(); + response.znodeVersion = requestParams.getZnodeVersion(); + + if (paramSetName == null) { + Map<String, Object> paramsMap = Utils.convertToMap(requestParams, new LinkedHashMap<>()); + response.params = (Map<String, Object>) paramsMap.get(RequestParams.NAME); + } else { + RequestParams.ParamSet paramSet = requestParams.getParams(paramSetName); + if (paramSet == null) { + throw new SolrException( + SolrException.ErrorCode.NOT_FOUND, "No paramset found with the name: " + paramSetName); + } + Map<String, Object> paramsMap = new LinkedHashMap<>(); + paramsMap.put(paramSetName, Utils.convertToMap(paramSet, new LinkedHashMap<>())); + response.params = paramsMap; + } + return response; + } +} diff --git a/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java b/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java index f00d7fd4cba..525eb36d673 100644 --- a/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java +++ b/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java @@ -26,6 +26,7 @@ import java.lang.invoke.MethodHandles; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -108,6 +109,15 @@ public class TestSolrConfigHandler extends RestTestBase { return harness.getBaseURL().contains("/____v2/") ? externalName : internalName; } + // The v2 paramset response is flat, while v1 nests it under "response" + private static List<String> paramSetsPath(RestTestHarness harness, String... keys) { + List<String> path = new ArrayList<>(); + if (!harness.getBaseURL().contains("/____v2/")) path.add("response"); + path.add("params"); + path.addAll(asList(keys)); + return path; + } + public void testProperty() throws Exception { RestTestHarness harness = restTestHarness; String requestHandlerKey = fullConfigKey(harness, "requestHandler", "requestHandlers"); @@ -646,6 +656,8 @@ public class TestSolrConfigHandler extends RestTestBase { public void testReqParams() throws Exception { RestTestHarness harness = restTestHarness; + String paramSetsKey = fullConfigKey(harness, "params", "paramsets"); + String payload = " {\n" + " 'set' : {'x': {" @@ -657,10 +669,20 @@ public class TestSolrConfigHandler extends RestTestBase { TestSolrConfigHandler.runConfigCommand(harness, "/config/params", payload); TestSolrConfigHandler.testForResponseElement( - harness, "/config/params", asList("response", "params", "x", "a"), "A val", TIMEOUT_S); + harness, "/config/" + paramSetsKey, paramSetsPath(harness, "x", "a"), "A val", TIMEOUT_S); TestSolrConfigHandler.testForResponseElement( - harness, "/config/params", asList("response", "params", "x", "b"), "B val", TIMEOUT_S); + harness, "/config/" + paramSetsKey, paramSetsPath(harness, "x", "b"), "B val", TIMEOUT_S); + + // v2 404s for an unknown paramset name; v1's long-standing behavior (a 200 with no matching + // entry) is preserved for existing v1 callers. + String missing = harness.query("/config/" + paramSetsKey + "/nonExistentParamSet"); + if (harness.getBaseURL().contains("/____v2/")) { + assertTrue(missing, missing.contains("No paramset found with the name: nonExistentParamSet")); + } else { + assertFalse(missing, missing.contains("nonExistentParamSet")); + assertTrue(missing, missing.contains("znodeVersion")); + } payload = "{\n" @@ -710,7 +732,7 @@ public class TestSolrConfigHandler extends RestTestBase { TestSolrConfigHandler.runConfigCommand(harness, "/config/params", payload); TestSolrConfigHandler.testForResponseElement( - harness, "/config/params", asList("response", "params", "y", "c"), "CY val", TIMEOUT_S); + harness, "/config/" + paramSetsKey, paramSetsPath(harness, "y", "c"), "CY val", TIMEOUT_S); TestSolrConfigHandler.testForResponseElement( harness, "/dump1?useParams=y", asList("params", "c"), "CY val", TIMEOUT_S); @@ -738,13 +760,13 @@ public class TestSolrConfigHandler extends RestTestBase { TestSolrConfigHandler.testForResponseElement( harness, - "/config/params", - asList("response", "params", "y", "c"), + "/config/" + paramSetsKey, + paramSetsPath(harness, "y", "c"), "CY val modified", TIMEOUT_S); TestSolrConfigHandler.testForResponseElement( - harness, "/config/params", asList("response", "params", "y", "e"), "EY val", TIMEOUT_S); + harness, "/config/" + paramSetsKey, paramSetsPath(harness, "y", "e"), "EY val", TIMEOUT_S); payload = " {\n" @@ -757,14 +779,14 @@ public class TestSolrConfigHandler extends RestTestBase { TestSolrConfigHandler.runConfigCommand(harness, "/config/params", payload); TestSolrConfigHandler.testForResponseElement( - harness, "/config/params", asList("response", "params", "y", "p"), "P val", TIMEOUT_S); + harness, "/config/" + paramSetsKey, paramSetsPath(harness, "y", "p"), "P val", TIMEOUT_S); TestSolrConfigHandler.testForResponseElement( - harness, "/config/params", asList("response", "params", "y", "c"), null, TIMEOUT_S); + harness, "/config/" + paramSetsKey, paramSetsPath(harness, "y", "c"), null, TIMEOUT_S); payload = " {'delete' : 'y'}"; TestSolrConfigHandler.runConfigCommand(harness, "/config/params", payload); TestSolrConfigHandler.testForResponseElement( - harness, "/config/params", asList("response", "params", "y", "p"), null, TIMEOUT_S); + harness, "/config/" + paramSetsKey, paramSetsPath(harness, "y", "p"), null, TIMEOUT_S); payload = "{\n" 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 a671cfce9f2..6efac088d30 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 @@ -17,9 +17,7 @@ package org.apache.solr.handler.admin; -import org.apache.solr.api.AnnotatedApi; import org.apache.solr.handler.SolrConfigHandler; -import org.apache.solr.handler.admin.api.GetConfigAPI; import org.apache.solr.handler.admin.api.ModifyConfigComponentAPI; import org.apache.solr.handler.admin.api.ModifyParamSetAPI; import org.junit.Test; @@ -39,29 +37,10 @@ public class V2ConfigAPIMappingTest extends V2ApiMappingTest<SolrConfigHandler> @Override public void populateApiBag() { - apiBag.registerObject(new GetConfigAPI(getRequestHandler())); apiBag.registerObject(new ModifyConfigComponentAPI(getRequestHandler())); apiBag.registerObject(new ModifyParamSetAPI(getRequestHandler())); } - @Test - public void testGetParamsetsConfig() { - assertAnnotatedApiExistsFor("GET", "/config/params"); - final AnnotatedApi getParamSetsApi = getAnnotatedApiFor("GET", "/config/params"); - // Ensure that the /config/params path redirects to the /config/params specific endpoint (and - // not the generic "/config/{component}") - final String[] getParamSetsApiPaths = getParamSetsApi.getEndPoint().path(); - assertEquals(1, getParamSetsApiPaths.length); - assertEquals("/config/params", getParamSetsApiPaths[0]); - - assertAnnotatedApiExistsFor("GET", "/config/params/someParamSet"); - final AnnotatedApi getSingleParamSetApi = - getAnnotatedApiFor("GET", "/config/params/someParamSet"); - final String[] getSingleParamsetApiPaths = getSingleParamSetApi.getEndPoint().path(); - assertEquals(1, getSingleParamsetApiPaths.length); - assertEquals("/config/params/{paramset}", getSingleParamsetApiPaths[0]); - } - @Test public void testModifyConfigComponentApis() { assertAnnotatedApiExistsFor("POST", "config"); diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/GetParamSetsTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/GetParamSetsTest.java new file mode 100644 index 00000000000..a74bcd9eb6f --- /dev/null +++ b/solr/core/src/test/org/apache/solr/handler/admin/api/GetParamSetsTest.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.handler.admin.api; + +import static org.apache.solr.core.CoreContainer.ALLOW_PATHS_SYSPROP; + +import java.nio.file.Path; +import java.util.Map; +import org.apache.commons.io.file.PathUtils; +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.SolrRequest; +import org.apache.solr.client.solrj.request.ParamSetsApi; +import org.apache.solr.client.solrj.request.V2Request; +import org.apache.solr.util.ExternalPaths; +import org.apache.solr.util.SolrJettyTestRule; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +/** HTTP tests for fetching request param sets through the generated SolrJ request. */ +public class GetParamSetsTest extends SolrTestCase { + + private static final String CORE_NAME = "paramSetsApiTestCore"; + + @ClassRule public static final SolrJettyTestRule solrTestRule = new SolrJettyTestRule(); + + @BeforeClass + public static void setupSolr() throws Exception { + // setting param sets writes params.json into the configset, so use a private copy + Path configSet = createTempDir("configSet"); + System.setProperty(ALLOW_PATHS_SYSPROP, configSet.toAbsolutePath().toString()); + solrTestRule.startSolr(createTempDir()); + PathUtils.copyDirectory(ExternalPaths.DEFAULT_CONFIGSET, configSet); + solrTestRule.newCollection(CORE_NAME).withConfigSet(configSet).create(); + } + + private static void setParamSet(String name, String payloadBody) throws Exception { + new V2Request.Builder("/cores/" + CORE_NAME + "/config/params") + .withMethod(SolrRequest.METHOD.POST) + .withPayload("{'set': {'" + name + "': " + payloadBody + "}}") + .build() + .process(solrTestRule.getSolrClient(CORE_NAME)); + } + + @Test + @SuppressWarnings("unchecked") + public void testListParamSets() throws Exception { + setParamSet("listA", "{'a': 'A val'}"); + setParamSet("listB", "{'b': 'B val'}"); + + var response = + new ParamSetsApi.ListParamSets(IndexType.CORE, CORE_NAME) + .process(solrTestRule.getSolrClient(CORE_NAME)); + + assertNotNull(response); + assertNull(response.error); + assertTrue(response.znodeVersion >= 0); + assertNotNull(response.params); + assertEquals("A val", ((Map<String, Object>) response.params.get("listA")).get("a")); + assertEquals("B val", ((Map<String, Object>) response.params.get("listB")).get("b")); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetSingleParamSet() throws Exception { + setParamSet("single", "{'a': 'A val', '_invariants_': {'wt': 'json'}}"); + + var response = + new ParamSetsApi.GetParamSet(IndexType.CORE, CORE_NAME, "single") + .process(solrTestRule.getSolrClient(CORE_NAME)); + + assertNull(response.error); + assertEquals(1, response.params.size()); + Map<String, Object> paramSet = (Map<String, Object>) response.params.get("single"); + assertEquals("A val", paramSet.get("a")); + assertEquals(Map.of("wt", "json"), paramSet.get("_invariants_")); + } + + @Test + public void testGetUnknownParamSetReturnsNotFound() { + var request = new ParamSetsApi.GetParamSet(IndexType.CORE, CORE_NAME, "nonExistentParamSet"); + final RemoteSolrException ex = + expectThrows( + RemoteSolrException.class, + () -> request.process(solrTestRule.getSolrClient(CORE_NAME))); + assertEquals(404, ex.code()); + assertTrue(ex.getMessage(), ex.getMessage().contains("nonExistentParamSet")); + } +} 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 c045ba30725..67b9557abfd 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 @@ -34,7 +34,8 @@ All Config API endpoints are collection-specific, meaning this API can inspect o Use GET to retrieve and POST for executing commands. * `_collection_/config/overlay`: retrieve the details in the `configoverlay.json` only, removing any options defined in `solrconfig.xml` directly or implicitly through defaults. * `_collection_/config/params`: create parameter sets that can override or take the place of parameters defined in `solrconfig.xml`. -See xref:request-parameters-api.adoc[] for more information about this endpoint. +In the V2 API, use `_collection_/config/paramsets` to read them. +See xref:request-parameters-api.adoc[] for more information about these endpoints. == Retrieving the Config diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/request-parameters-api.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/request-parameters-api.adoc index cc1af781d49..fc5820a18ff 100644 --- a/solr/solr-ref-guide/modules/configuration-guide/pages/request-parameters-api.adoc +++ b/solr/solr-ref-guide/modules/configuration-guide/pages/request-parameters-api.adoc @@ -159,8 +159,13 @@ curl 'http://localhost:8983/solr/techproducts/config/requestHandler?componentNam == Viewing Request Parameters -To see the paramsets that have been created, you can use the `/config/params` endpoint to read the contents of `params.json`, or use the name in the request: +To see the paramsets that have been created, we can read the contents of `params.json`, or request a single paramset by name: +[tabs#viewparamsets] +====== +V1 API:: ++ +==== [source,bash] ---- curl 'http://localhost:8983/solr/techproducts/config/params' @@ -168,6 +173,26 @@ curl 'http://localhost:8983/solr/techproducts/config/params' #Or use the paramset name curl 'http://localhost:8983/solr/techproducts/config/params/myQueries' ---- +==== + +V2 API:: ++ +==== +[source,bash] +---- +curl http://localhost:8983/api/collections/techproducts/config/paramsets + +#Or use the paramset name +curl http://localhost:8983/api/collections/techproducts/config/paramsets/myQueries +---- + +Creating, updating and deleting paramsets still uses `POST api/collections/techproducts/config/params`. + +==== + +Both APIs return a `404` if the named paramset does not exist. + +====== == The useParams Parameter diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiParamsetsScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiParamsetsScreenTest.java index a5f3bba025c..cbaf9871766 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiParamsetsScreenTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiParamsetsScreenTest.java @@ -62,6 +62,8 @@ public class AdminUiParamsetsScreenTest extends AdminUiTestBase { /** Returns the rows param of the paramset, or empty string when absent. */ private String paramsetRows(String name) { try { + // adminApi() is a v1 request; v1's /config/params path and response shape are unchanged + // by the GET /config/paramsets v2 migration. Object rows = adminApi("/" + COLLECTION + "/config/params/" + name, params()) ._get(List.of("response", "params", name, "rows"), ""); diff --git a/solr/webapp/web/js/angular/controllers/paramsets.js b/solr/webapp/web/js/angular/controllers/paramsets.js index d466c31a8a6..32d0ecec29b 100644 --- a/solr/webapp/web/js/angular/controllers/paramsets.js +++ b/solr/webapp/web/js/angular/controllers/paramsets.js @@ -17,7 +17,7 @@ //helper for formatting JSON and others solrAdminApp.controller('ParamSetsController', - function($scope, $location, $routeParams, ParamSet, Constants) { + function($scope, $location, $routeParams, $timeout, ParamSet, ParamSetsV2, Constants, ApiErrorHandler) { $scope.paramsetList = []; @@ -33,7 +33,7 @@ solrAdminApp.controller('ParamSetsController', } } - // The v2 config/params API needs to know up front whether ":core" is a collection name + // The v2 config/paramsets API needs to know up front whether ":core" is a collection name // (SolrCloud) or an actual core name (standalone/user-managed) -- there's no equivalent of // v1's flexible core-or-collection routing. $scope.paramSetIndexType = function() { @@ -48,60 +48,43 @@ solrAdminApp.controller('ParamSetsController', $scope.getParamset = function (paramset) { $scope.refresh(); - var params = {}; - params.core = $routeParams.core; - params.indexType = $scope.paramSetIndexType(); - params.wt = "json"; - params.name = paramset; - - ParamSet.get(params, callback, failure); - - /////// - - function callback(success) { - $scope.responseStatus = "success"; - delete success.$promise; - delete success.$resolved; - $scope.response = JSON.stringify(success, null, ' '); - var apiPayload = { - "set": success.response.params - }; - // remove json key that is defined as "", it can't be submitted via the API. - var paramsetName = Object.keys(apiPayload.set)[0]; - delete apiPayload.set[paramsetName][""] - - $scope.paramsetContent = JSON.stringify(apiPayload, null, ' '); + // The dropdown's blank option (deselecting) fires this with an empty name; the generated + // client requires a real paramSetName, so just leave the content cleared in that case. + if (!paramset) { + return; } - function failure (failure) { - $scope.responseStatus = failure; - } + ParamSetsV2.getParamSet($scope.paramSetIndexType(), $routeParams.core, paramset, function(error, data, response) { + $timeout(function() { + if (error) { ApiErrorHandler.handle(response); $scope.responseStatus = response; return; } + + $scope.responseStatus = "success"; + $scope.response = JSON.stringify(data, null, ' '); + var apiPayload = { + "set": data.params + }; + // remove json key that is defined as "", it can't be submitted via the API. + var paramsetName = Object.keys(apiPayload.set)[0]; + delete apiPayload.set[paramsetName][""] + + $scope.paramsetContent = JSON.stringify(apiPayload, null, ' '); + }); + }); } $scope.getParamsets = function () { $scope.refresh(); - var params = {}; - params.core = $routeParams.core; - params.indexType = $scope.paramSetIndexType(); - params.wt = "json"; - - ParamSet.get(params, callback, failure); + ParamSetsV2.listParamSets($scope.paramSetIndexType(), $routeParams.core, function(error, data, response) { + $timeout(function() { + if (error) { ApiErrorHandler.handle(response); $scope.responseStatus = response; return; } - /////// - - function callback(success) { - $scope.responseStatus = "success"; - delete success.$promise; - delete success.$resolved; - $scope.response = JSON.stringify(success, null, ' '); - $scope.paramsetList = success.response.params ? Object.keys(success.response.params) : []; - } - - function failure (failure) { - $scope.responseStatus = failure; - } + $scope.responseStatus = "success"; + $scope.response = JSON.stringify(data, null, ' '); + $scope.paramsetList = data.params ? Object.keys(data.params) : []; + }); + }); } // resetMenu() above populates $scope.isCloudEnabled asynchronously, so calling diff --git a/solr/webapp/web/js/angular/controllers/query.js b/solr/webapp/web/js/angular/controllers/query.js index 1d135e2069c..e11702ba1a3 100644 --- a/solr/webapp/web/js/angular/controllers/query.js +++ b/solr/webapp/web/js/angular/controllers/query.js @@ -16,7 +16,7 @@ */ solrAdminApp.controller('QueryController', - function($scope, $route, $routeParams, $location, Query, Constants, ParamSet){ + function($scope, $route, $routeParams, $location, $timeout, Query, Constants, ParamSetsV2, ApiErrorHandler){ $scope.resetMenu("query", Constants.IS_COLLECTION_PAGE); $scope._models = []; @@ -40,28 +40,18 @@ solrAdminApp.controller('QueryController', }); function getParamsets() { - - var params = {}; - params.core = $routeParams.core; - // The v2 config/params API needs to know up front whether ":core" is a collection name + // The v2 config/paramsets API needs to know up front whether ":core" is a collection name // (SolrCloud) or an actual core name (standalone/user-managed); see paramsets.js for details. - params.indexType = $scope.isCloudEnabled ? "collections" : "cores"; - params.wt = "json"; - - ParamSet.get(params, callback, failure); + var indexType = $scope.isCloudEnabled ? "collections" : "cores"; - /////// + ParamSetsV2.listParamSets(indexType, $routeParams.core, function(error, data, response) { + $timeout(function() { + if (error) { ApiErrorHandler.handle(response); $scope.responseStatus = response; return; } - function callback(success) { - $scope.responseStatus = "success"; - delete success.$promise; - delete success.$resolved; - $scope.paramsetList = success.response.params ? Object.keys(success.response.params) : []; - } - - function failure (failure) { - $scope.responseStatus = failure; - } + $scope.responseStatus = "success"; + $scope.paramsetList = data.params ? Object.keys(data.params) : []; + }); + }); } // get list of ng-models that have a form element diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 39441a90724..2cac309ec3b 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -260,20 +260,23 @@ solrAdminServices.factory('Metrics', "postCsv": {headers: {'Content-type': 'application/csv'}, method: "POST", params: {handler: '@handler'}} }); }]) +.factory('ParamSetsV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.ParamSetsApi(); + }) .factory('ParamSet', ['$resource', function($resource) { - // v2 GetConfigAPI/ModifyParamSetAPI (/api/(cores|collections)/:core/config/params) still - // delegate straight through to the same v1 SolrConfigHandler, so the response shape is - // byte-identical -- no generated solrApi client class exists for it (old-style @EndPoint API, - // predates the OpenAPI-based v2 framework), so this stays a plain $resource. + // Creating, updating and deleting paramsets is still on ModifyParamSetAPI (/config/params) -- + // reading them goes through ParamSetsV2 instead (see paramsets.js, query.js). // NB: unlike v1's flexible routing, the v2 API requires knowing up front whether ":core" is a // collection name (SolrCloud) or an actual core name (standalone/user-managed) -- // /api/collections/... 500s in standalone mode (it tries to resolve aliases, which needs ZK), // and there's no such thing as a "collection" there anyway. Callers must pass "indexType" as // "collections" or "cores" (see paramsets.js, driven by $scope.isCloudEnabled). - return $resource('/api/:indexType/:core/config/params/:name', {core: '@core', indexType: '@indexType', wt:'json', _:Date.now()}, { - "submit": {headers: {'Content-type': 'application/json'}, method: "POST"}, - "get": {headers: {'Content-type': 'application/json'}, method: "GET"} + return $resource('/api/:indexType/:core/config/params', {core: '@core', indexType: '@indexType', wt:'json', _:Date.now()}, { + "submit": {headers: {'Content-type': 'application/json'}, method: "POST"} }); }]) .service('FileUpload', function ($http) {
