Bauke Scholtz created MYFACES-4777:
--------------------------------------

             Summary: With rowStatePreserved="true", a row without saved state 
of its own shows the local value of the row before it
                 Key: MYFACES-4777
                 URL: https://issues.apache.org/jira/browse/MYFACES-4777
             Project: MyFaces Core
          Issue Type: Bug
    Affects Versions: 4.1.5
            Reporter: Bauke Scholtz


With {{rowStatePreserved="true"}}, an {{<h:dataTable>}} or {{<ui:repeat>}} 
restores the initial state of its children when it enters a row which has no 
saved state of its own yet, such as a row added to the model after the last 
render. When a child had nothing in its state helper initially, that restore 
keeps what the row before left in the child. So the new row shows the local 
value of the row before it.

h3. Reproducer

{code:xml}
<h:form id="form">
    <h:dataTable id="table" value="#{bean.tableRows}" var="row" 
rowStatePreserved="true">
        <h:column>
            <h:panelGroup id="cell">
                <f:event type="postAddToView" listener="#{bean.addInput}" />
            </h:panelGroup>
        </h:column>
    </h:dataTable>
    <ui:repeat id="repeat" value="#{bean.repeatRows}" var="row" 
rowStatePreserved="true">
        <h:panelGroup id="cell">
            <f:event type="postAddToView" listener="#{bean.addInput}" />
        </h:panelGroup>
    </ui:repeat>
    <h:commandButton id="submit" value="Submit" />
    <h:commandButton id="addRow" value="Add row" action="#{bean.addRow}">
        <f:ajax execute="@this" render="@form" />
    </h:commandButton>
</h:form>
{code}

{code:java}
@Named
@ViewScoped
public class Bean implements Serializable {

    private List<String> tableRows = new ArrayList<>(List.of("0", "1"));
    private List<String> repeatRows = new ArrayList<>(List.of("0", "1"));

    // Adds an input with only an id, so that its initial state has nothing in 
its state helper.
    public void addInput(ComponentSystemEvent event) {
        UIComponent cell = event.getComponent();
        if (cell.getChildCount() == 0) {
            HtmlInputText input = new HtmlInputText();
            input.setId("in");
            cell.getChildren().add(input);
        }
    }

    public void addRow() {
        tableRows.add(String.valueOf(tableRows.size()));
        repeatRows.add(String.valueOf(repeatRows.size()));
    }

    // Getters.
}
{code}

The input has no value expression, so it keeps its local value after the submit.

# Type {{typed}} in row 1 of the table and of the repeat, and click Submit.
# Click Add row. The ajax request adds row 2 to the model and re-renders the 
form.

h3. Expected

Row 1 shows {{typed}}, and the new row 2 is empty.

h3. Actual

Row 2 of the table and of the repeat shows {{typed}}, the local value of row 1.

Reproduced on MyFaces 4.1.5-SNAPSHOT on Tomcat 11, with server side state 
saving.

h3. Cause

When {{UIData}} enters a row without saved state of its own, 
{{restoreFullDescendantComponentStates()}} restores the initial full state of 
each child:

{code:java}
component.clearInitialState();
component.restoreState(facesContext, childState);
component.markInitialState();
{code}

{{UIComponentBase.restoreState()}} passes the state helper slot of that full 
state to {{_DeltaStateHelper.restoreState()}}. The full state of a component 
which had nothing in its state helper has {{null}} in that slot, and 
{{_DeltaStateHelper.restoreState()}} then returns at once:

{code:java}
if (state == null)
{
    return;
}
{code}

So the state helper keeps what the row before put in it. For a {{UIInput}}, 
that includes {{value}} and {{localValueSet}}. {{UIRepeat}} restores the 
initial state of its rows in the same way. The reproducer creates the input in 
Java with only an id, so that its state helper is empty initially.

h3. Spec

The javadoc of {{UIData.setRowStatePreserved()}}:

{quote}
To accomplish this, {{UIData}} must call {{StateHolder#saveState}} and 
{{TransientStateHolder#saveTransientState}} on its children to capture their 
state on exiting each row. When re-entering the row, 
{{StateHolder#restoreState}} and {{TransientStateHolder#restoreTransientState}} 
must be called in order to reinitialize the children to the correct state for 
the new row.
{quote}

The local value of row 1 applies to row 2, so the children of row 2 are not in 
the correct state for that row.

The specification, section "StateHolder", on {{saveState()}} and 
{{restoreState()}}:

{quote}
Gets or restores the state of the instance as a _Serializable_ _Object_.
{quote}

Restoring the full state of a component which had nothing in its state helper 
does not restore that state, because the current state helper stays as it is.

h3. Proposed fix

When {{UIComponentBase.restoreState()}} restores a full state ({{values.length 
== FULL_STATE_ARRAY_SIZE}}) whose state helper slot is {{null}}, clear the 
state helper. A delta keeps its current meaning, where {{null}} means no change.

Mojarra had the same bug, and fixed it the same way in 4.0.30 and 4.1.21 
([eclipse-ee4j/mojarra#6128|https://github.com/eclipse-ee4j/mojarra/issues/6128]).

TCK: {{Issue6128IT}} of the Jakarta Faces 5.0 TCK 
([jakartaee/faces#2266|https://github.com/jakartaee/faces/pull/2266]). MyFaces 
4.1.5-SNAPSHOT fails it on an equivalent page.

🤖 Generated with Claude Opus 5.5



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to