[ 
https://issues.apache.org/jira/browse/MYFACES-4775?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Thomas Andraschko resolved MYFACES-4775.
----------------------------------------
    Resolution: Fixed

> c:forEach writes a submitted value into the element saved with the view state 
> instead of into the live item
> -----------------------------------------------------------------------------------------------------------
>
>                 Key: MYFACES-4775
>                 URL: https://issues.apache.org/jira/browse/MYFACES-4775
>             Project: MyFaces Core
>          Issue Type: Bug
>    Affects Versions: 5.0.0, 4.1.4
>            Reporter: Bauke Scholtz
>            Assignee: Thomas Andraschko
>            Priority: Major
>             Fix For: 5.0.0
>
>
> While analyzing what the Faces view state contains, inputs bound to a 
> property of the {{var}} of a {{<c:forEach>}} turned out to lose their 
> submitted value without any message, also when nothing changes between the 
> requests.
> h3. Reproducer
> {code:xml}
> <h:form id="viewForm">
>     <c:forEach items="#{viewItems.list}" var="item">
>         <h:inputText id="name_#{item.id}" value="#{item.name}" />
>     </c:forEach>
>     <h:commandButton id="submit" value="Submit" />
> </h:form>
> <h:outputText value="Bean values: #{viewItems.list[0].name}, 
> #{viewItems.list[1].name}" />
> <h:form id="requestForm">
>     <c:forEach items="#{requestItems.list}" var="item">
>         <h:inputText id="name_#{item.id}" value="#{item.name}" />
>     </c:forEach>
>     <h:commandButton id="submit" value="Submit" />
> </h:form>
> <h:outputText value="Bean values: #{requestItems.list[0].name}, 
> #{requestItems.list[1].name}" />
> {code}
> {code:java}
> public class Item implements Serializable {
>     private final String id;
>     private String name;
>     // Constructor, getters and setter.
> }
> @Named @ViewScoped
> public class ViewItems implements Serializable {
>     private List<Item> list;
>     @PostConstruct
>     public void init() {
>         list = new ArrayList<>(List.of(new Item("a", "original a"), new 
> Item("b", "original b")));
>     }
>     public List<Item> getList() {
>         return list;
>     }
> }
> @Named @RequestScoped
> public class RequestItems {
>     private List<Item> list;
>     @PostConstruct
>     public void init() {
>         list = List.of(new Item("a", "original a"), new Item("b", "original 
> b"));
>     }
>     public List<Item> getList() {
>         return list;
>     }
> }
> {code}
> Open the page, change both names of a form to "edited a" and "edited b", and 
> submit it. Nothing changes between the requests, so every build makes the 
> same decisions. The response renders the names from the bean:
> ||Items in||MyFaces 4.1.4, server||MyFaces 4.1.4, client||Mojarra 4.1.20, 
> server||Mojarra 4.1.20, client||
> |{{@ViewScoped}} bean|edited a, edited b|*original a, original b*|edited a, 
> edited b|edited a, edited b|
> |{{@RequestScoped}} bean|*original a, original b*|*original a, original 
> b*|edited a, edited b|edited a, edited b|
> MyFaces 4.1.3 gives the same result. The same inputs in a {{<ui:repeat>}} 
> instead of a {{<c:forEach>}} receive the values in all cases.
> h3. Cause
> {{ForEachHandler.getVarExpr()}} binds the {{var}} of each row to the element 
> itself, with {{new IteratedValueExpression(ve, value)}}, also for a {{List}} 
> and an array. {{setVar()}} then stores that expression in the 
> {{FaceletState}} via {{putBinding()}}, so the element is saved with the view 
> state. On the postback, {{#\{item.name\}}} resolves against that saved 
> element:
> * With client side state saving, it is always a deserialized copy.
> * With server side state saving, it is a copy as soon as the bean holds new 
> instances, as a {{@RequestScoped}} bean does on every request, or a bean 
> which loads the items from a database.
> The submitted value then lands in an object which nobody refers to anymore.
> h3. Specification
> MYFACES-4029 and MYFACES-4318 report the same behavior. Both were closed as 
> not a bug, because {{<c:forEach>}} "just does not play well with JSF 
> lifecycle". That was a fair reading of the JSF 2.x specification, which 
> indeed did not clearly specify how the rows of a build time iteration are 
> restored.
> The Faces 5.0 specification, which is in progress, specifies it since 
> [jakartaee/faces#2234|https://github.com/jakartaee/faces/pull/2235/changes]. 
> The javadoc of {{StateManagementStrategy.restoreView()}} sets the contract:
> bq. This method returns the view that was saved: restoring the {{Object}} 
> that {{saveView}} returned for a view must produce that view again, holding 
> the same components at the same positions with the same state.
> In short, the spec now finally requires {{restore(save(view))}} to equal 
> exactly {{view}}, in all cases.
> The rows of a {{<c:forEach>}} are reproduced from the build time decisions 
> saved with the view, such as the range and the row count. The elements are 
> not part of the view: each row reads its element from the items. Restore 
> View, in the description of build time conditions:
> bq. The items an iteration was rendered over are a further exception, since 
> its rows read their element from those items rather than from the state: 
> where the items no longer hold them, the value submitted for such a row 
> reaches nothing that outlives the request.
> So the elements themselves are not needed to restore the view, and the 
> reproducer above is a valid page. The javadoc of {{restoreView()}} calls the 
> case where the items no longer hold the elements "the case which cannot be 
> reproduced from saved state at all".
> The Faces 5.0 TCK covers that case in 
> {{Issue5961IT.testIterationOverVanishedItems()}}: the rows are restored, but 
> the submitted values must not reach the model. With server side state saving, 
> MyFaces 4.1.5-SNAPSHOT fails that test: the saved element is then the same 
> session scoped instance, so the submitted values reach the model, and the 
> names read "first,second" instead of "a,b". With client side state saving, it 
> passes only because the values land in a deserialized copy. This was run with 
> the page and beans of that test deployed on Tomcat, not with the TCK harness.
> MyFaces itself has replayed the decisions of {{<c:if>}}, {{<c:choose>}} and 
> {{ui:include}} already correctly since 2.0.10 and 2.1.4 (MYFACES-3329), and 
> the rows of {{<c:forEach>}} since 2.2.0 (MYFACES-3811). Only for 
> {{<c:forEach>}} does it save more than the decision: the elements themselves.
> Mojarra implements this only since 4.0.23 and 4.1.14 
> ([eclipse-ee4j/mojarra#5965|https://github.com/eclipse-ee4j/mojarra/pull/5965]),
>  with {{com.sun.faces.restoreBuildTimeDecisions}} set to {{true}}, which 
> becomes default in Mojarra 5.0. Each row then reads its element live, by 
> position or by key. A row whose element is gone reads a stand-in, and 
> [{{ForEachHandler}} logs a 
> warning|https://github.com/eclipse-ee4j/mojarra/blob/f4d595f502b864b3e57bff6c6ec93564a28a7f07/impl/src/main/java/com/sun/faces/facelets/tag/jstl/core/ForEachHandler.java#L453-L455]
>  which names the {{<c:forEach>}}. On the page of that TCK test, Mojarra 
> 4.1.21-SNAPSHOT passes and logs:
> {noformat}
> WARNING: The build which restored this postback reproduced 2 of the 2 rows 
> /issue5961-vanisheditems.xhtml @10,51 <c:forEach> rendered over items which 
> no longer hold their elements, so a value submitted for such a row is decoded 
> into a stand-in for the element that is gone rather than into the model.
> {noformat}
> h3. Proposed fix
> Let a row read its element from the live items, by index for a {{List}} and 
> an array, as {{IndexedValueExpression(ve, i)}} did before MYFACES-3811. Keep 
> the saved state only for what decides the restored rows, such as the range 
> and the row count, and not for the elements themselves.
> {{LegacyForEachHandler}}, enabled with 
> {{org.apache.myfaces.STRICT_JSF_2_FACELETS_COMPATIBILITY=true}}, already 
> binds the {{var}} of a {{List}} with {{new IndexedValueExpression(ve, i)}} 
> and does not put it in the {{FaceletState}}. With that handler, the 
> reproducer above writes "edited a, edited b" into both beans, with server and 
> client side state saving alike. That only shows the approach works in 
> MyFaces: the context param also affects the scope of {{<c:set>}} and 
> {{<ui:param>}} across inclusions (see 
> [jakartaee/faces#2248|https://github.com/jakartaee/faces/issues/2248]), and 
> the {{<c:forEach>}} fix must apply by default, independent of any context 
> param.
> Fix is needed in MyFaces 5.0.0 in order to pass Faces 5.0 TCK and just a nice 
> to have in MyFaces 4.1.5.
> h3. History
> * MYFACES-3811 
> ([a2b11c073|https://github.com/apache/myfaces/commit/a2b11c073]) replaced 
> {{new IndexedValueExpression(ve, i)}} by {{new IteratedValueExpression(ve, 
> value)}}. The old line is still there as a comment.
> * MYFACES-3825 
> ([2d351df95|https://github.com/apache/myfaces/commit/2d351df95]) put the 
> {{var}} expression in the {{FaceletState}}, which saves it with the view 
> state.
> 🤖 Generated with Claude Opus 5.5



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

Reply via email to