You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by no...@apache.org on 2021/04/14 05:18:21 UTC

[solr] 01/01: untested

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

noble pushed a commit to branch jira/solr15337
in repository https://gitbox.apache.org/repos/asf/solr.git

commit 102d7cba756be062eb7e3fa4c292f5e1d298b1b1
Author: Noble Paul <no...@gmail.com>
AuthorDate: Wed Apr 14 15:18:03 2021 +1000

    untested
---
 .../java/org/apache/solr/core/ConfigOverlay.java   |  13 +-
 .../src/java/org/apache/solr/core/PluginInfo.java  |  24 +++
 .../src/java/org/apache/solr/core/SolrConfig.java  | 210 ++++++++++++---------
 .../java/org/apache/solr/search/CacheConfig.java   |  28 +--
 .../org/apache/solr/update/SolrIndexConfig.java    |  33 ++--
 .../org/apache/solr/core/TestCodecSupport.java     |   6 +-
 .../src/test/org/apache/solr/core/TestConfig.java  |  18 +-
 .../org/apache/solr/cluster/api/SimpleMap.java     |   8 +
 .../java/org/apache/solr/common/ConfigNode.java    |  77 ++++++++
 .../apache/solr/common/util/WrappedSimpleMap.java  |   5 +
 10 files changed, 293 insertions(+), 129 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/core/ConfigOverlay.java b/solr/core/src/java/org/apache/solr/core/ConfigOverlay.java
index 66991a4..069de5d 100644
--- a/solr/core/src/java/org/apache/solr/core/ConfigOverlay.java
+++ b/solr/core/src/java/org/apache/solr/core/ConfigOverlay.java
@@ -16,12 +16,14 @@
  */
 package org.apache.solr.core;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.solr.common.MapSerializable;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.CoreAdminParams;
@@ -62,6 +64,12 @@ public class ConfigOverlay implements MapSerializable {
     return Utils.getObjectByPath(props, onlyPrimitive, hierarchy);
   }
 
+  public Object getXPathProperty(List<String> path) {
+    List<String> hierarchy = new ArrayList<>();
+    if(isEditable(true, hierarchy, path) == null) return null;
+    return Utils.getObjectByPath(props, true, hierarchy);
+  }
+
   @SuppressWarnings({"unchecked"})
   public ConfigOverlay setUserProperty(String key, Object val) {
     @SuppressWarnings({"rawtypes"})
@@ -180,7 +188,10 @@ public class ConfigOverlay implements MapSerializable {
 
   @SuppressWarnings({"rawtypes"})
   public static Class checkEditable(String path, boolean isXpath, List<String> hierarchy) {
-    List<String> parts = StrUtils.splitSmart(path, isXpath ? '/' : '.');
+    return isEditable(isXpath, hierarchy, StrUtils.splitSmart(path, isXpath ? '/' : '.'));
+  }
+
+  private static Class isEditable(boolean isXpath, List<String> hierarchy, List<String> parts) {
     Object obj = editable_prop_map;
     for (int i = 0; i < parts.size(); i++) {
       String part = parts.get(i);
diff --git a/solr/core/src/java/org/apache/solr/core/PluginInfo.java b/solr/core/src/java/org/apache/solr/core/PluginInfo.java
index 9dff7d2..b32bac7 100644
--- a/solr/core/src/java/org/apache/solr/core/PluginInfo.java
+++ b/solr/core/src/java/org/apache/solr/core/PluginInfo.java
@@ -23,6 +23,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.solr.common.ConfigNode;
 import org.apache.solr.common.MapSerializable;
 import org.apache.solr.common.util.DOMUtil;
 import org.apache.solr.common.util.NamedList;
@@ -99,6 +100,18 @@ public class PluginInfo implements MapSerializable {
   }
 
 
+  public PluginInfo(ConfigNode node,  String err,boolean requireName, boolean requireClass) {
+    type = node.name();
+    name = node.requiredStrAttr(NAME,requireName? () -> new RuntimeException(err + ": missing mandatory attribute 'name'"):null);
+    cName = parseClassName(node.requiredStrAttr(CLASS_NAME, requireClass? () -> new RuntimeException(err + ": missing mandatory attribute 'class'"):null ));
+    className = cName.className;
+    pkgName = cName.pkg;
+    initArgs = DOMUtil.childNodesToNamedList(node);
+    attributes = node.attributes().asMap();
+    children = loadSubPlugins(node);
+    isFromSolrConfig = true;
+
+  }
   public PluginInfo(Node node, String err, boolean requireName, boolean requireClass) {
     type = node.getNodeName();
     name = DOMUtil.getAttr(node, NAME, requireName ? err : null);
@@ -143,6 +156,17 @@ public class PluginInfo implements MapSerializable {
     isFromSolrConfig = true;
   }
 
+  private List<PluginInfo> loadSubPlugins(ConfigNode node) {
+    List<PluginInfo> children = new ArrayList<>();
+    //if there is another sub tag with a non namedlist tag that has to be another plugin
+    node.forEachChild(nd -> {
+      if (NL_TAGS.contains(nd.name())) return null;
+      PluginInfo pluginInfo = new PluginInfo(nd, null, false, false);
+      if (pluginInfo.isEnabled()) children.add(pluginInfo);
+      return null;
+    });
+    return children.isEmpty() ? Collections.<PluginInfo>emptyList() : unmodifiableList(children);
+  }
   private List<PluginInfo> loadSubPlugins(Node node) {
     List<PluginInfo> children = new ArrayList<>();
     //if there is another sub tag with a non namedlist tag that has to be another plugin
diff --git a/solr/core/src/java/org/apache/solr/core/SolrConfig.java b/solr/core/src/java/org/apache/solr/core/SolrConfig.java
index 804ceab..65f5043 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrConfig.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrConfig.java
@@ -18,7 +18,6 @@ package org.apache.solr.core;
 
 
 import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.xpath.XPathConstants;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -51,11 +50,12 @@ import org.apache.lucene.util.Version;
 import org.apache.solr.client.solrj.io.stream.expr.Expressible;
 import org.apache.solr.cloud.RecoveryStrategy;
 import org.apache.solr.cloud.ZkSolrResourceLoader;
+import org.apache.solr.common.ConfigNode;
 import org.apache.solr.common.MapSerializable;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
-import org.apache.solr.common.util.DOMUtil;
 import org.apache.solr.common.util.IOUtils;
+import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.handler.component.SearchComponent;
 import org.apache.solr.pkg.PackageListeners;
 import org.apache.solr.pkg.PackageLoader;
@@ -77,11 +77,12 @@ import org.apache.solr.update.SolrIndexConfig;
 import org.apache.solr.update.UpdateLog;
 import org.apache.solr.update.processor.UpdateRequestProcessorChain;
 import org.apache.solr.update.processor.UpdateRequestProcessorFactory;
+import org.apache.solr.util.DOMConfigNode;
+import org.apache.solr.util.DataConfigNode;
 import org.apache.solr.util.circuitbreaker.CircuitBreakerManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
 import org.xml.sax.SAXException;
 
 import static org.apache.solr.common.params.CommonParams.NAME;
@@ -95,6 +96,7 @@ import static org.apache.solr.core.SolrConfig.PluginOpts.NOOP;
 import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_CLASS;
 import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME;
 import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME_IN_OVERLAY;
+import static org.apache.solr.core.XmlConfigFile.assertWarnOrFail;
 
 
 /**
@@ -102,12 +104,14 @@ import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME_IN_OVERLAY
  * configuration data for a Solr instance -- typically found in
  * "solrconfig.xml".
  */
-public class SolrConfig extends XmlConfigFile implements MapSerializable {
+public class SolrConfig implements MapSerializable {
 
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   public static final String DEFAULT_CONF_FILE = "solrconfig.xml";
 
+  private XmlConfigFile xml;
+  ConfigNode root;
 
   private RequestParams requestParams;
 
@@ -171,25 +175,30 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
   private SolrConfig(SolrResourceLoader loader, String name, boolean isConfigsetTrusted, Properties substitutableProperties)
       throws ParserConfigurationException, IOException, SAXException {
     // insist we have non-null substituteProperties; it might get overlayed
-    super(loader, name, null, "/config/", substitutableProperties == null ? new Properties() : substitutableProperties);
+    xml = new XmlConfigFile(loader, name, null, "/config/", substitutableProperties == null ? new Properties() : substitutableProperties );
+    root = new DataConfigNode(new DOMConfigNode(xml.getDocument().getDocumentElement()));
+//    super(loader, name, null, "/config/", substitutableProperties == null ? new Properties() : substitutableProperties);
     getOverlay();//just in case it is not initialized
     getRequestParams();
     initLibs(loader, isConfigsetTrusted);
-    luceneMatchVersion = SolrConfig.parseLuceneVersionString(getVal(IndexSchema.LUCENE_MATCH_VERSION_PARAM, true));
+    String val =  root.child(IndexSchema.LUCENE_MATCH_VERSION_PARAM,
+        () -> new RuntimeException("Missing: "+ IndexSchema.LUCENE_MATCH_VERSION_PARAM)) .textValue() ;
+
+    luceneMatchVersion = SolrConfig.parseLuceneVersionString(val);
     log.info("Using Lucene MatchVersion: {}", luceneMatchVersion);
 
     String indexConfigPrefix;
 
     // Old indexDefaults and mainIndex sections are deprecated and fails fast for luceneMatchVersion=>LUCENE_4_0_0.
     // For older solrconfig.xml's we allow the old sections, but never mixed with the new <indexConfig>
-    boolean hasDeprecatedIndexConfig = (getNode("indexDefaults", false) != null) || (getNode("mainIndex", false) != null);
+    boolean hasDeprecatedIndexConfig = (root.child("indexDefaults") != null) || (root.child("mainIndex") != null);
     if (hasDeprecatedIndexConfig) {
       throw new SolrException(ErrorCode.FORBIDDEN, "<indexDefaults> and <mainIndex> configuration sections are discontinued. Use <indexConfig> instead.");
     } else {
       indexConfigPrefix = "indexConfig";
     }
     assertWarnOrFail("The <nrtMode> config has been discontinued and NRT mode is always used by Solr." +
-            " This config will be removed in future versions.", getNode(indexConfigPrefix + "/nrtMode", false) == null,
+            " This config will be removed in future versions.", root.__("indexDefaults").child("nrtMode") == null,
         true
     );
     assertWarnOrFail("Solr no longer supports forceful unlocking via the 'unlockOnStartup' option.  "+
@@ -197,14 +206,17 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
                      "it would be dangerous and should not be done.  For other lockTypes and/or "+
                      "directoryFactory options it may also be dangerous and users must resolve "+
                      "problematic locks manually.",
-                     null == getNode(indexConfigPrefix + "/unlockOnStartup", false),
+                     null == root.__(indexConfigPrefix).child( "unlockOnStartup"),
                      true // 'fail' in trunk
                      );
                      
     // Parse indexConfig section, using mainIndex as backup in case old config is used
     indexConfig = new SolrIndexConfig(this, "indexConfig", null);
 
-    booleanQueryMaxClauseCount = getInt("query/maxBooleanClauses", IndexSearcher.getMaxClauseCount());
+    booleanQueryMaxClauseCount = root
+        .__("query")
+        .__( "maxBooleanClauses")
+        ._int(IndexSearcher.getMaxClauseCount());
     if (IndexSearcher.getMaxClauseCount() < booleanQueryMaxClauseCount) {
       log.warn("solrconfig.xml: <maxBooleanClauses> of {} is greater than global limit of {} {}"
           , booleanQueryMaxClauseCount, IndexSearcher.getMaxClauseCount()
@@ -213,9 +225,9 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
     
     // Warn about deprecated / discontinued parameters
     // boolToFilterOptimizer has had no effect since 3.1
-    if (get("query/boolTofilterOptimizer", null) != null)
+    if (__("query").child("boolTofilterOptimizer") != null)
       log.warn("solrconfig.xml: <boolTofilterOptimizer> is currently not implemented and has no effect.");
-    if (get("query/HashDocSet", null) != null)
+    if (__("query").child("HashDocSet") != null)
       log.warn("solrconfig.xml: <HashDocSet> is deprecated and no longer used.");
 
 // TODO: Old code - in case somebody wants to re-enable. Also see SolrIndexSearcher#search()
@@ -223,15 +235,15 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
 //    filtOptCacheSize = getInt("query/boolTofilterOptimizer/@cacheSize",32);
 //    filtOptThreshold = getFloat("query/boolTofilterOptimizer/@threshold",.05f);
 
-    useFilterForSortedQuery = getBool("query/useFilterForSortedQuery", false);
-    queryResultWindowSize = Math.max(1, getInt("query/queryResultWindowSize", 1));
-    queryResultMaxDocsCached = getInt("query/queryResultMaxDocsCached", Integer.MAX_VALUE);
-    enableLazyFieldLoading = getBool("query/enableLazyFieldLoading", false);
+    useFilterForSortedQuery = __("query").__("useFilterForSortedQuery")._bool(false);
+    queryResultWindowSize = Math.max(1, __("query").__("queryResultWindowSize")._int(1));
+    queryResultMaxDocsCached = __("query").__("queryResultMaxDocsCached")._int(Integer.MAX_VALUE);
+    enableLazyFieldLoading = __("query").__("enableLazyFieldLoading")._bool(false);
     
-    filterCacheConfig = CacheConfig.getConfig(this, "query/filterCache");
-    queryResultCacheConfig = CacheConfig.getConfig(this, "query/queryResultCache");
-    documentCacheConfig = CacheConfig.getConfig(this, "query/documentCache");
-    CacheConfig conf = CacheConfig.getConfig(this, "query/fieldValueCache");
+    filterCacheConfig = CacheConfig.getConfig(this, __("query").child("filterCache"), "query/filterCache");
+    queryResultCacheConfig = CacheConfig.getConfig(this, __("query").child("queryResultCache"), "query/queryResultCache");
+    documentCacheConfig = CacheConfig.getConfig(this, __("query").child("documentCache"), "query/documentCache");
+    CacheConfig conf = CacheConfig.getConfig(this, __("query").child("fieldValueCache"), "query/fieldValueCache");
     if (conf == null) {
       Map<String, String> args = new HashMap<>();
       args.put(NAME, "fieldValueCache");
@@ -241,24 +253,25 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
       conf = new CacheConfig(CaffeineCache.class, args, null);
     }
     fieldValueCacheConfig = conf;
-    useColdSearcher = getBool("query/useColdSearcher", false);
-    dataDir = get("dataDir", null);
+    useColdSearcher = __("query").__("useColdSearcher")._bool(false);
+    dataDir = __("dataDir").textValue();
     if (dataDir != null && dataDir.length() == 0) dataDir = null;
 
 
     org.apache.solr.search.SolrIndexSearcher.initRegenerators(this);
 
-    if (get("jmx", null) != null) {
+    if (root.child("jmx") != null) {
       log.warn("solrconfig.xml: <jmx> is no longer supported, use solr.xml:/metrics/reporter section instead");
     }
 
     httpCachingConfig = new HttpCachingConfig(this);
 
-    maxWarmingSearchers = getInt("query/maxWarmingSearchers", 1);
-    slowQueryThresholdMillis = getInt("query/slowQueryThresholdMillis", -1);
+    maxWarmingSearchers = __("query").__("maxWarmingSearchers")._int(1);
+    slowQueryThresholdMillis = __("query").__("slowQueryThresholdMillis")._int(-1);
     for (SolrPluginInfo plugin : plugins) loadPluginInfo(plugin);
 
-    Map<String, CacheConfig> userCacheConfigs = CacheConfig.getMultipleConfigs(this, "query/cache");
+    Map<String, CacheConfig> userCacheConfigs = CacheConfig.getMultipleConfigs(this, "query/cache",
+        __("query").children("cache"));
     List<PluginInfo> caches = getPluginInfos(SolrCache.class.getName());
     if (!caches.isEmpty()) {
       for (PluginInfo c : caches) {
@@ -269,25 +282,19 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
 
     updateHandlerInfo = loadUpdatehandlerInfo();
 
-    multipartUploadLimitKB = getInt(
-        "requestDispatcher/requestParsers/@multipartUploadLimitInKB", Integer.MAX_VALUE);
+    multipartUploadLimitKB = __("requestDispatcher").__("requestParsers").intAttr("multipartUploadLimitInKB", Integer.MAX_VALUE);
     if (multipartUploadLimitKB == -1) multipartUploadLimitKB = Integer.MAX_VALUE;
 
-    formUploadLimitKB = getInt(
-        "requestDispatcher/requestParsers/@formdataUploadLimitInKB", Integer.MAX_VALUE);
+    formUploadLimitKB = __("requestDispatcher").__("requestParsers").intAttr("formdataUploadLimitInKB", Integer.MAX_VALUE);
     if (formUploadLimitKB == -1) formUploadLimitKB = Integer.MAX_VALUE;
 
-    enableRemoteStreams = getBool(
-        "requestDispatcher/requestParsers/@enableRemoteStreaming", false);
+    enableRemoteStreams = __("requestDispatcher").__("requestParsers").boolAttr("enableRemoteStreaming", false);
 
-    enableStreamBody = getBool(
-        "requestDispatcher/requestParsers/@enableStreamBody", false);
+    enableStreamBody = __("requestDispatcher").__("requestParsers").boolAttr("enableStreamBody", false);
 
-    handleSelect = getBool(
-        "requestDispatcher/@handleSelect", false);
+    handleSelect =  __("requestDispatcher").boolAttr("handleSelect", false);
 
-    addHttpRequestToContext = getBool(
-        "requestDispatcher/requestParsers/@addHttpRequestToContext", false);
+    addHttpRequestToContext = __("requestDispatcher").__("requestParsers").boolAttr("addHttpRequestToContext", false);
 
     List<PluginInfo> argsInfos = getPluginInfos(InitParams.class.getName());
     if (argsInfos != null) {
@@ -428,15 +435,15 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
   }
 
   protected UpdateHandlerInfo loadUpdatehandlerInfo() {
-    return new UpdateHandlerInfo(get("updateHandler/@class", null),
-        getInt("updateHandler/autoCommit/maxDocs", -1),
-        getInt("updateHandler/autoCommit/maxTime", -1),
-        convertHeapOptionStyleConfigStringToBytes(get("updateHandler/autoCommit/maxSize", "")),
-        getBool("updateHandler/indexWriter/closeWaitsForMerges", true),
-        getBool("updateHandler/autoCommit/openSearcher", true),
-        getInt("updateHandler/autoSoftCommit/maxDocs", -1),
-        getInt("updateHandler/autoSoftCommit/maxTime", -1),
-        getBool("updateHandler/commitWithin/softCommit", true));
+    return new UpdateHandlerInfo( __("updateHandler").attr("class"),
+        __("updateHandler").__("autoCommit").__("maxDocs")._int( -1),
+        __("updateHandler").__("autoCommit").__("maxTime")._int( -1),
+        convertHeapOptionStyleConfigStringToBytes(__("updateHandler").__("autoCommit").__("maxSize").txt("")),
+       __("updateHandler").__("indexWriter").__("closeWaitsForMerges")._bool(true),
+        __("updateHandler").__("autoCommit").__("openSearcher")._bool(true),
+        __("updateHandler").__("autoSoftCommit").__("autoSoftCommit")._int(-1),
+        __("updateHandler").__("autoSoftCommit").__("maxTime")._int(-1),
+        __("updateHandler").__("commitWithin").__("maxTime")._bool(true));
   }
 
   /**
@@ -494,9 +501,9 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
 
   public List<PluginInfo> readPluginInfos(String tag, boolean requireName, boolean requireClass) {
     ArrayList<PluginInfo> result = new ArrayList<>();
-    NodeList nodes = (NodeList) evaluate(tag, XPathConstants.NODESET);
-    for (int i = 0; i < nodes.getLength(); i++) {
-      PluginInfo pluginInfo = new PluginInfo(nodes.item(i), "[solrconfig.xml] " + tag, requireName, requireClass);
+    List<ConfigNode> nodes = root.children(tag);
+    for (int i = 0; i < nodes.size(); i++) {
+      PluginInfo pluginInfo = new PluginInfo(nodes.get(i), "[solrconfig.xml] " + tag, requireName, requireClass);
       if (pluginInfo.isEnabled()) result.add(pluginInfo);
     }
     return result;
@@ -548,9 +555,9 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
     /**
      * config xpath prefix for getting HTTP Caching options
      */
-    private final static String CACHE_PRE
+   /* private final static String CACHE_PRE
         = "requestDispatcher/httpCaching/";
-
+*/
     /**
      * For extracting Expires "ttl" from <cacheControl> config
      */
@@ -589,15 +596,15 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
 
     private HttpCachingConfig(SolrConfig conf) {
 
-      never304 = conf.getBool(CACHE_PRE + "@never304", false);
+      //"requestDispatcher/httpCaching/";
+      never304 = conf.__("requestDispatcher").__("httpCaching").boolAttr("never304", false);
 
-      etagSeed = conf.get(CACHE_PRE + "@etagSeed", "Solr");
+      etagSeed = conf.__("requestDispatcher").__("httpCaching").attr("etagSeed", "Solr");
 
 
-      lastModFrom = LastModFrom.parse(conf.get(CACHE_PRE + "@lastModFrom",
-          "openTime"));
+      lastModFrom = LastModFrom.parse(conf.__("requestDispatcher").__("httpCaching").attr("lastModFrom","openTime"));
 
-      cacheControlHeader = conf.get(CACHE_PRE + "cacheControl", null);
+      cacheControlHeader = conf.__("requestDispatcher").__("httpCaching").__("cacheControl").textValue();
 
       Long tmp = null; // maxAge
       if (null != cacheControlHeader) {
@@ -763,8 +770,8 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
       }
     }
 
-    NodeList nodes = (NodeList) evaluate("lib", XPathConstants.NODESET);
-    if (nodes != null && nodes.getLength() > 0) {
+    List<ConfigNode> nodes = root.children("lib");
+    if (nodes != null && nodes.size() > 0) {
       if (!isConfigsetTrusted) {
         throw new SolrException(ErrorCode.UNAUTHORIZED,
           "The configset for this collection was uploaded without any authentication in place,"
@@ -772,14 +779,14 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
             + " after enabling authentication and authorization.");
       }
 
-      for (int i = 0; i < nodes.getLength(); i++) {
-        Node node = nodes.item(i);
-        String baseDir = DOMUtil.getAttr(node, "dir");
-        String path = DOMUtil.getAttr(node, PATH);
+      for (int i = 0; i < nodes.size(); i++) {
+        ConfigNode node = nodes.get(i);
+        String baseDir = node.attr("dir");
+        String path = node.attr(PATH);
         if (null != baseDir) {
           // :TODO: add support for a simpler 'glob' mutually exclusive of regex
           Path dir = instancePath.resolve(baseDir);
-          String regex = DOMUtil.getAttr(node, "regex");
+          String regex = node.attr("regex");
           try {
             if (regex == null)
               urls.addAll(SolrResourceLoader.getURLs(dir));
@@ -831,42 +838,44 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
     return enableStreamBody;
   }
 
-  @Override
-  public int getInt(String path) {
-    return getInt(path, 0);
+  private Object _getVal(String path) {
+    List<String> parts = StrUtils.split(path,'/');
+    Object val = overlay.getXPathProperty(parts);
+    if(val !=null) return val;
+    return root.child(parts);
   }
 
-  @Override
-  public int getInt(String path, int def) {
-    Object val = overlay.getXPathProperty(path);
-    if (val != null) return Integer.parseInt(val.toString());
-    return super.getInt(path, def);
+
+
+  static int getInt(Object v, int def) {
+    if (v instanceof Number) return ((Number) v).intValue();
+    return v == null ? def : Integer.parseInt(v.toString());
   }
 
-  @Override
-  public boolean getBool(String path, boolean def) {
-    Object val = overlay.getXPathProperty(path);
-    if (val != null) return Boolean.parseBoolean(val.toString());
-    return super.getBool(path, def);
+  static boolean getBool(Object v, boolean def) {
+    if (v instanceof Boolean) return (Boolean) v;
+    return v == null ? def : Boolean.parseBoolean(v.toString());
   }
 
-  @Override
+  public int _getInt(String path, int def) {
+    Object val = _getVal(path);
+    return val == null ? def : Integer.parseInt(val.toString());
+  }
   public String get(String path) {
-    Object val = overlay.getXPathProperty(path, true);
-    return val != null ? val.toString() : super.get(path);
+    Object val = _getVal(path);
+    return val != null ? val.toString() :null;
   }
 
-  @Override
   public String get(String path, String def) {
-    Object val = overlay.getXPathProperty(path, true);
-    return val != null ? val.toString() : super.get(path, def);
+    Object val = _getVal(path);
+    return val != null ? val.toString() : xml.get(path, def);
 
   }
 
   @Override
   @SuppressWarnings({"unchecked", "rawtypes"})
   public Map<String, Object> toMap(Map<String, Object> result) {
-    if (getZnodeVersion() > -1) result.put(ZNODEVER, getZnodeVersion());
+    if (xml.getZnodeVersion() > -1) result.put(ZNODEVER, xml.getZnodeVersion());
     if(luceneMatchVersion != null) result.put(IndexSchema.LUCENE_MATCH_VERSION_PARAM, luceneMatchVersion.toString());
     result.put("updateHandler", getUpdateHandlerInfo());
     Map m = new LinkedHashMap();
@@ -927,11 +936,10 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
 
   }
 
-  @Override
   public Properties getSubstituteProperties() {
     Map<String, Object> p = getOverlay().getUserProps();
-    if (p == null || p.isEmpty()) return super.getSubstituteProperties();
-    Properties result = new Properties(super.getSubstituteProperties());
+    if (p == null || p.isEmpty()) return xml.getSubstituteProperties();
+    Properties result = new Properties(xml.getSubstituteProperties());
     result.putAll(p);
     return result;
   }
@@ -940,7 +948,7 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
 
   public ConfigOverlay getOverlay() {
     if (overlay == null) {
-      overlay = getConfigOverlay(getResourceLoader());
+      overlay = getConfigOverlay(xml.getResourceLoader());
     }
     return overlay;
   }
@@ -967,13 +975,37 @@ public class SolrConfig extends XmlConfigFile implements MapSerializable {
     if (o == null || PackageLoader.LATEST.equals(o)) return null;
     return o.toString();
   }
+  ConfigNode getRoot() {
+    return root;
+  }
 
   public RequestParams refreshRequestParams() {
-    requestParams = RequestParams.getFreshRequestParams(getResourceLoader(), requestParams);
+    requestParams = RequestParams.getFreshRequestParams(xml.getResourceLoader(), requestParams);
     if (log.isDebugEnabled()) {
       log.debug("current version of requestparams : {}", requestParams.getZnodeVersion());
     }
     return requestParams;
   }
 
+  public SolrResourceLoader getResourceLoader() {
+    return xml.getResourceLoader();
+  }
+
+  public int getZnodeVersion() {
+    return xml.getZnodeVersion();
+  }
+
+  public String getName() {
+    return xml.getName();
+  }
+
+  public String getResourceName() {
+    return xml.getResourceName();
+  }
+
+  public ConfigNode __(String name) {
+    return root.__(name);
+  }
+
+
 }
diff --git a/solr/core/src/java/org/apache/solr/search/CacheConfig.java b/solr/core/src/java/org/apache/solr/search/CacheConfig.java
index 1520f80..1f83b56 100644
--- a/solr/core/src/java/org/apache/solr/search/CacheConfig.java
+++ b/solr/core/src/java/org/apache/solr/search/CacheConfig.java
@@ -23,13 +23,17 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiConsumer;
 import java.util.function.Supplier;
 
+import org.apache.solr.cluster.api.SimpleMap;
+import org.apache.solr.common.ConfigNode;
 import org.apache.solr.common.MapSerializable;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.util.DOMUtil;
 import org.apache.solr.common.util.StrUtils;
 
+import org.apache.solr.common.util.WrappedSimpleMap;
 import org.apache.solr.core.PluginInfo;
 
 import org.apache.solr.core.SolrConfig;
@@ -88,15 +92,13 @@ public class CacheConfig implements MapSerializable{
     this.regenerator = regenerator;
   }
 
-  public static Map<String, CacheConfig> getMultipleConfigs(SolrConfig solrConfig, String configPath) {
-    NodeList nodes = (NodeList) solrConfig.evaluate(configPath, XPathConstants.NODESET);
-    if (nodes == null || nodes.getLength() == 0) return new LinkedHashMap<>();
-    Map<String, CacheConfig> result = new HashMap<>(nodes.getLength());
-    for (int i = 0; i < nodes.getLength(); i++) {
-      Node node = nodes.item(i);
-      if ("true".equals(DOMUtil.getAttrOrDefault(node, "enabled", "true"))) {
-        CacheConfig config = getConfig(solrConfig, node.getNodeName(),
-                DOMUtil.toMap(node.getAttributes()), configPath);
+  public static Map<String, CacheConfig> getMultipleConfigs(SolrConfig solrConfig, String configPath, List<ConfigNode> nodes) {
+    if (nodes == null || nodes.size() == 0) return new LinkedHashMap<>();
+    Map<String, CacheConfig> result = new HashMap<>(nodes.size());
+    for (int i = 0; i < nodes.size(); i++) {
+      ConfigNode node = nodes.get(i);
+      if (node.boolAttr( "enabled", true)) {
+        CacheConfig config = getConfig(solrConfig, node.name(),node.attributes().asMap(), configPath);
         result.put(config.args.get(NAME), config);
       }
     }
@@ -105,15 +107,15 @@ public class CacheConfig implements MapSerializable{
 
 
   @SuppressWarnings({"unchecked"})
-  public static CacheConfig getConfig(SolrConfig solrConfig, String xpath) {
-    Node node = solrConfig.getNode(xpath, false);
-    if(node == null || !"true".equals(DOMUtil.getAttrOrDefault(node, "enabled", "true"))) {
+  public static CacheConfig getConfig(SolrConfig solrConfig, ConfigNode node, String xpath) {
+//    Node node = solrConfig.getNode(xpath, false);
+    if(node == null || !"true".equals(node.attributes().get( "enabled", "true"))) {
       Map<String, String> m = solrConfig.getOverlay().getEditableSubProperties(xpath);
       if(m==null) return null;
       List<String> parts = StrUtils.splitSmart(xpath, '/');
       return getConfig(solrConfig,parts.get(parts.size()-1) , Collections.EMPTY_MAP,xpath);
     }
-    return getConfig(solrConfig, node.getNodeName(),DOMUtil.toMap(node.getAttributes()), xpath);
+    return getConfig(solrConfig, node.name(),node.attributes().asMap(), xpath);
   }
 
 
diff --git a/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java b/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java
index a364757..cf24e33 100644
--- a/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java
+++ b/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java
@@ -31,6 +31,7 @@ import org.apache.lucene.index.MergePolicy;
 import org.apache.lucene.index.MergeScheduler;
 import org.apache.lucene.search.Sort;
 import org.apache.lucene.util.InfoStream;
+import org.apache.solr.common.ConfigNode;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.DirectoryFactory;
@@ -47,6 +48,7 @@ import org.apache.solr.schema.IndexSchema;
 import org.apache.solr.util.SolrPluginUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.w3c.dom.Node;
 
 import static org.apache.solr.core.XmlConfigFile.assertWarnOrFail;
 
@@ -128,29 +130,29 @@ public class SolrIndexConfig implements MapSerializable {
 
     // sanity check: this will throw an error for us if there is more then one
     // config section
-    Object unused = solrConfig.getNode(prefix, false);
+//    Object unused =  solrConfig.getNode(prefix, false);
 
     // Assert that end-of-life parameters or syntax is not in our config.
     // Warn for luceneMatchVersion's before LUCENE_3_6, fail fast above
     assertWarnOrFail("The <mergeScheduler>myclass</mergeScheduler> syntax is no longer supported in solrconfig.xml. Please use syntax <mergeScheduler class=\"myclass\"/> instead.",
-        !((solrConfig.getNode(prefix + "/mergeScheduler", false) != null) && (solrConfig.get(prefix + "/mergeScheduler/@class", null) == null)),
+        !(solrConfig.__(prefix).__("mergeScheduler") != null && (solrConfig.__(prefix).attr("class", null) == null)),
         true);
     assertWarnOrFail("Beginning with Solr 7.0, <mergePolicy>myclass</mergePolicy> is no longer supported, use <mergePolicyFactory> instead.",
-        !((solrConfig.getNode(prefix + "/mergePolicy", false) != null) && (solrConfig.get(prefix + "/mergePolicy/@class", null) == null)),
+        !((solrConfig.__(prefix).__("mergePolicy") != null) && (solrConfig.__(prefix).__("mergePolicy").attr("class", null) == null)),
         true);
     assertWarnOrFail("The <luceneAutoCommit>true|false</luceneAutoCommit> parameter is no longer valid in solrconfig.xml.",
         solrConfig.get(prefix + "/luceneAutoCommit", null) == null,
         true);
 
-    useCompoundFile = solrConfig.getBool(prefix+"/useCompoundFile", def.useCompoundFile);
-    maxBufferedDocs = solrConfig.getInt(prefix+"/maxBufferedDocs", def.maxBufferedDocs);
-    ramBufferSizeMB = solrConfig.getDouble(prefix+"/ramBufferSizeMB", def.ramBufferSizeMB);
-    maxCommitMergeWaitMillis = solrConfig.getInt(prefix+"/maxCommitMergeWaitTime", def.maxCommitMergeWaitMillis);
+    useCompoundFile = solrConfig.__(prefix).__("useCompoundFile")._bool(def.useCompoundFile);
+    maxBufferedDocs = solrConfig.__(prefix).__("maxBufferedDocs")._int(def.maxBufferedDocs);
+    ramBufferSizeMB = solrConfig.__(prefix).__("ramBufferSizeMB").doubleVal(def.ramBufferSizeMB);
+    maxCommitMergeWaitMillis = solrConfig.__(prefix).__("maxCommitMergeWaitTime")._int(def.maxCommitMergeWaitMillis);
 
     // how do we validate the value??
-    ramPerThreadHardLimitMB = solrConfig.getInt(prefix+"/ramPerThreadHardLimitMB", def.ramPerThreadHardLimitMB);
+    ramPerThreadHardLimitMB = solrConfig.__(prefix).__("ramPerThreadHardLimitMB")._int(def.ramPerThreadHardLimitMB);
 
-    writeLockTimeout=solrConfig.getInt(prefix+"/writeLockTimeout", def.writeLockTimeout);
+    writeLockTimeout= solrConfig.__(prefix).__("writeLockTimeout")._int(def.writeLockTimeout);
     lockType=solrConfig.get(prefix+"/lockType", def.lockType);
 
     List<PluginInfo> infos = solrConfig.readPluginInfos(prefix + "/metrics", false, false);
@@ -166,10 +168,10 @@ public class SolrIndexConfig implements MapSerializable {
         getPluginInfo(prefix + "/mergePolicy", solrConfig, null) == null,
         true);
     assertWarnOrFail("Beginning with Solr 7.0, <maxMergeDocs> is no longer supported, configure it on the relevant <mergePolicyFactory> instead.",
-        solrConfig.getInt(prefix+"/maxMergeDocs", 0) == 0,
+        solrConfig.__(prefix).__("maxMergeDocs")._int(0) == 0,
         true);
     assertWarnOrFail("Beginning with Solr 7.0, <mergeFactor> is no longer supported, configure it on the relevant <mergePolicyFactory> instead.",
-        solrConfig.getInt(prefix+"/mergeFactor", 0) == 0,
+        solrConfig.__(prefix).__("mergeFactor")._int(0) == 0,
         true);
 
     String val = solrConfig.get(prefix + "/termIndexInterval", null);
@@ -177,9 +179,9 @@ public class SolrIndexConfig implements MapSerializable {
       throw new IllegalArgumentException("Illegal parameter 'termIndexInterval'");
     }
 
-    boolean infoStreamEnabled = solrConfig.getBool(prefix + "/infoStream", false);
+    boolean infoStreamEnabled = solrConfig.__(prefix).__("infoStream")._bool(false);
     if(infoStreamEnabled) {
-      String infoStreamFile = solrConfig.get(prefix + "/infoStream/@file", null);
+      String infoStreamFile = solrConfig.__(prefix).__("infoStream").attr("file") ;
       if (infoStreamFile == null) {
         log.info("IndexWriter infoStream solr logging is enabled");
         infoStream = new LoggingInfoStream();
@@ -187,10 +189,11 @@ public class SolrIndexConfig implements MapSerializable {
         throw new IllegalArgumentException("Remove @file from <infoStream> to output messages to solr's logfile");
       }
     }
-    mergedSegmentWarmerInfo = getPluginInfo(prefix + "/mergedSegmentWarmer", solrConfig, def.mergedSegmentWarmerInfo);
+    ConfigNode warmerInfo = solrConfig.__(prefix).__("mergedSegmentWarmer");
+    mergedSegmentWarmerInfo = warmerInfo==null? def.mergedSegmentWarmerInfo : new PluginInfo(warmerInfo, "[solrconfig.xml] mergedSegmentWarmer" , false, false);
 
     assertWarnOrFail("Beginning with Solr 5.0, <checkIntegrityAtMerge> option is no longer supported and should be removed from solrconfig.xml (these integrity checks are now automatic)",
-        (null == solrConfig.getNode(prefix + "/checkIntegrityAtMerge", false)),
+        (null == solrConfig.__(prefix).__( "checkIntegrityAtMerge")),
         true);
   }
 
diff --git a/solr/core/src/test/org/apache/solr/core/TestCodecSupport.java b/solr/core/src/test/org/apache/solr/core/TestCodecSupport.java
index cd86d4f..6bc20b3 100644
--- a/solr/core/src/test/org/apache/solr/core/TestCodecSupport.java
+++ b/solr/core/src/test/org/apache/solr/core/TestCodecSupport.java
@@ -200,9 +200,9 @@ public class TestCodecSupport extends SolrTestCaseJ4 {
     SolrCore c = null;
     
     SolrConfig config = TestHarness.createConfig(testSolrHome, previousCoreName, "solrconfig_codec2.xml");
-    assertEquals("Unexpected codec factory for this test.", "solr.SchemaCodecFactory", config.get("codecFactory/@class"));
-    assertNull("Unexpected configuration of codec factory for this test. Expecting empty element", 
-        config.getNode("codecFactory", false).getFirstChild());
+    assertEquals("Unexpected codec factory for this test.", "solr.SchemaCodecFactory", config.__("codecFactory").attr("class"));
+    assertTrue("Unexpected configuration of codec factory for this test. Expecting empty element",
+        config.__("codecFactory").children(null, (String)null).isEmpty());
     IndexSchema schema = IndexSchemaFactory.buildIndexSchema("schema_codec.xml", config);
 
     CoreContainer coreContainer = h.getCoreContainer();
diff --git a/solr/core/src/test/org/apache/solr/core/TestConfig.java b/solr/core/src/test/org/apache/solr/core/TestConfig.java
index ccf3114..7a61fb7 100644
--- a/solr/core/src/test/org/apache/solr/core/TestConfig.java
+++ b/solr/core/src/test/org/apache/solr/core/TestConfig.java
@@ -21,12 +21,14 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.LinkedHashMap;
 import java.util.Collections;
+import java.util.List;
 
 import org.apache.lucene.index.ConcurrentMergeScheduler;
 import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.index.TieredMergePolicy;
 import org.apache.lucene.util.InfoStream;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.ConfigNode;
 import org.apache.solr.handler.admin.ShowFileRequestHandler;
 import org.apache.solr.schema.IndexSchema;
 import org.apache.solr.schema.IndexSchemaFactory;
@@ -80,24 +82,24 @@ public class TestConfig extends SolrTestCaseJ4 {
   public void testJavaProperty() {
     // property values defined in build.xml
 
-    String s = solrConfig.get("propTest");
+    String s = solrConfig.__("propTest").textValue();
     assertEquals("prefix-proptwo-suffix", s);
 
-    s = solrConfig.get("propTest/@attr1", "default");
+    s = solrConfig.__("propTest").attr("attr1", "default");
     assertEquals("propone-${literal}", s);
 
-    s = solrConfig.get("propTest/@attr2", "default");
+    s = solrConfig.__("propTest").attr("attr2", "default");
     assertEquals("default-from-config", s);
 
     s = solrConfig.get("propTest[@attr2='default-from-config']", "default");
     assertEquals("prefix-proptwo-suffix", s);
 
-    NodeList nl = (NodeList) solrConfig.evaluate("propTest", XPathConstants.NODESET);
-    assertEquals(1, nl.getLength());
-    assertEquals("prefix-proptwo-suffix", nl.item(0).getTextContent());
+    List<ConfigNode> nl = solrConfig.root.children("propTest");
+    assertEquals(1, nl.size());
+    assertEquals("prefix-proptwo-suffix", nl.get(0).textValue());
 
-    Node node = solrConfig.getNode("propTest", true);
-    assertEquals("prefix-proptwo-suffix", node.getTextContent());
+
+    assertEquals("prefix-proptwo-suffix", solrConfig.__("propTest"));
   }
 
   // sometime if the config referes to old things, it must be replaced with new stuff
diff --git a/solr/solrj/src/java/org/apache/solr/cluster/api/SimpleMap.java b/solr/solrj/src/java/org/apache/solr/cluster/api/SimpleMap.java
index 81da171..cd9a372 100644
--- a/solr/solrj/src/java/org/apache/solr/cluster/api/SimpleMap.java
+++ b/solr/solrj/src/java/org/apache/solr/cluster/api/SimpleMap.java
@@ -20,6 +20,8 @@ package org.apache.solr.cluster.api;
 import org.apache.solr.common.MapWriter;
 
 import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
@@ -82,4 +84,10 @@ public interface SimpleMap<T> extends MapWriter {
   default void writeMap(EntryWriter ew) throws IOException {
     forEachEntry(ew::putNoEx);
   }
+
+  default Map<String, T> asMap() {
+    Map<String, T> result = new LinkedHashMap<>();
+    forEachEntry((k, v) -> result.put(k, v));
+    return result;
+  }
 }
diff --git a/solr/solrj/src/java/org/apache/solr/common/ConfigNode.java b/solr/solrj/src/java/org/apache/solr/common/ConfigNode.java
index 1a67b52..17fd678 100644
--- a/solr/solrj/src/java/org/apache/solr/common/ConfigNode.java
+++ b/solr/solrj/src/java/org/apache/solr/common/ConfigNode.java
@@ -19,11 +19,15 @@ package org.apache.solr.common;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 
 import org.apache.solr.cluster.api.SimpleMap;
+import org.apache.solr.common.util.WrappedSimpleMap;
 
 /**
  * A generic interface that represents a config file, mostly XML
@@ -53,6 +57,46 @@ public interface ConfigNode {
     return child(null, name);
   }
 
+  /**
+   * Child by name or return an empty node if null
+   */
+  default ConfigNode __(String name) {
+    ConfigNode child = child(null, name);
+    return child == null? EMPTY: child;
+  }
+
+  default ConfigNode child(List<String> path) {
+    ConfigNode node = this;
+    for (String s : path) {
+      node = node.child(s);
+      if (node == null) break;
+    }
+    return node;
+  }
+
+  default ConfigNode child(String name, Supplier<RuntimeException> err) {
+    ConfigNode n = child(name);
+    if(n == null) throw err.get();
+    return n;
+  }
+
+  default boolean _bool(boolean def) { return __bool(textValue(),def); }
+  default int _int(int def) { return __int(textValue(), def); }
+  default String attr(String name, String def) { return __txt(attributes().get(name), def);}
+  default String attr(String name) { return attributes().get(name);}
+  default String requiredStrAttr(String name, Supplier<RuntimeException> err) {
+    if(attributes().get(name) == null && err != null) throw err.get();
+    return attributes().get(name);
+  }
+  default int intAttr(String name, int def) { return __int(attributes().get(name), def); }
+  default boolean boolAttr(String name, boolean def){ return __bool(attributes().get(name), def); }
+  default String txt(String def) { return textValue() == null ? def : textValue();}
+  default double doubleVal(double def){ return __double(textValue(), def); }
+  default boolean __bool(Object v, boolean def) { return v == null ? def : Boolean.parseBoolean(v.toString()); }
+  default String __txt(Object v, String def) { return v == null ? def : v.toString(); }
+  default int __int(Object v, int def) { return v==null? def: Integer.parseInt(v.toString()); }
+  default double __double(Object v, double def) { return v == null ? def: Double.parseDouble(v.toString()); }
+
   /**Iterate through child nodes with the name and return the first child that matches
    */
   default ConfigNode child(Predicate<ConfigNode> test, String name) {
@@ -100,5 +144,38 @@ public interface ConfigNode {
    */
   void forEachChild(Function<ConfigNode, Boolean> fun);
 
+  ConfigNode EMPTY = new ConfigNode() {
+    @Override
+    public String name() {
+      return null;
+    }
+
+    @Override
+    public String textValue() {
+      return null;
+    }
+
+    @Override
+    public SimpleMap<String> attributes() {
+      return empty_attrs;
+    }
+
+    @Override
+    public ConfigNode child(String name) {
+      return null;
+    }
+
+    @Override
+    public ConfigNode __(String name) {
+      return EMPTY;
+    }
+
+    @Override
+    public void forEachChild(Function<ConfigNode, Boolean> fun) {
+
+    }
+  } ;
+  SimpleMap<String> empty_attrs = new WrappedSimpleMap<>(Collections.emptyMap());
+
 
 }
diff --git a/solr/solrj/src/java/org/apache/solr/common/util/WrappedSimpleMap.java b/solr/solrj/src/java/org/apache/solr/common/util/WrappedSimpleMap.java
index e8f58a5..e8689d2 100644
--- a/solr/solrj/src/java/org/apache/solr/common/util/WrappedSimpleMap.java
+++ b/solr/solrj/src/java/org/apache/solr/common/util/WrappedSimpleMap.java
@@ -19,6 +19,7 @@ package org.apache.solr.common.util;
 
 import org.apache.solr.cluster.api.SimpleMap;
 
+import java.util.Collections;
 import java.util.Map;
 import java.util.function.BiConsumer;
 
@@ -46,4 +47,8 @@ public class WrappedSimpleMap<T>  implements SimpleMap<T> {
         this.delegate = delegate;
     }
 
+    @Override
+    public Map<String, T> asMap() {
+        return Collections.unmodifiableMap(delegate);
+    }
 }