[ 
https://issues.apache.org/jira/browse/WW-5685?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18107167#comment-18107167
 ] 

Lukasz Lenart commented on WW-5685:
-----------------------------------

PR [#1860|https://github.com/apache/struts/pull/1860] opened against main. 
Confirmed the defect is still live on {{main}} and fixed it as suggested — 
{{break}} to {{continue}}, plus a debug log line so a skipped key is at least 
traceable.

h3. A correction to the suggested test

This ticket proposed "a {{-conversion.properties}} file whose first key is 
already mapped". That cannot be written as a fixture: {{Properties}} extends 
{{Hashtable}}, so there is no first key you can control from the file. A 
fixture whose colliding key happens to hash to the last position passes against 
the unfixed code, because {{break}} then drops nothing.

That is not hypothetical — it is what happened here. The hierarchy test was 
written first, its shared key landed last in iteration order, and it passed 
with {{break}} still in place. Only reverting the fix and re-running exposed it.

Two tests resulted, both mutation-checked:
* {{DefaultConversionFileProcessorTest}} derives the colliding key from the 
actual iteration order at run time and pre-maps whichever key comes first. 
{{break}} then registers nothing at all, so the test discriminates on any JDK 
in any hash order, with no dependence on {{Hashtable}} layout. A second case 
asserts every entry registers when nothing is pre-mapped, so a fix that skips 
too much would also fail.
* 
{{XWorkConverterTest.testPropertiesEntriesAfterAKeyCollisionAreStillRegistered}}
 covers the realistic hierarchy trigger from this ticket, a subclass and 
superclass file sharing a key. It does depend on fixture ordering, so the 
shared key is now named to be read first — verified identical on Temurin 
17.0.14, 21.0.7 and 25.0.1 — and the test asserts that precondition, failing 
loudly with an explanatory message if a future reordering would make it vacuous 
again.

h3. Scope

The {{mapping.containsKey}} precedence rule is untouched: the first source to 
claim a key still wins, matching the annotation path. Only the loop exit 
changed.

The remaining {{break}} at {{XWorkConverter:761}} was checked and is correct — 
it ends an interface search after a hit, unrelated to this defect.

{{mvn test -DskipAssembly -pl core}}: 3193 tests, 0 failures, 0 errors. 
Behaviour does change, in that previously dropped entries now register, so this 
is worth a Version Notes line; no Migration Guide entry is needed.

> DefaultConversionFileProcessor silently drops the rest of a 
> -conversion.properties file after the first already-mapped key
> --------------------------------------------------------------------------------------------------------------------------
>
>                 Key: WW-5685
>                 URL: https://issues.apache.org/jira/browse/WW-5685
>             Project: Struts 2
>          Issue Type: Bug
>          Components: Core Actions
>            Reporter: Lukasz Lenart
>            Priority: Major
>             Fix For: 7.4.0
>
>          Time Spent: 10m
>  Remaining Estimate: 0h
>
> h3. Problem
> {{DefaultConversionFileProcessor.process}} iterates the entries of a 
> {{-conversion.properties}} file and skips keys that are already present in 
> the converter mapping. The skip is written as a {{break}} rather than a 
> {{continue}}, so instead of skipping that one entry it abandons the whole 
> file:
> {code:java}
> for (Map.Entry entry : prop.entrySet()) {
>     String key = (String) entry.getKey();
>     if (mapping.containsKey(key)) {
>         break;          // <-- should be continue
>     }
>     ...
> }
> {code}
> Every remaining entry in that properties file is silently dropped. There is 
> no warning and no error; the affected properties simply fall back to default 
> conversion at runtime.
> h3. Why it triggers in practice
> {{XWorkConverter.buildConverterMapping}} walks the class hierarchy — the 
> class itself, then its interfaces, then its superclass — and passes _one 
> shared, accumulating_ mapping into each {{addConverterMapping}} call. So a 
> key claimed earlier in that walk aborts a later file entirely:
> * A subclass {{Foo-conversion.properties}} declares {{bar}}.
> * The superclass {{FooBase-conversion.properties}} declares {{bar}}, {{baz}} 
> and {{qux}}.
> * When the superclass file is processed, {{bar}} is already mapped, so 
> {{baz}} and {{qux}} are never registered.
> Annotation-derived entries land in the same map, so an {{@TypeConversion}} on 
> the class can abort its properties file the same way.
> h3. Nondeterminism
> {{Properties}} extends {{Hashtable}} and {{entrySet()}} has no defined 
> iteration order, so _which_ entries survive depends on hash order rather than 
> file order. The same file can behave differently across JDK versions or after 
> an unrelated key is added, which makes this hard to diagnose from the symptom.
> h3. Related
> WW-3871 fixed the identical {{break}} instead of {{continue}} defect in the 
> annotation path ({{DefaultConversionAnnotationProcessor}}), merged as PR 
> #1812 and shipped in 7.3.0. This is the same bug in the properties-file path, 
> which that change did not touch. It was noted during that work but 
> deliberately left out of scope.
> h3. Suggested fix
> Change the {{break}} to {{continue}}, and add a regression test with a 
> {{-conversion.properties}} file whose first key is already mapped, asserting 
> the later keys still register. A hierarchy case (subclass and superclass 
> files sharing a key) covers the realistic trigger.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to