This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch branch_10_1
in repository https://gitbox.apache.org/repos/asf/solr.git

commit f3d5f420b9b205d894f6da57149f249582fc49ec
Author: Jalaz Kumar <[email protected]>
AuthorDate: Sat Oct 3 21:08:30 2026 +0530

    SOLR-16396 | Migration of Get Config V2 API to JAX-RS construct (#4951)
    
    Co-authored-by: Eric Pugh <[email protected]>
    (cherry picked from commit 14c7aac0d151402b00259e2fb9bf5eed7049ec5d)
    (cherry picked from commit 33e3f25e51ede7097b4f706965df8ab723b462f7)
---
 changelog/unreleased/SOLR-16396-config-get.yml     |  14 +++
 .../apache/solr/client/api/endpoint/ConfigApi.java |  46 ++++++++
 .../solr/client/api/model/ConfigInfoResponse.java  |  25 +++++
 .../org/apache/solr/handler/SolrConfigHandler.java |  11 +-
 .../apache/solr/handler/admin/api/GetConfig.java   | 123 +++++++++++++++++++++
 .../solr/handler/admin/api/GetConfigAPI.java       |  12 +-
 .../org/apache/solr/jersey/NullKeyTolerantMap.java |  88 +++++++++++++++
 .../solr/handler/admin/V2ConfigAPIMappingTest.java |   6 -
 .../handler/admin/api/GetConfigSolrCloudTest.java  |  55 +++++++++
 .../solr/handler/admin/api/GetConfigTest.java      |  59 ++++++++++
 .../configuration-guide/pages/config-api.adoc      |  32 +++++-
 solr/webapp/web/js/angular/controllers/schema.js   |  14 ++-
 solr/webapp/web/js/angular/services.js             |  12 +-
 13 files changed, 468 insertions(+), 29 deletions(-)

diff --git a/changelog/unreleased/SOLR-16396-config-get.yml 
b/changelog/unreleased/SOLR-16396-config-get.yml
new file mode 100644
index 00000000000..729eb3eaa30
--- /dev/null
+++ b/changelog/unreleased/SOLR-16396-config-get.yml
@@ -0,0 +1,14 @@
+title:
+  Migration of Get Config V2 API to JAX-RS construct. Note one response-shape 
difference from V1 -
+  a plugin's unnamed child elements (e.g. anonymous `processor` entries under 
an
+  `updateRequestProcessorChain`) are now grouped under a `children` key in the 
V2 JSON response,
+  instead of V1's legacy empty-string (`""`) key.
+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
new file mode 100644
index 00000000000..3e0ab463af2
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java
@@ -0,0 +1,46 @@
+/*
+ * 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.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.QueryParam;
+import org.apache.solr.client.api.model.ConfigInfoResponse;
+import org.apache.solr.client.api.util.StoreApiParameters;
+
+/** V2 API for reading and modifying a core/collection's config */
+public interface ConfigApi {
+
+  @Path(INDEX_PATH_PREFIX + "/config")
+  interface Get {
+    @GET
+    @StoreApiParameters
+    @Operation(
+        summary = "Fetch the entire config of the specified core or 
collection",
+        tags = {"config"})
+    ConfigInfoResponse getConfig(
+        @QueryParam("expandParams")
+            @DefaultValue("false")
+            @Parameter(description = "Expand request handler useParams and 
effective parameters")
+            boolean expandParams);
+  }
+}
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/ConfigInfoResponse.java 
b/solr/api/src/java/org/apache/solr/client/api/model/ConfigInfoResponse.java
new file mode 100644
index 00000000000..e57a59c726f
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/ConfigInfoResponse.java
@@ -0,0 +1,25 @@
+/*
+ * 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 ConfigInfoResponse extends SolrJerseyResponse {
+  @JsonProperty("config")
+  public Map<String, Object> config;
+}
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 dc2fcc2b7fc..f8b96086fd7 100644
--- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
@@ -52,6 +52,7 @@ import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import org.apache.solr.api.AnnotatedApi;
 import org.apache.solr.api.Api;
+import org.apache.solr.api.JerseyResource;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.client.solrj.impl.HttpSolrClient;
@@ -85,9 +86,11 @@ import org.apache.solr.core.RequestParams;
 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.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;
@@ -194,7 +197,8 @@ public class SolrConfigHandler extends RequestHandlerBase
     private void handleGET() {
       if (parts.size() == 1) {
         // this is the whole config. sent out the whole payload
-        resp.add("config", getConfigDetails(null, req));
+        V2ApiUtils.squashIntoSolrResponseWithoutHeader(
+            resp, new 
GetConfig(req).getConfig(req.getParams().getBool("expandParams", false)));
       } else {
         if (ConfigOverlay.NAME.equals(parts.get(1))) {
           resp.add(ConfigOverlay.NAME, 
req.getCore().getSolrConfig().getOverlay());
@@ -1066,6 +1070,11 @@ public class SolrConfigHandler extends RequestHandlerBase
     return apis;
   }
 
+  @Override
+  public Collection<Class<? extends JerseyResource>> getJerseyResources() {
+    return List.of(GetConfig.class);
+  }
+
   @Override
   public Boolean registerV2() {
     return Boolean.TRUE;
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
new file mode 100644
index 00000000000..d46738ea347
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
@@ -0,0 +1,123 @@
+/*
+ * 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.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.security.PermissionNameProvider.Name.CONFIG_READ_PERM;
+
+import jakarta.inject.Inject;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.endpoint.ConfigApi;
+import org.apache.solr.client.api.model.ConfigInfoResponse;
+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.StrUtils;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.core.PluginInfo;
+import org.apache.solr.handler.RequestHandlerBase;
+import org.apache.solr.jersey.NullKeyTolerantMap;
+import org.apache.solr.jersey.PermissionName;
+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 {
+
+  private final SolrQueryRequest solrQueryRequest;
+
+  @Inject
+  public GetConfig(SolrQueryRequest solrQueryRequest) {
+    this.solrQueryRequest = solrQueryRequest;
+  }
+
+  @Override
+  @PermissionName(CONFIG_READ_PERM)
+  public ConfigInfoResponse getConfig(boolean expandParams) {
+    final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
+    response.config = buildConfigMap(solrQueryRequest, expandParams);
+    return response;
+  }
+
+  @SuppressWarnings({"unchecked"})
+  private Map<String, Object> buildConfigMap(
+      SolrQueryRequest solrQueryRequest, boolean expandParams) {
+    Map<String, Object> map =
+        Utils.convertToMap(solrQueryRequest.getCore().getSolrConfig(), new 
LinkedHashMap<>());
+
+    Map<String, Object> reqHandlers =
+        (Map<String, Object>)
+            map.computeIfAbsent(SolrRequestHandler.TYPE, k -> new 
LinkedHashMap<>());
+    List<PluginInfo> plugins = 
solrQueryRequest.getCore().getImplicitHandlers();
+    for (PluginInfo plugin : plugins) {
+      if (SolrRequestHandler.TYPE.equals(plugin.type) && 
!reqHandlers.containsKey(plugin.name)) {
+        reqHandlers.put(plugin.name, plugin);
+      }
+    }
+
+    if (expandParams) {
+      for (Map.Entry<String, Object> entry : reqHandlers.entrySet()) {
+        entry.setValue(expandUseParams(solrQueryRequest, entry.getValue()));
+      }
+    }
+
+    final var jsonSafeCopy = new NullKeyTolerantMap("children");
+    jsonSafeCopy.putAll((Map<String, Object>) Utils.getDeepCopy(map, 20, 
true));
+    return jsonSafeCopy;
+  }
+
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  private Map<String, Object> expandUseParams(SolrQueryRequest req, Object 
plugin) {
+    Map<String, Object> pluginInfo;
+    if (plugin instanceof Map) {
+      pluginInfo = (Map<String, Object>) plugin;
+    } else {
+      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<String, Object> expanded = new LinkedHashMap<>();
+      pluginInfo.put("_useParamsExpanded_", expanded);
+      List<String> params = new ArrayList<>();
+      if (useParams != null) params.addAll(StrUtils.splitSmart(useParams, 
','));
+      if (useParamsInReq != null) 
params.addAll(StrUtils.splitSmart(useParamsInReq, ','));
+      for (String param : params) {
+        var paramSet = 
req.getCore().getSolrConfig().getRequestParams().getParams(param);
+        expanded.put(param, paramSet != null ? paramSet : "[NOT AVAILABLE]");
+      }
+      SolrQueryRequest subRequest = req.subRequest(req.getOriginalParams());
+      subRequest.getContext().put(USEPARAM, useParams);
+      NamedList<?> initArgs = new PluginInfo(SolrRequestHandler.TYPE, 
pluginInfo).initArgs;
+      SolrPluginUtils.setDefaults(
+          subRequest,
+          RequestHandlerBase.getSolrParamsFromNamedList(initArgs, DEFAULTS),
+          RequestHandlerBase.getSolrParamsFromNamedList(initArgs, APPENDS),
+          RequestHandlerBase.getSolrParamsFromNamedList(initArgs, INVARIANTS));
+      SolrParams mask = new MapSolrParams(Map.of("componentName", "", 
"expandParams", ""));
+      pluginInfo.put("_effectiveParams_", SolrParams.wrapDefaults(mask, 
subRequest.getParams()));
+    }
+    return pluginInfo;
+  }
+}
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 8c8dcd25d53..4dab3ed7459 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
@@ -28,8 +28,8 @@ import org.apache.solr.response.SolrQueryResponse;
 /**
  * V2 APIs for retrieving some or all configuration relevant to a particular 
collection (or core).
  *
- * <p>This class covers a handful of distinct paths under GET 
/v2/c/collectionName/config: /,
- * /overlay, /params, /znodeVersion, etc.
+ * <p>This class covers legacy AnnotatedApi paths under GET /config: /params, 
/params{paramset},
+ * /overlay, /znodeVersion, etc.
  */
 public class GetConfigAPI {
   private final SolrConfigHandler configHandler;
@@ -38,14 +38,6 @@ public class GetConfigAPI {
     this.configHandler = configHandler;
   }
 
-  @EndPoint(
-      path = {"/config"},
-      method = GET,
-      permission = CONFIG_READ_PERM)
-  public void getAllConfig(SolrQueryRequest req, SolrQueryResponse rsp) {
-    configHandler.handleRequest(req, rsp);
-  }
-
   @EndPoint(
       path = {"/config/params"},
       method = GET,
diff --git a/solr/core/src/java/org/apache/solr/jersey/NullKeyTolerantMap.java 
b/solr/core/src/java/org/apache/solr/jersey/NullKeyTolerantMap.java
new file mode 100644
index 00000000000..90b87c2d625
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/jersey/NullKeyTolerantMap.java
@@ -0,0 +1,88 @@
+/*
+ * 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.jersey;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializable;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@link Map} that renders identically to a plain {@link LinkedHashMap} 
everywhere except through
+ * Jackson.
+ *
+ * <p>{@code org.apache.solr.core.PluginInfo#writeMap} groups a plugin's 
unnamed children (e.g.
+ * anonymous {@code <processor>} entries under an {@code 
updateRequestProcessorChain}) under a
+ * {@code null} key. Solr's legacy (v1) JSON response writer tolerates that 
null key natively,
+ * rendering it as {@code ""}. Jackson, however, rejects null Map keys 
outright and (by default)
+ * omits null-valued entries entirely.
+ *
+ * <p>Wrap a config/plugin-derived map in this class to let the v2/JAX-RS 
(Jackson) response path
+ * render a self-describing key in place of the null key and keep null values, 
without changing what
+ * any non-Jackson consumer (e.g. a v1 handler reading the same map instance) 
sees.
+ */
+public class NullKeyTolerantMap extends LinkedHashMap<String, Object> 
implements JsonSerializable {
+
+  private final String nullKeyReplacement;
+
+  /**
+   * @param nullKeyReplacement the JSON field name to substitute for a {@code 
null} Map key,
+   *     wherever one is found (at any nesting depth of Maps/Lists reachable 
from this instance).
+   */
+  public NullKeyTolerantMap(String nullKeyReplacement) {
+    this.nullKeyReplacement = nullKeyReplacement;
+  }
+
+  @Override
+  public void serialize(JsonGenerator gen, SerializerProvider serializers) 
throws IOException {
+    writeValue(this, gen, serializers);
+  }
+
+  @Override
+  public void serializeWithType(
+      JsonGenerator gen, SerializerProvider serializers, TypeSerializer 
typeSer)
+      throws IOException {
+    serialize(gen, serializers);
+  }
+
+  private void writeValue(Object value, JsonGenerator gen, SerializerProvider 
provider)
+      throws IOException {
+    if (value instanceof Map<?, ?> m) {
+      gen.writeStartObject();
+      for (Map.Entry<?, ?> e : m.entrySet()) {
+        Object key = e.getKey();
+        gen.writeFieldName(key == null ? nullKeyReplacement : key.toString());
+        writeValue(e.getValue(), gen, provider);
+      }
+      gen.writeEndObject();
+    } else if (value instanceof List<?> l) {
+      gen.writeStartArray();
+      for (Object item : l) {
+        writeValue(item, gen, provider);
+      }
+      gen.writeEndArray();
+    } else if (value == null) {
+      gen.writeNull();
+    } else {
+      provider.defaultSerializeValue(value, gen);
+    }
+  }
+}
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 3912e52d281..0a5aaab1f88 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
@@ -44,12 +44,6 @@ public class V2ConfigAPIMappingTest extends 
V2ApiMappingTest<SolrConfigHandler>
     apiBag.registerObject(new ModifyParamSetAPI(getRequestHandler()));
   }
 
-  // GET /v2/c/collectionName/config is a pure pass-through to the underlying 
request handler
-  @Test
-  public void testGetAllConfig() {
-    assertAnnotatedApiExistsFor("GET", "/config");
-  }
-
   // GET /v2/collectionName/config/<component> is a pure pass-through to the 
underlying request
   // handler.  Just check
   // the API lookup works for a handful of the valid config "components".
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
new file mode 100644
index 00000000000..297da96353d
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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 org.apache.solr.client.api.model.IndexType;
+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.util.ExternalPaths;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** HTTP tests for fetching the full config via the collection-scoped v2 path. 
*/
+public class GetConfigSolrCloudTest extends SolrCloudTestCase {
+
+  private static final String COLLECTION_NAME = "configApiTestCollection";
+
+  @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();
+    CollectionAdminRequest.createCollection(COLLECTION_NAME, "conf", 1, 1)
+        .process(cluster.getSolrClient());
+  }
+
+  @Test
+  public void testGetConfigFromCore() throws Exception {
+    var request = new ConfigApi.GetConfig(IndexType.COLLECTION, 
COLLECTION_NAME);
+    var response = request.process(cluster.getSolrClient());
+
+    assertNotNull(response);
+    assertNull(response.error);
+    assertNotNull(response.config);
+    assertTrue(response.config.containsKey("luceneMatchVersion"));
+    assertTrue(response.config.containsKey("updateHandler"));
+    assertTrue(response.config.containsKey("query"));
+    assertTrue(response.config.containsKey("requestHandler"));
+  }
+}
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
new file mode 100644
index 00000000000..c122e8d497a
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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 org.apache.solr.SolrTestCase;
+import org.apache.solr.client.api.model.IndexType;
+import org.apache.solr.client.solrj.request.ConfigApi;
+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 the full config through the generated SolrJ 
request. */
+public class GetConfigTest extends SolrTestCase {
+
+  private static final String CORE_NAME = "configApiTestCore";
+
+  @ClassRule public static final SolrJettyTestRule solrTestRule = new 
SolrJettyTestRule();
+
+  @BeforeClass
+  public static void setupSolr() throws Exception {
+    System.setProperty(ALLOW_PATHS_SYSPROP, 
ExternalPaths.SERVER_HOME.toAbsolutePath().toString());
+    solrTestRule.startSolr(createTempDir());
+    
solrTestRule.newCollection(CORE_NAME).withConfigSet(ExternalPaths.DEFAULT_CONFIGSET).create();
+  }
+
+  @Test
+  public void testGetConfigFromCore() throws Exception {
+    var request = new ConfigApi.GetConfig(IndexType.CORE, CORE_NAME);
+    var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+    assertNotNull(response);
+    assertNull(response.error);
+    assertNotNull(response.config);
+    assertTrue(response.config.containsKey("luceneMatchVersion"));
+    assertTrue(response.config.containsKey("updateHandler"));
+    assertTrue(response.config.containsKey("query"));
+    assertTrue(response.config.containsKey("requestHandler"));
+    assertTrue(response.config.containsKey("searchComponent"));
+    assertTrue(response.config.containsKey("updateProcessor"));
+  }
+}
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 93e94a69045..878e8dd0d6a 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
@@ -49,6 +49,21 @@ V1 API::
 ----
 http://localhost:8983/solr/techproducts/config
 ----
+
+The response is the Solr configuration resulting from merging settings in 
`configoverlay.json` with those in `solrconfig.xml`.
+A plugin's *unnamed* child elements - for example, the anonymous `<processor>` 
entries inside an `updateRequestProcessorChain` that has no `name` attribute of 
its own - are grouped under an empty-string (`""`) key, matching the legacy 
behavior of `solrconfig.xml -> JSON` conversion:
+
+[source,json]
+----
+"updateRequestProcessorChain": [{
+    "name": "add-unknown-fields-to-the-schema",
+    "": [
+      {"class": "solr.LogUpdateProcessorFactory"},
+      {"class": "solr.DistributedUpdateProcessorFactory"},
+      {"class": "solr.RunUpdateProcessorFactory"}
+    ]
+}]
+----
 ====
 
 V2 API::
@@ -58,11 +73,22 @@ V2 API::
 ----
 http://localhost:8983/api/collections/techproducts/config
 ----
-====
-======
 
-The response will be the Solr configuration resulting from merging settings in 
`configoverlay.json` with those in `solrconfig.xml`.
+The response is the same merged configuration as the V1 API, with one 
difference: a plugin's unnamed child elements are grouped under a 
self-describing `children` key instead of an empty string, since Solr's V2 JSON 
serialization library does not allow empty-string keys:
 
+[source,json]
+----
+"updateRequestProcessorChain": [{
+    "name": "add-unknown-fields-to-the-schema",
+    "children": [
+      {"class": "solr.LogUpdateProcessorFactory"},
+      {"class": "solr.DistributedUpdateProcessorFactory"},
+      {"class": "solr.RunUpdateProcessorFactory"}
+    ]
+}]
+----
+====
+======
 
 It's possible to restrict the returned configuration to a top-level section, 
such as, `query`, `requestHandler` or `updateHandler`.
 To do this, append the name of the section to the `config` endpoint.
diff --git a/solr/webapp/web/js/angular/controllers/schema.js 
b/solr/webapp/web/js/angular/controllers/schema.js
index d70f60d538e..0e1741bab2b 100644
--- a/solr/webapp/web/js/angular/controllers/schema.js
+++ b/solr/webapp/web/js/angular/controllers/schema.js
@@ -49,7 +49,7 @@ function schemaApiErrorMessage(data, response) {
 }
 
 solrAdminApp.controller('SchemaController',
-    function($scope, $routeParams, $location, $cookies, $timeout, Luke, 
Constants, SchemaV2, Config, ApiErrorHandler) {
+    function($scope, $routeParams, $location, $cookies, $timeout, Luke, 
Constants, SchemaV2, ConfigV2, ApiErrorHandler) {
         $scope.resetMenu("schema", Constants.IS_COLLECTION_PAGE);
 
         $scope.refresh = function () {
@@ -117,8 +117,16 @@ solrAdminApp.controller('SchemaController',
                     $scope.types = Object.keys(schema.schema.types);
                 });
             });
-            Config.get({core: $routeParams.core}, function(data) {
-                $scope.isSchemaUpdatable = 
(data.config.hasOwnProperty('schemaFactory') == false || 
data.config.schemaFactory.class == "ManagedIndexSchemaFactory");
+            var configIndexType = $scope.isCloudEnabled ? "collections" : 
"cores";
+            ConfigV2.getConfig(configIndexType, $routeParams.core, {}, 
function(error, data, response) {
+                $timeout(function() {
+                    if (error || (data && data.error)) {
+                        ApiErrorHandler.handle(response);
+                        return;
+                    }
+                    var schemaFactory = data.config && 
data.config.schemaFactory;
+                    $scope.isSchemaUpdatable = !schemaFactory || 
schemaFactory.class == "ManagedIndexSchemaFactory";
+                });
             });
         };
         $scope.refresh();
diff --git a/solr/webapp/web/js/angular/services.js 
b/solr/webapp/web/js/angular/services.js
index e6fa6b37cfa..c0b525b1698 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -160,6 +160,12 @@ solrAdminServices.factory('Metrics',
       delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"];
       return new solrApi.SchemaApi();
     })
+.factory('ConfigV2',
+    function() {
+      solrApi.ApiClient.instance.basePath = '/api';
+      delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"];
+      return new solrApi.ConfigApi();
+    })
 .factory('Collections',
   ['$resource', function ($resource) {
     // v2 ClusterAPI (/api/cluster) delegates straight through to the same v1 
CollectionsHandler
@@ -381,12 +387,6 @@ solrAdminServices.factory('Metrics',
        }
        return resource;
 }])
-.factory('Config',
-   ['$resource', function($resource) {
-     return $resource(':core/config', {wt: 'json', core: '@core', 
_:Date.now()}, {
-       get: {method: "GET"}
-     })
-}])
 .factory('SchemaDesigner',
    ['$resource', function($resource) {
      // Schema Designer's analyze (sample-doc upload/paste, dynamic 
content-type) and query

Reply via email to