Repository navigation
[#159] Refuse a duplicate name and return the new uid in an XML update - #197
maximthomas merged 5 commits into
Conversation
… to a name another entry has update() now checks a new __NAME__ against the entry index in its check phase, before the entry changes, and refuses it with AlreadyExistsException when another entry of the same class has it. An entry that already has the name may send it again, even when another entry has it too.
…mes an entry whose uid is its name For an object class without a __UID__ element, create() makes the uid the name, but update() returned the uid it was given after a rename, so the caller kept a uid that no longer finds the entry. update() now returns the new name as the uid in that case.
…__ on more than one entry create() refuses a blank name, so a blank name identifies no entry. The new uniqueness check treated a cleared optional __NAME__ as taken once another entry had cleared its name too; it now skips blank values.
…f a class without __UID__
Without a __UID__ element the uid is the name, so a cleared name leaves an entry that no search of the class can read back. With an optional __NAME__ the required-name blank check is skipped, and update failed on new Uid("") with the framework's "Uid value must not be blank!"; it now refuses the blank name first with "Parameter '__NAME__' must not be blank.", as for a required name.
vharseko
left a comment
There was a problem hiding this comment.
Checked at 9e7d4dc. The duplicate check and the returned uid work as described: a refused rename leaves the entry and the file alone, an entry may re-send its own name, and a class without __UID__ gets its new uid back. One point, about the blank-name branch.
A cleared __NAME__ in a class with __UID__ leaves an entry that can no longer be read (XMLHandlerImpl.java:757-761, clearingAnOptionalNameOfTwoEntriesIsAllowed)
The description refuses a blank name in a class without __UID__ because "a search of the whole class then fails". The same happens in a class with __UID__, only with a different message. I ran it on this branch with the optional-name XSD from clearingAnOptionalNameOfTwoEntriesIsAllowed, accounts alice and bob:
| step | result |
|---|---|
update(ACCOUNT, uid-alice, {__NAME__=""}), then the same for uid-bob |
both accepted, as the test asserts |
search of the whole __ACCOUNT__ class |
IllegalArgumentException: The Attribute set must contain a 'Name'. |
search by __UID__ = uid-alice |
the same |
update(ACCOUNT, uid-carol, {__NAME__=" "}), then a search |
uid-carol| : readable until the save |
dispose(), a new handler on the file, a search |
The Attribute set must contain a 'Name'. |
setTextContent("") leaves the __NAME__ element without a text node (normalizeText does the same to " " on save or load). ConnectorObjectCreator.addAllAttributesToBuilder skips such an element, so the object has no Name, and ConnectorObject's constructor refuses it. One cleared entry is enough to make every search of the class fail, reconciliation included. getObject on that entry fails too, so it cannot even be read in order to repair it.
This is not new: on master, update accepted a blank optional __NAME__ in any class. But this PR now decides that case on purpose, and the test pins it as allowed. Two things suggest refusing it in every class, as the PR already does without __UID__:
createnever makes a nameless entry. It requires__NAME__whatever the XSD says (__NAME__ must be defined.), andnew Uid(name)refuses a blank one.- The framework requires a
Nameon every object.
Doing so would make the blank branch in nameTakenByAnother unnecessary, and clearingAnOptionalNameOfTwoEntriesIsAllowed would turn into a refusal test. Its current form only checks that the two updates do not throw, which is why it stays green. If clearing has to stay, the read side has to accept a nameless entry instead, and that is a larger change than this PR.
(The fixture's u10, with __NAME__ = " ", is equally unreadable: a search by __UID__ = u10 fails the same way. That is a hand-written file and outside this PR; I mention it only because it shows the read side does not tolerate such entries.)
…f any class create() requires __NAME__ whatever the XSD says, and the framework builds no ConnectorObject without a Name. With an optional __NAME__ in a class with __UID__, update accepted "" and left an entry that made every search of its class fail with "The Attribute set must contain a 'Name'.", the search by the entry's own __UID__ included; " " did the same once the file was saved and loaded again. update now checks __NAME__ as a required attribute in every class, so a blank one is refused with "Parameter '__NAME__' must not be blank." before the entry changes. The blank branch in nameTakenByAnother and the separate check for a class without __UID__ are no longer needed.
|
Fixed in 36def07:
The earlier mutants were re-run on the new code and are still killed. |
vharseko
left a comment
There was a problem hiding this comment.
Checked at 36def07. The blank-name point is fixed: update now checks __NAME__ as required in every class (XMLHandlerImpl.java:328), so "" and " " are refused with Parameter '__NAME__' must not be blank. before the entry changes, and a search of the class still returns both accounts.
Two mutants on this head: checking __NAME__ only when the XSD requires it fails clearingAnOptionalNameIsRefused and clearingTheOptionalNameOfAnEntryWithoutUidIsRefused; running the duplicate check before the blank check fails step 3 of renameIntoANameAnotherEntryHasIsRefused. mvn -pl OpenICF-xml-connector -am verify on JDK 21: 248 unit tests and 1 IT, green.
Nothing further from me.
…et evicted scripts unload (#196) Fixes #165. `runScriptOnConnector` no longer compiles a new class into the script engine's loader on every call. Each distinct script text is compiled once per engine and kept in a bounded LRU cache. Classes that leave the cache, and every cached class on `release()`, are unregistered so that they can be unloaded. With the default size an engine keeps at most 64 runScriptOnConnector scripts, each with its closures and declared classes, however many calls it serves. - **One class per text, in its own loader.** `ScriptedConfiguration.getScriptOnConnectorClass` compiles a text with a `ScriptOnConnectorLoader`, a child of the engine's loader, which keeps the new classes out of the engine's class cache. Before, every call went through `getGroovyScriptEngine().getGroovyClassLoader().parseClass(...)`. Each class then stayed in the engine loader, and in `GroovyScriptEngine`'s script cache, until `release()`. - **Script-root classes are the engine's.** A class whose source is in the script roots, such as `Helper`, resolves through `GroovyScriptEngine.loadScriptByName`, as it does in the engine's own compilations. A text therefore uses the `Helper` that the operation scripts use. The engine compiles it when it is first needed or after its source changes, and a script-root class still shadows a class of the connector's class loader. Other classes resolve through the engine's loader. - **A cached text runs against the classes it was compiled against.** Its code links a class by name the first time each reference runs. `ScriptOnConnectorLoader` keeps every class the compiler resolved for the text, and the loaders of the script-root classes among them. Its `loadClass` returns a resolved class, or else a class that one of those loaders defined: a nested class, a closure or another class compiled from the same source. Without that, code that first ran after an operation script had recompiled an edited `Helper` linked the new classes. `@CompileStatic` code then failed with `NoSuchMethodError` where it was compiled against an old signature, and with a `LinkageError` (loader constraint violation) where it reached a class such as `Helper.Greeting` only through the return type of a `Helper` method. - **The engine's configuration and compiler lock.** A text compiles with the engine's compiler configuration, the one `getRoots(CompilerConfiguration, GroovyClassLoader)` receives, so with the default imports and with whatever a subclass adds there, a `SecureASTCustomizer` for example. It compiles under the lock that the engine's loader holds while it compiles an operation script or a script-root class, as texts compiled on master: the monitor of the loader's `GroovyClassLoader.sourceCache`. The engine therefore cannot recompile a script-root class while a text resolves the classes it uses, and the customizers of the configuration are not used by two compilations at once. The compiler resolves the class nodes of an `ImportCustomizer` in place. `sourceCache` is protected and the loader is a private class of `GroovyScriptEngine`, so the field is read by reflection; Groovy 2.2.1, 2.4.21, 4.0.22 and 5.0.3 have it and lock it in `GroovyScriptEngine`'s loader. `getImportCustomizer(ImportCustomizer)` and `getImports()` are called once per engine, as on master. - **One compilation at a time, no lock for a cached text.** The configuration monitor is held only to read the engine and the cache, and the cache's monitor only around map operations. Texts compile one at a time, and not while the engine compiles, as on master. A cached text takes no lock and does not wait for a compilation. If two threads compile the same new text, the first one cached wins and the other's classes are unregistered. - **What a text adds or compiles through its loader goes to the engine.** A text's class loader delegates `addURL`, `addClasspath` and `parseClass` to the loader that compiled the text, now its `ScriptOnConnectorLoader`. That loader passes them on to the engine's loader, as on master. The operation scripts and the other texts load classes from a URL that a text adds, and see a class that a text compiles at run time. That compilation takes the engine's compiler lock, and the class resolves `Helper` as the operation scripts do. The text itself compiles through a separate method, into its own loader. - **Bounded, least recently used first.** An engine keeps 64 scripts by default. The system property `org.identityconnectors.framework.groovy.scriptOnConnectorCacheSize` changes the size. It is read when an engine is created. A value that is not a positive integer logs a WARN and falls back to 64. - **Evicted and released classes can unload.** Eviction and `release()` call `InvokerHelper.removeClass` on every class of the script's child loader, closures and declared classes included, outside the cache monitor. The cache is created with the engine once its customizer has succeeded. `release()` clears it last in its `finally`. - **Run from Java.** `ScriptedConnectorBase.runScriptOnConnector` now calls `evaluateScriptOnConnector`, which runs `InvokerHelper.createScript(...).run()` in Java. A Groovy call site caches the metaclass of the last script it ran, and so keeps that class alive. The arguments, the `log` binding and the `catch` blocks are unchanged. - **Without a cache, as on master.** While the customizer runs, the engine is not published and there is no cache. A subclass that overrides `getGroovyScriptEngine()` without calling super has no cache either. A text then compiles through the engine's loader, with the engine's configuration, as on master, and is not cached. The configuration of an engine that a subclass builds, a `SecureASTCustomizer` for example, therefore still applies to its texts. If `GroovyClassLoader.sourceCache` cannot be read, there is no cache either, and a WARN is logged when `ScriptedConfiguration` is loaded. - **A broken script-root class still fails as a compilation error.** The engine compiles it, and its `CompilationFailedException` reaches the caller, as on master. A `ScriptException` from `loadScriptByName`, which a `GroovyScriptEngine` subclass may throw, also fails the text's compilation with a `CompilationFailedException`, not a `GroovyBugError`. Behaviour changes: - a script's class name is stable while its text stays cached; - static fields, metaclass changes and classes declared in a script survive between calls with the same text; - classes declared in one text are no longer visible to other texts or to the operation scripts, and a text can no longer replace a script-root class for everyone else; - a text's class loader answers `getURLs`, `findResource`, `findResources`, `getResourceLoader`, `setResourceLoader`, `getLoadedClasses` and `clearCache` for its `ScriptOnConnectorLoader`, not for the engine's loader. `getResource` and `getResourceAsStream` give the same answers as on master; - a cached script runs against the classes it was compiled against, script-root classes such as `Helper` and the classes compiled with them included, until it is evicted or the configuration is released. This holds even after their source changes on disk and the operation scripts move to the new classes. Master recompiled the text on every call and so picked up the change; a newly compiled text still does; - a class evicted while another thread runs it loses any metaclass changes it made. Its metaclass is rebuilt on its next dynamic call, after which the class is only softly held; - a compilation that finishes after `release()` goes into the orphaned cache, so its class is only softly held. Not changed: - operation scripts (`loadScript`, `evaluate`) are still compiled and cached by the engine; - the customizer lifecycle and the exceptions `runScriptOnConnector` maps. ## Test strength All in `ScriptOnConnectorCacheTest`. The ed25f73, ce3a86f, 37b50d1 and 21f99b4 cells are runs of the new tests against the production code of those earlier heads of this PR. | arm | case | evidence | |---|---|---| | one class per text | `#testSameTextReusesTheCompiledClass` | compile-red at c328553 | | classes stay out of the engine loader | `#testScriptClassStaysOutOfTheEngineLoader` | compile-red at c328553; mutants "child loader's parent is `getParentLoader()`, not the engine's loader" and "the text compiles through the `parseClass` it passes on to the engine" killed | | the text's configuration has the default imports | `#testScriptSeesTheDefaultImports` | mutant "text configuration without the import customizer" killed | | script-root classes resolve | `#testScriptSeesTheClassesInTheScriptRoots` | kept behaviour | | a text shares script-root classes with the operation scripts | `#testScriptSharesScriptRootClassesWithOperationScripts` | red at ed25f73; mutants "no engine resolver" and "default resolver first" killed | | … also after a source edit, the text compiled first | `#testNewScriptSharesTheEditedScriptRootClassWithOperationScripts` | red at ed25f73; both resolver mutants killed | | script roots shadow the connector's classes | `#testScriptRootClassShadowsAClassOfTheConnectorLoader` | red at ed25f73; both resolver mutants killed | | a cached text keeps the class it was compiled against | `#testCachedScriptKeepsTheScriptRootClassItWasCompiledAgainst` | kept behaviour, green at ed25f73 | | … also where its code first runs after the engine recompiled the class, nested classes included | `#testCachedScriptLinksTheScriptRootClassesItWasCompiledAgainst` | red at ce3a86f (`hi hi`); mutant "no `loadClass` override" killed | | … a class of a script-root source that the text names without that source's own class | `#testCachedScriptLinksTheEngineClassesItWasCompiledAgainst` | kept behaviour, green at 37b50d1; mutants "no lookup of the resolved classes" and "keep only the script-root classes" killed | | … statically compiled code, with classes reached only through the methods of a script-root class | `#testCachedStaticScriptLinksTheClassesCompiledWithAScriptRootClass` | red at 37b50d1 (`LinkageError`, loader constraint violation); mutants "no lookup in the script-root loaders" and "nested classes only" killed | | texts compiled together share a script-root class | `#testScriptsCompiledTogetherShareTheScriptRootClasses` | red at ed25f73; mutant "no engine compiler lock" killed (five rounds of four threads: a race, so a likely kill, not a certain one) | | a text and an operation script that load a script-root class together share it | `#testTextAndOperationScriptThatLoadAScriptRootClassTogetherShareIt` | red at 37b50d1 (two copies of `Helper`); mutant "no engine compiler lock" killed | | texts compile together on a new engine | `#testScriptsCompileTogetherOnANewEngine` | red at ed25f73 (`unable to resolve class org$forgerock$openicf$misc$scriptedcommon$ICFObjectBuilder`); mutant "no engine compiler lock" killed, with the engine's configuration shared | | what a subclass adds to the engine's configuration in `getRoots` applies to texts | `#testScriptCompilesWithWhatASubclassAddsToTheEngineConfiguration` | red at 37b50d1 (`unable to resolve class RootsList`); mutant "a new configuration built as the engine's" killed | | binding reaches the script | `#testScriptSeesItsBinding` | compile-red at c328553 | | a URL that a text adds reaches the operation scripts | `#testUrlThatATextAddsToItsLoaderReachesTheOperationScripts` | red at 37b50d1 (`ClassNotFoundException: drv.Driver`); mutant "no `addURL` override" killed | | a class that a text compiles at run time reaches the operation scripts | `#testClassThatATextParsesReachesTheOperationScripts` | red at 21f99b4 (`MissingPropertyException: No such property: Parsed`); mutant "the `parseClass` override takes the engine's lock but compiles into the text's loader" killed | | … and waits for the engine's compilation | `#testClassThatATextParsesWaitsForTheEngineCompilation` | red at 21f99b4 (`parseClass` returns while the engine compiles an operation script) | | `release()` unregisters and recompiles | `#testReleaseUnregistersTheCachedClasses` | compile-red at c328553 | | … also when the release closure throws | `#testReleaseUnregistersTheCachedClassesWhenTheReleaseClosureThrows` | mutant "clear the cache after the `try`/`finally`" killed | | customizer before publish | `#testCustomizerCanRunAScriptOnConnector` | mutant without the null-cache guard killed (`NullPointerException`) | | … and its script is not cached | `#testScriptRunByTheCustomizerIsNotCached` | mutant "cache created before the customizer runs" killed | | an engine built by a subclass compiles with its own configuration | `#testEngineBuiltByASubclassCompilesTheScriptWithItsOwnConfiguration` | red at ce3a86f (`unable to resolve class EngineList`); mutants "no cache: compile with the child loader anyway" and "no null-cache guard" killed | | a hit does not wait for a compile | `#testCachedScriptDoesNotWaitForAnotherCompilation` | mutants `synchronized getScriptOnConnectorClass` and "cache lookup under the engine's compiler lock" killed (`TimeoutException`) | | size property: name, default, `5`, ` 7 `, `0`, `-3`, `many` | `#testCacheSizeComesFromTheSystemProperty` | compile-red at f4b1286 | | LRU, not insertion order | `#testLeastRecentlyUsedScriptIsEvicted` | mutant `accessOrder` `false` killed | | eviction unregisters closures too | `#testEvictedScriptClassesAreUnregistered` | mutants "unregister only the main class" and "no unregister in `put`" killed | | an evicted class unloads | `#testEvictedScriptClassCanBeUnloaded` | same two mutants killed | | the connector reuses the class | `#testConnectorReusesTheCompiledScript` | red at a40030c | | arguments and `log` binding | `#testConnectorScriptSeesItsArgumentsAndTheLogger` | kept behaviour, green at a40030c; mutants "`log` is `Log.getLog(Object.class)`" and "`evaluateScriptOnConnector` rebinds `log` to the script's class" killed | | a connector-run class unloads once evicted | `#testScriptRunByTheConnectorCanBeUnloadedOnceEvicted` | red at a40030c; mutant Groovy-side `createScript(...).run()` killed (the call site keeps the class) | | broken script-root class → compile error | `#testBrokenScriptRootClassFailsAsACompilationError` | red at a872c5b (`GroovyBugError`); both resolver mutants killed | | … also from the connector | `#testConnectorReportsABrokenScriptRootClassAsACompilationError` | red at a872c5b (`GroovyBugError`); both resolver mutants killed | | a `ScriptException` from the engine → compile error | `#testScriptExceptionFromTheEngineFailsAsACompilationError` | red at 37b50d1 (`GroovyBugError`); mutants "`ConnectorException.wrap`" and "no cause" killed | The two unload tests run `System.gc()` for up to about 5 s and return as soon as the class is gone. They rely on the JVM's default soft-reference policy, which surefire leaves unchanged. Not pinned by a test: - the first-wins rule when two threads compile the same new text; - the WARN for an invalid size, and reading the size once per engine; - unregistering closure classes on `release()` (eviction pins the shared `unregister()`); - the fallback without a cache, and its WARN, when `GroovyClassLoader.sourceCache` cannot be read; - that `loadClass` takes from the loader of a script-root class only the classes that loader defined. A mutant that takes any class the loader returns passes: the two differ only for a class that the script-root class linked before the engine replaced it. `mvn -pl OpenICF-groovy-connector,OpenICF-ssh-connector verify` on JDK 26: - groovy: `Tests run: 179, Failures: 0, Errors: 0, Skipped: 42`. The 42 skipped tests are the five `*SampleTest` classes, which need a private configuration; - ssh: `Tests run: 135, Failures: 0, Errors: 0, Skipped: 120`. The skipped sample tests need a live host. Master is at 4648e38 now. #188, #191, #194 and #197 touch none of these files. The branch merges into it cleanly, and the groovy module then runs `Tests run: 210, Failures: 0, Errors: 0, Skipped: 42`.
XMLHandlerImpl.updatenow refuses to rename an entry to a__NAME__that another entry of the same class has, and it refuses a blank__NAME__even where the XSD makes__NAME__optional. When the class has no__UID__, so the uid is the name, it returns the new uid.Before,
createrefused a name that already existed or was blank, butupdateaccepted any new__NAME__, a blank one too when the XSD made__NAME__optional, and it always returned the uid it was given.update(ACCOUNT, <uid of n2>, {__NAME__=n1})while another account isn1n1returns two objects, andauthenticateasn1withn2's password fails withInvalidPasswordException(#159)AlreadyExistsException:Could not update entry. An entry with the __NAME__ of n1 already exists.; entry and file unchangedupdate(GROUP, Rebels, {__NAME__=Alliance})(__GROUP__has no__UID__)Rebels;getObject(GROUP, Rebels)isnullAllianceupdate(ACCOUNT, <uid>, {__NAME__=""})where a user XSD makes__ACCOUNT__'s__NAME__optional__UID__, then fails withIllegalArgumentException: The Attribute set must contain a 'Name'." "is accepted as well and fails the same way once the file is saved and loaded againIllegalArgumentException:Parameter '__NAME__' must not be blank.; entry unchangedupdate(GROUP, dup, {__NAME__=""})where a user XSD makes__GROUP__'s__NAME__optionalIllegalArgumentException: Uid value must not be blank!IllegalArgumentException:Parameter '__NAME__' must not be blank.; entry unchanged__NAME__is still reported asParameter '__NAME__' must not be blank.create's duplicate check: names are looked up within the entry's object class, so an account may still have a group's name, and values are compared as they are (" padded "is not"padded").update(u2, {__NAME__=dup, lastname=L2b})still succeeds whenu1isdupas well.__NAME__is checked as a required attribute in every class, even where a user XSD makes it optional:createrequires a name whatever the XSD says, and the framework builds no object without aName. One cleared entry made every search of its class fail, reconciliation included, and it could not be read by its__UID__to be repaired. The blank check runs before the duplicate check, so a blank name is reported as blank, not as taken.__NAME__.create's duplicate message still names__UID__; it is not changed here.__UID__, the returned uid is unchanged.Two existing tests renamed an entry into a name another entry had, and they now use other names.
XMLHandlerIndexTests#renamedEntryIsFoundByItsNewNameOnlyrenamesu1torenamed-againinstead of back todup, whichu2has.XMLHandlerIndexTests#indexAgreesWithXQueryAfterChangesno longer renamesu2torenamedand back todup; insteadu1re-sendsdup, its own name. Re-linkingu1in front ofu2also pins the ordered insert ofEntryIndex.link(EntryIndex.java:225). The old step re-linkedu2at the end of the list, which is also its place in document order, so it could not catch an insert at the end.Not fixed here:
__NAME__element (XML connector: update, delete and authenticate break on an entry below another element #182). Changing the inner entry leaves the outer entry's index key stale until the next reload (EntryIndex.java:49-51). Until then, the check can accept a rename into the outer entry's current name, and refuse the outer entry re-sending its current name when another entry has it too. The connector never writes that shape.__UID__, the caller gets the exception and keeps the old uid.__NAME__is already blank in the file (hand-edited, or cleared byupdatebefore this change) still makes every search of its class fail. The read side is not changed.Test strength
XMLHandlerImpl.java:357-362refuse a__NAME__another entry hasXMLHandlerIndexTests#renameIntoANameAnotherEntryHasIsRefusedstep 1 (the message)__UID__in the messageXMLHandlerReloadTests#renameIntoATakenNameLeavesTheEntryAndTheFileAlone(firstnamefirst, then the taken name: the message, the file's mtime and bytes, then both entries):756-757looked up within the entry's classrenameIntoANameAnotherEntryHasIsRefusedstep 2 (groupduptoThe Empire, a group's name);keepingANameAnotherEntryAlsoHasIsAllowedstep 3 (groupduptono-uid, an account's name)ObjectClass.ACCOUNTconstant; both classes looked uprenameIntoATakenNameLeavesTheEntryAndTheFileAlone(accounts only),keepingANameAnotherEntryAlsoHasIsAllowedstep 1,indexAgreesWithXQueryAfterChangesObjectClass.GROUPconstant (step 1 ofrenameIntoANameAnotherEntryHasIsRefuseddoes not kill it: a group is namedduptoo):758an entry that has the name may keep itkeepingANameAnotherEntryAlsoHasIsAllowedstep 1 (u2re-sendsdup, whichu1has first);indexAgreesWithXQueryAfterChanges("first duplicate re-keyed":u1re-sendsdup):357only__NAME__is checkedkeepingANameAnotherEntryAlsoHasIsAllowedstep 2 (lastname=dup):328__NAME__checked as required in every class, so a blank one is refusedclearingAnOptionalNameIsRefused(a copy of the test XSD with an optional account__NAME__;""and" "refused with the message, then a search of the whole class returns both accounts);clearingTheOptionalNameOfAnEntryWithoutUidIsRefused(a copy with an optional group__NAME__; the same refusals, then the group is still found asdup)__NAME__checked only when required (the account's clear accepted; the group's fails on the framework'sUid value must not be blank!):334the blank check before the duplicate checkrenameIntoANameAnotherEntryHasIsRefusedstep 3 (u1to""whileu10's name is""in the index: the blank message, not the duplicate one)EntryIndex.java:225a re-keyed entry re-linked in document orderindexAgreesWithXQueryAfterChanges("first duplicate re-keyed"):364,:365the new name as uid when the class has no__UID__keepingANameAnotherEntryAlsoHasIsAllowedstep 3 (duptono-uidreturnsno-uid);XMLConnectorTests#renamingAGroupReturnsItsNewNameAsUid(through the facade:RebelstoAlliancereturnsAlliance,getObject(Rebels)isnull, a rename ofAlliancetoThe Empireis refused)renamedEntryIsFoundByItsNewNameOnly(a renamed account keepsu1)isSupportUidignored; inverted:401the input uid otherwisekeepingANameAnotherEntryAlsoHasIsAllowedstep 3 (a__DESCRIPTION__update ofno-uidreturnsno-uid)newUidreturned without the fallbackmvn -pl OpenICF-xml-connector verify: 248 unit tests (242 + 6) and 1 IT, green on JDK 26 and JDK 11.Fixes #159