[MNG-8655] Add a settings parser SPI - #13118
Conversation
Allow extensions to parse settings sources without replacing the settings builder. Preserve XML fallback and the shared interpolation, decryption, validation, and merging pipeline. Cover parser selection, diagnostics, native injection, and the existing bootstrap loading boundary with unit and CLI integration tests.
gnodet-bot
left a comment
There was a problem hiding this comment.
Overall this is a clean design — the SPI contract is well-specified, the retry semantics are preserved from the original code, and the test coverage is comprehensive. A few issues need addressing before merge.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| public SettingsParserException(String message) { | ||
| this(message, null); | ||
| } | ||
|
|
There was a problem hiding this comment.
SettingsParserException() and SettingsParserException(Throwable cause) both delegate to this(null, null) / this(null, cause), setting message = null. In DefaultSettingsBuilder.readSettings(), the warning and fatal problem messages include e.getMessage() directly — so a parser that throws new SettingsParserException(cause) (no message) will produce a problem like "Non-parseable settings settings.yaml: null", which is useless for diagnostics.
The @param on SettingsParserException(Throwable cause) should either:
- Require a non-null message (match the 2-arg constructor pattern), or
- At least document that message may be null, so callers of
getMessage()know to guard.
Prefer option 1 — make it impossible to create a message-less SettingsParserException from a cause:
| public SettingsParserException(String message, Throwable cause) { | |
| this(message != null ? message : (cause != null ? cause.getMessage() : "unknown error"), -1, -1, cause); | |
| } |
(And drop the no-arg constructor or at least give it a default message.)
| } catch (SettingsParserException e) { | ||
| settings = parser.parse(settingsSource, Map.of(SettingsParser.STRICT, false)); | ||
| problems.reportProblem(new DefaultBuilderProblem( | ||
| settingsSource.getLocation(), | ||
| e.getLineNumber(), | ||
| e.getColumnNumber(), | ||
| e, | ||
| e.getMessage(), | ||
| BuilderProblem.Severity.WARNING)); |
There was a problem hiding this comment.
When the strict parse throws SettingsParserException e, the inner catch retries with strict=false (line 174). If that retry also throws a SettingsParserException, it escapes the inner catch block and is caught by the outer catch on line 183 as a FATAL error. Two consequences:
- The WARNING problem for the original strict failure is never recorded (line 175-181 are never reached).
- The outer FATAL records the lenient-parse exception, losing the original strict one entirely — no cause chain linking them.
The old code had the same structure, so this is not a regression, but the new code is a good opportunity to fix it. The simplest fix is to record the strict-parse warning first, before retrying:
| } catch (SettingsParserException e) { | |
| settings = parser.parse(settingsSource, Map.of(SettingsParser.STRICT, false)); | |
| problems.reportProblem(new DefaultBuilderProblem( | |
| settingsSource.getLocation(), | |
| e.getLineNumber(), | |
| e.getColumnNumber(), | |
| e, | |
| e.getMessage(), | |
| BuilderProblem.Severity.WARNING)); | |
| } catch (SettingsParserException e) { | |
| problems.reportProblem(new DefaultBuilderProblem( | |
| settingsSource.getLocation(), | |
| e.getLineNumber(), | |
| e.getColumnNumber(), | |
| e, | |
| e.getMessage(), | |
| BuilderProblem.Severity.WARNING)); | |
| settings = parser.parse(settingsSource, Map.of(SettingsParser.STRICT, false)); |
With this ordering, if the lenient parse succeeds, the WARNING is recorded. If the lenient parse throws, the WARNING is still recorded before the FATAL, giving the user both data points.
…ngsParser.java Co-authored-by: Guillaume Nodet - AI Bot <gnodet+bot@gmail.com>
| * Option that can be specified in the options map. The value should be a {@code Boolean}; | ||
| * when {@code true} or absent, unknown input is rejected. | ||
| */ | ||
| String STRICT = "strict"; |
| } catch (SettingsParserException e) { | ||
| settings = parser.parse(settingsSource, Map.of(SettingsParser.STRICT, false)); | ||
| problems.reportProblem(new DefaultBuilderProblem( | ||
| settingsSource.getLocation(), | ||
| e.getLineNumber(), | ||
| e.getColumnNumber(), | ||
| e, | ||
| e.getMessage(), | ||
| BuilderProblem.Severity.WARNING)); |
| } catch (SettingsParserException e) { | ||
| problems.reportProblem(new DefaultBuilderProblem( | ||
| settingsSource.getLocation(), | ||
| loc != null ? loc.getLineNumber() : -1, | ||
| loc != null ? loc.getColumnNumber() : -1, | ||
| e.getLineNumber(), | ||
| e.getColumnNumber(), | ||
| e, | ||
| "Non-parseable settings " + settingsSource.getLocation() + ": " + e.getMessage(), | ||
| BuilderProblem.Severity.FATAL)); |
There was a problem hiding this comment.
test selectParser comment at 183-190
| public SettingsParserException(String message, Throwable cause) { | ||
| this(message, -1, -1, cause); | ||
| } | ||
|
|
||
| public SettingsParserException(String message, int lineNumber, int columnNumber, Throwable cause) { | ||
| super(message, cause); | ||
| this.lineNumber = lineNumber; | ||
| this.columnNumber = columnNumber; | ||
| } | ||
|
|
||
| public SettingsParserException(Throwable cause) { | ||
| this(null, cause); | ||
| } |
gnodet-bot
left a comment
There was a problem hiding this comment.
Three findings from the previous review are still unaddressed, plus one new issue. None are catastrophic but together they leave diagnostic gaps that will hurt real users.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| * Option that can be specified in the options map. The value should be a {@code Boolean}; | ||
| * when {@code true} or absent, unknown input is rejected. | ||
| */ | ||
| String STRICT = "strict"; |
There was a problem hiding this comment.
STRICT
The field still has two consecutive Javadoc comments — lines 48–50 (Boolean parsing option...) and lines 51–54 (Option that can be specified...). Only the second block is shown by IDE tooling; the first is silently discarded by the Javadoc compiler. Drop the first block:
| String STRICT = "strict"; | |
| /** | |
| * Option that can be specified in the options map. The value should be a {@code Boolean}; | |
| * when {@code true} or absent, unknown input is rejected. | |
| */ | |
| String STRICT = "strict"; |
There was a problem hiding this comment.
Fixed. Removed the duplicate block. STRICT now has a single Javadoc explaining the Boolean option and default strict behavior.
| public SettingsParserException(String message, Throwable cause) { | ||
| this(message, -1, -1, cause); | ||
| } | ||
|
|
||
| public SettingsParserException(String message, int lineNumber, int columnNumber, Throwable cause) { | ||
| super(message, cause); | ||
| this.lineNumber = lineNumber; | ||
| this.columnNumber = columnNumber; | ||
| } | ||
|
|
||
| public SettingsParserException(Throwable cause) { | ||
| this(null, cause); | ||
| } |
There was a problem hiding this comment.
SettingsParserException() (line 42) and SettingsParserException(Throwable cause) (line 60) both set message to null. In DefaultSettingsBuilder.readSettings(), e.getMessage() is used directly in the warning (line 180) and fatal (line 189) problem messages — so a parser that throws new SettingsParserException(cause) produces:
Non-parseable settings settings.yaml: null
That is useless for diagnostics. Fall back to the cause's message:
| public SettingsParserException(String message, Throwable cause) { | |
| this(message, -1, -1, cause); | |
| } | |
| public SettingsParserException(String message, int lineNumber, int columnNumber, Throwable cause) { | |
| super(message, cause); | |
| this.lineNumber = lineNumber; | |
| this.columnNumber = columnNumber; | |
| } | |
| public SettingsParserException(Throwable cause) { | |
| this(null, cause); | |
| } | |
| public SettingsParserException(String message, Throwable cause) { | |
| this(message != null ? message : (cause != null ? cause.getMessage() : "unknown error"), -1, -1, cause); | |
| } | |
| public SettingsParserException(Throwable cause) { | |
| this(cause != null ? cause.getMessage() : "unknown error", cause); | |
| } |
There was a problem hiding this comment.
Fixed. Message fallback now lives in the full constructor, covering every overload. Explicit messages are preserved; otherwise the cause's message is used, with a default when that is also null. Added constructor and builder-diagnostic tests, including preservation of causes and line/column information.
| } catch (SettingsParserException e) { | ||
| settings = parser.parse(settingsSource, Map.of(SettingsParser.STRICT, false)); | ||
| problems.reportProblem(new DefaultBuilderProblem( | ||
| settingsSource.getLocation(), | ||
| e.getLineNumber(), | ||
| e.getColumnNumber(), | ||
| e, | ||
| e.getMessage(), | ||
| BuilderProblem.Severity.WARNING)); |
There was a problem hiding this comment.
Current flow: strict parse throws SettingsParserException e (line 173) → lenient retry is attempted (line 174) → if the retry also throws, it escapes the inner catch, hits the outer catch (SettingsParserException) at line 183 as a FATAL, and reportProblem(WARNING, e) on lines 175–181 is never reached. The user gets a FATAL with the retry's exception, losing the original strict-parse error and all its location info.
Record the WARNING before retrying so it survives either outcome:
| } catch (SettingsParserException e) { | |
| settings = parser.parse(settingsSource, Map.of(SettingsParser.STRICT, false)); | |
| problems.reportProblem(new DefaultBuilderProblem( | |
| settingsSource.getLocation(), | |
| e.getLineNumber(), | |
| e.getColumnNumber(), | |
| e, | |
| e.getMessage(), | |
| BuilderProblem.Severity.WARNING)); | |
| } catch (SettingsParserException e) { | |
| problems.reportProblem(new DefaultBuilderProblem( | |
| settingsSource.getLocation(), | |
| e.getLineNumber(), | |
| e.getColumnNumber(), | |
| e, | |
| e.getMessage(), | |
| BuilderProblem.Severity.WARNING)); | |
| settings = parser.parse(settingsSource, Map.of(SettingsParser.STRICT, false)); |
There was a problem hiding this comment.
Fixed. The strict warning is now recorded before the lenient retry. Added regressions for retries ending in either a parse error or IOException, verifying that both the original warning and subsequent fatal problem are retained. Updated the SPI documentation accordingly.
| } catch (SettingsParserException e) { | ||
| problems.reportProblem(new DefaultBuilderProblem( | ||
| settingsSource.getLocation(), | ||
| loc != null ? loc.getLineNumber() : -1, | ||
| loc != null ? loc.getColumnNumber() : -1, | ||
| e.getLineNumber(), | ||
| e.getColumnNumber(), | ||
| e, | ||
| "Non-parseable settings " + settingsSource.getLocation() + ": " + e.getMessage(), | ||
| BuilderProblem.Severity.FATAL)); |
There was a problem hiding this comment.
selectParser conflict error misformatted as "Non-parseable settings"
selectParser() is called at line 170, inside the outer try. When multiple parsers match, it throws SettingsParserException("Multiple settings parsers support this source: ..."), which propagates here and produces:
Non-parseable settings settings.properties: Multiple settings parsers support this source: first, second
That message is wrong — the source was never parsed; the error is a configuration conflict between registered parsers. A user seeing "Non-parseable settings" will look for a syntax error in their file, not a duplicated extension registration.
The test at conflictingParsersAreReportedBeforeParsing only asserts contains("Multiple settings parsers...") so it misses this misleading prefix.
Cleanest fix: have selectParser throw a RuntimeException or dedicated non-SettingsParserException type that bypasses this catch entirely. Alternatively, move the selectParser() call outside of the try block.
There was a problem hiding this comment.
Fixed. Parser conflicts now report a fatal builder problem directly, without the misleading “Non-parseable settings” prefix. This preserves the existing SettingsBuilderException handling. Strengthened the tests to check the complete diagnostic, source location, sorted provider names, and that neither parser runs.
Retain strict parse warnings when lenient parsing fails, provide fallback exception messages, and report parser conflicts as structured builder problems. Remove duplicate STRICT documentation and cover the diagnostic paths with regression tests.
gnodet-bot
left a comment
There was a problem hiding this comment.
All four findings from the previous review are addressed:
- ✅ Duplicate Javadoc on
STRICT— single clean block, aligned withModelParser. - ✅ Null-message constructors —
messageOrDefault()guarantees a non-null message;SettingsParserExceptionTestcovers all constructor paths. - ✅ WARNING lost when lenient retry fails —
reportProblem(WARNING, e)now fires before the lenientparse()call. If that retry throws, the WARNING is already recorded.strictWarningSurvivesFailedLenientParsingandstrictWarningSurvivesUnreadableLenientInputpin this. - ✅
selectParserconflict misformatted as "Non-parseable settings" — conflict is now reported as a structured FATAL viareportProblemandselectParserreturnsnull, bypassing the outercatchentirely.conflictingParsersAreReportedBeforeParsingcovers it.
One new concern.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| @Nullable | ||
| private SettingsParser selectParser(Source source, ProblemCollector<BuilderProblem> problems) { | ||
| List<Map.Entry<String, SettingsParser>> matches = settingsParsers.entrySet().stream() | ||
| .filter(entry -> entry.getValue().supports(source)) |
There was a problem hiding this comment.
💡 supports() RuntimeException leaks as a raw stack trace
If a custom SettingsParser.supports() throws a RuntimeException (e.g. an NPE from accessing source.getLocation() before the source is ready), it propagates unchecked out of selectParser, exits readSettings through neither the SettingsParserException nor IOException branches, and surfaces to the caller as a raw exception — not a structured FATAL problem.
For a user with a buggy extension, this means a raw stack trace instead of a Maven diagnostic message. ModelParser has the same gap, so this is consistent with the existing SPI contract, but since SettingsParser.supports() is being introduced fresh, it's worth documenting or guarding:
| .filter(entry -> entry.getValue().supports(source)) | |
| .filter(entry -> { | |
| try { | |
| return entry.getValue().supports(source); | |
| } catch (RuntimeException e) { | |
| problems.reportProblem(new DefaultBuilderProblem( | |
| source.getLocation(), -1, -1, e, | |
| "Settings parser '" + entry.getKey() + "' failed during supports() check: " + e.getMessage(), | |
| BuilderProblem.Severity.FATAL)); | |
| return false; | |
| } | |
| }) |
Alternatively, document explicitly in SettingsParser.supports() Javadoc that the method must not throw — as-is, the contract is silent on this.
There was a problem hiding this comment.
Fixed. Runtime exceptions from supports() now produce a fatal settings-builder problem identifying the provider and source, with the original exception retained. Selection stops immediately for that source, preventing further provider checks, parsing, or XML fallback. Added named and unnamed provider regressions, including failure after an earlier match, and documented the behavior. Full mvn -Prun-its verify passed on Java 17.
Convert runtime failures from supports() to fatal settings problems and stop selection for that source. Retain provider identification, source location and the original cause, with regression coverage for named and unnamed providers.
gnodet-bot
left a comment
There was a problem hiding this comment.
All previous findings are addressed, and the new commit correctly resolves the last open concern.
- ✅
supports()RuntimeException guard —selectParsernow catchesRuntimeException, records a FATALDefaultBuilderProblem(with provider name, source location, and the original cause), and returnsnullimmediately.readSettingsthen returnsSettings.newInstance(), andbuild()converts the FATAL to aSettingsBuilderExceptionviahasErrorProblems(). End-to-end flow is correct. - ✅ Javadoc on
supports()— the added sentence accurately describes the runtime behavior: "A runtime exception from this check is reported as a fatal selection error; Maven does not try another parser or the XML fallback for that source." - ✅ Test coverage — four test methods cover named/unnamed × broken-first/matching-first.
verifyNoInteractions(unvisited, xmlFactory)confirms the loop stops early AND the XML fallback is bypassed.verify(source, never()).openStream()confirms no stream is consumed.
One nit in the shared test helper:
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| assertSame(failure, fatal.getException()); | ||
| assertEquals(0, error.getProblemCollector().problemsReportedFor(BuilderProblem.Severity.WARNING)); | ||
| verify(broken).supports(source); | ||
| verify(matching, never()).parse(any(), any()); |
There was a problem hiding this comment.
💡 Nit: vacuous assertion when matchingFirst=false
When matchingFirst is false, matching is never added to the parsers map, so verify(matching, never()).parse(any(), any()) passes trivially — Mockito never saw an interaction to record. The assertion says nothing in that branch.
If the intent is to prove that stopping on the broken provider also prevents any previously-seen match from being parsed, the meaningful assertion is only valid when matchingFirst=true. For the false case, a more useful assertion would be that the broken provider's supports() was actually called:
| verify(matching, never()).parse(any(), any()); | |
| verify(broken).supports(source); | |
| if (matchingFirst) { | |
| verify(matching, never()).parse(any(), any()); | |
| } | |
| verify(broken, never()).parse(any(), any()); |
(Or just leave the verify(matching, never()).parse(...) call guarded by if (matchingFirst) — it is harmless as-is but misleading.)
There was a problem hiding this comment.
Fixed. The assertion for the previously matched parser is now guarded by matchingFirst. The broken provider's supports() invocation is still verified in every case.
Prevent ambient user core extensions from triggering bootstrap settings reads in classpath parser tests. Keep the declared-extension failure scenario isolated as well, and guard the matching-parser assertion when that parser is registered.
|
@gnodet Fixed. The settings parser ITs now use an isolated user home, so the Mimir core extension installed by CI cannot trigger an unintended bootstrap settings read in the classpath-parser scenario. The negative test still declares its project extension explicitly. |
gnodet-bot
left a comment
There was a problem hiding this comment.
The nit from the previous review is correctly fixed:
- ✅
matchingFirstguard —verify(matching, never()).parse(any(), any())is now insideif (matchingFirst).verify(broken).supports(source)runs unconditionally in both branches. No vacuous assertion. - ✅ IT isolation — All three tests (
extensionClassPathParserAndXmlFallback,declaredParserCannotReadItsOwnBootstrapSettings,coreRealmParserReadsBootstrapSettings) now set an isolated user home viasetUserHomeDirectory. The second verifier inextensionClassPathParserAndXmlFallbackreuses the samedirectory.resolve("home")path already created by the first call — intentional and correct.
The 2026-09-16 commit touches only test files (7 lines). No production code was changed. Nothing else to raise.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| @Nullable | ||
| private SettingsParser selectParser(Source source, ProblemCollector<BuilderProblem> problems) { | ||
| List<Map.Entry<String, SettingsParser>> matches = new ArrayList<>(); | ||
| for (Map.Entry<String, SettingsParser> entry : settingsParsers.entrySet()) { | ||
| boolean supported; | ||
| try { | ||
| supported = entry.getValue().supports(source); | ||
| } catch (RuntimeException e) { | ||
| problems.reportProblem(new DefaultBuilderProblem( | ||
| source.getLocation(), | ||
| -1, | ||
| -1, | ||
| e, | ||
| "Settings parser '" + (entry.getKey() != null ? entry.getKey() : "<unnamed>") | ||
| + "' failed to determine support for this source", | ||
| BuilderProblem.Severity.FATAL)); | ||
| return null; | ||
| } | ||
| if (supported) { | ||
| matches.add(entry); | ||
| } | ||
| } | ||
| if (matches.size() > 1) { | ||
| problems.reportProblem(new DefaultBuilderProblem( | ||
| source.getLocation(), | ||
| -1, | ||
| -1, | ||
| null, | ||
| "Multiple settings parsers support this source: " | ||
| + String.join( | ||
| ", ", | ||
| matches.stream() | ||
| .map(entry -> entry.getKey() != null ? entry.getKey() : "<unnamed>") | ||
| .sorted() | ||
| .toList()), | ||
| BuilderProblem.Severity.FATAL)); | ||
| return null; | ||
| } | ||
| return matches.isEmpty() ? xmlSettingsParser : matches.get(0).getValue(); | ||
| } |
There was a problem hiding this comment.
selectParser can be made more compact by:
- Extracting the repeated
entry.getKey() != null ? entry.getKey() : "<unnamed>"into a smallparserNamehelper - Building the message in a local variable before
reportProblem, so the 6-arg constructor fits on one line - Moving
matches.add(entry)inside thetry, eliminating theboolean supportedtemp var
Spotless-verified (compiled and reformatted cleanly).
| @Nullable | |
| private SettingsParser selectParser(Source source, ProblemCollector<BuilderProblem> problems) { | |
| List<Map.Entry<String, SettingsParser>> matches = new ArrayList<>(); | |
| for (Map.Entry<String, SettingsParser> entry : settingsParsers.entrySet()) { | |
| boolean supported; | |
| try { | |
| supported = entry.getValue().supports(source); | |
| } catch (RuntimeException e) { | |
| problems.reportProblem(new DefaultBuilderProblem( | |
| source.getLocation(), | |
| -1, | |
| -1, | |
| e, | |
| "Settings parser '" + (entry.getKey() != null ? entry.getKey() : "<unnamed>") | |
| + "' failed to determine support for this source", | |
| BuilderProblem.Severity.FATAL)); | |
| return null; | |
| } | |
| if (supported) { | |
| matches.add(entry); | |
| } | |
| } | |
| if (matches.size() > 1) { | |
| problems.reportProblem(new DefaultBuilderProblem( | |
| source.getLocation(), | |
| -1, | |
| -1, | |
| null, | |
| "Multiple settings parsers support this source: " | |
| + String.join( | |
| ", ", | |
| matches.stream() | |
| .map(entry -> entry.getKey() != null ? entry.getKey() : "<unnamed>") | |
| .sorted() | |
| .toList()), | |
| BuilderProblem.Severity.FATAL)); | |
| return null; | |
| } | |
| return matches.isEmpty() ? xmlSettingsParser : matches.get(0).getValue(); | |
| } | |
| @Nullable | |
| private SettingsParser selectParser(Source source, ProblemCollector<BuilderProblem> problems) { | |
| List<Map.Entry<String, SettingsParser>> matches = new ArrayList<>(); | |
| for (Map.Entry<String, SettingsParser> entry : settingsParsers.entrySet()) { | |
| try { | |
| if (entry.getValue().supports(source)) { | |
| matches.add(entry); | |
| } | |
| } catch (RuntimeException e) { | |
| String msg = "Settings parser '" + parserName(entry.getKey()) | |
| + "' failed to determine support for this source"; | |
| problems.reportProblem( | |
| new DefaultBuilderProblem(source.getLocation(), -1, -1, e, msg, BuilderProblem.Severity.FATAL)); | |
| return null; | |
| } | |
| } | |
| if (matches.size() > 1) { | |
| String parsers = | |
| matches.stream().map(e -> parserName(e.getKey())).sorted().collect(Collectors.joining(", ")); | |
| String msg = "Multiple settings parsers support this source: " + parsers; | |
| problems.reportProblem( | |
| new DefaultBuilderProblem(source.getLocation(), -1, -1, null, msg, BuilderProblem.Severity.FATAL)); | |
| return null; | |
| } | |
| return matches.isEmpty() ? xmlSettingsParser : matches.get(0).getValue(); | |
| } | |
| private static String parserName(@Nullable String key) { | |
| return key != null ? key : "<unnamed>"; | |
| } |
Also needs import java.util.stream.Collectors; added to the imports.
gnodet-bot
left a comment
There was a problem hiding this comment.
The selectParser refactor from the previous review is correctly applied:
- ✅
parserName()helper — extracts the repeatedkey != null ? key : "<unnamed>"into a reusable private method. Used in both theRuntimeExceptioncatch and the multi-match conflict path. - ✅
matches.add()moved insidetry— eliminates theboolean supportedtemp variable. Cleaner control flow with no behavioral change. - ✅
Collectors.joining(,)replacesString.join(…, .toList())— more idiomatic streaming; produces identical output. - ✅
import java.util.stream.Collectors— added as requested. The previously presentimport java.util.stream.Collectorsin gnodet's suggestion block is present in the commit.
Single-file change, purely cosmetic/readability. No production behaviour changed, no test changes needed.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
Add a
SettingsParserSPI so extensions can read settings in an alternative syntax (e.g. YAML, properties) without replacingDefaultSettingsBuilder.Each
Sourceselects one parser viasupports(). Strict parse failures are retried leniently with the strict warning preserved. Multiple matching parsers produce a structured fatalBuilderProblem.RuntimeExceptionfromsupports()is caught, recorded as a fatal problem with provider name and source location, and selection stops for that source. XML fallback throughXmlSettingsParserremains the default.Interpolation, decryption, validation, project-settings restrictions, and merging remain in
DefaultSettingsBuilderunchanged. The SPI accepts file-backed and in-memory sources.Settings discovery and extension loading are unchanged. A parser must already be available to the container reading settings; an extension cannot parse the bootstrap settings needed to resolve itself.
New files:
api/maven-api-spi:SettingsParserSPI interface,SettingsParserExceptionimpl/maven-impl:XmlSettingsParser(wraps existing XML logic), updatedDefaultSettingsBuilderDefaultSettingsParserTest(parser selection, diagnostics, fallback),SettingsParserExceptionTestMavenITmng8655SettingsParserTest— extension-classpath parsing, XML fallback, bootstrap limitation, core-realm parserCloses #10436.