Skip to content

WW-5701 fix(conversion): compare the conversion marker by identity, not equals - #1874

Open
lukaszlenart wants to merge 2 commits into
mainfrom
fix/WW-5701-collection-converter-identity
Open

WW-5701 fix(conversion): compare the conversion marker by identity, not equals#1874
lukaszlenart wants to merge 2 commits into
mainfrom
fix/WW-5701-collection-converter-identity

Conversation

@lukaszlenart

Copy link
Copy Markdown
Member

Fixes WW-5701

Problem

CollectionConverter decided whether an element had converted successfully by
comparing the result to TypeConverter.NO_CONVERSION_POSSIBLE with equals().
The marker's value is the ordinary text ognl.NoConversionPossible, so an
element that genuinely held that text converted fine and was then silently
discarded.

vs.setValue("names", new String[]{"alpha", "ognl.NoConversionPossible", "omega"});

result:   [alpha, omega]
expected: [alpha, ognl.NoConversionPossible, omega]

Nothing signalled the loss. No conversion had failed, so no conversion error was
registered — the action simply saw a shorter collection.

The exposure is wider than collections declared to hold String: at
CollectionConverter.java:48-50, when no element type can be determined the
member type defaults to String.class, so untyped collections are affected too.

Fix

Compare by reference instead, at all three sites (lines 64, 75, 83).

Please do not simplify this back to equals() — that is what caused the bug.
Identity is correct here rather than incidental:

  • The constant is declared Object, not String
    (TypeConverter.java:49), so it is not a JLS constant variable and is not
    inlined into referencing class files. Every reference resolves to the one field
    value at runtime, third-party converters compiled elsewhere included.
  • A parameter value built by a servlet container from request bytes is a distinct
    object, so reference comparison separates "the converter signalled failure"
    from "the user submitted this text".

Scope, stated precisely

This protects values arriving from a request, which is the case that matters.
It does not protect a value that happens to be interned — application code
calling the converter programmatically with a String literal would still see it
dropped, because all identical literals share one interned instance.

Closing that too would mean giving the marker an identity no user string can
share, which changes a published constant and is a binary-compatibility question
rather than a bug fix. Out of scope here, and noted so the limit is not
misread as an oversight.

Testing

New CollectionConverterTest, written test-first and mutation-checked: the
production change was stashed to confirm the test fails without it
([alpha, omega]).

  • testElementWhoseTextEqualsTheMarkerIsKept — the fixture is built at runtime
    rather than written as a literal, precisely because a literal would be interned
    to the same instance as the constant and would not represent a real request.
    An assertNotSame guards that, so the test cannot silently regress into
    testing nothing.
  • testUnconvertibleElementIsStillDropped — the guard must keep doing its job;
    this passed before and after the change.

Full core suite: 3199 tests, 0 failures.

Related

WW-5700 / #1873 fixes the
mirror-image defect in the map and list property accessors, which stored the
marker instead of skipping it, and uses identity comparison for the same reason.
This one was found while reviewing that fix. The two are independent and can
merge in either order.

🤖 Generated with Claude Code

…ot equals

CollectionConverter decided whether an element had converted successfully
by comparing the result to TypeConverter.NO_CONVERSION_POSSIBLE with
equals(). The marker's value is the ordinary text "ognl.NoConversionPossible",
so an element that genuinely held that text converted fine and was then
silently discarded from the resulting collection.

Nothing signalled the loss: no conversion had failed, so no conversion
error was registered and the action simply saw a shorter collection.
The exposure is not limited to collections declared to hold Strings -
when no element type can be determined the member type defaults to
String.class, so untyped collections are affected too.

Compare by reference instead, at all three sites.

Identity is correct here rather than incidental. The constant is declared
Object, not String, so it is not a JLS constant variable and is not
inlined into referencing class files; every reference resolves to the one
field value at runtime, including in third-party converters compiled
elsewhere. A parameter value built by a servlet container from request
bytes is a distinct object, so reference comparison separates "the
converter signalled failure" from "the user submitted this text". Please
do not simplify this back to equals(), which is what caused the bug.

WW-5700 fixed the mirror-image defect in the map and list property
accessors, which stored the marker instead of skipping it, and used
identity comparison for the same reason. Found while reviewing that fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e guard paths

The Sonar quality gate on the pull request failed at 77.8% coverage of new
code: the marker guard was only exercised on the array-source path, leaving
the false branch of the other two guards uncovered.

Both added paths are reachable from a request - a Set-typed property fed from
a List, and a single-valued parameter assigned to a collection property. The
single-value holder is seeded before the assignment so that a setter which is
never called cannot make the test pass vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes WW-5701 by preventing CollectionConverter from discarding legitimate user-supplied values that happen to equal the textual representation of the NO_CONVERSION_POSSIBLE marker.

Changes:

  • Switch marker checks in CollectionConverter from equals() to reference comparison (!=) to correctly detect converter-failure signalling.
  • Add a new CollectionConverterTest covering the WW-5701 regression and related conversion-drop scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
core/src/main/java/org/apache/struts2/conversion/impl/CollectionConverter.java Uses identity comparison against NO_CONVERSION_POSSIBLE to avoid false positives when user values equal the marker’s text.
core/src/test/java/org/apache/struts2/conversion/impl/CollectionConverterTest.java Adds regression tests ensuring marker-text values are preserved and truly unconvertible values are still dropped.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

for (Object anObjArray : objArray) {
Object convertedValue = converter.convertValue(context, target, member, propertyName, anObjArray, memberType);
if (!NO_CONVERSION_POSSIBLE.equals(convertedValue)) {
if (convertedValue != NO_CONVERSION_POSSIBLE) {
for (Object aCol : col) {
Object convertedValue = converter.convertValue(context, target, member, propertyName, aCol, memberType);
if (!NO_CONVERSION_POSSIBLE.equals(convertedValue)) {
if (convertedValue != NO_CONVERSION_POSSIBLE) {
TypeConverter converter = getTypeConverter(context);
Object convertedValue = converter.convertValue(context, target, member, propertyName, value, memberType);
if (!NO_CONVERSION_POSSIBLE.equals(convertedValue)) {
if (convertedValue != NO_CONVERSION_POSSIBLE) {
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants