From 8c6f1576a100fc6576fe1e9f840e8237a09db717 Mon Sep 17 00:00:00 2001 From: Bauke Scholtz Date: Wed, 4 Nov 2026 14:33:26 -0400 Subject: [PATCH] Save valid and localValueSet of UIInput with its local value again Since 4e2bf732f, valid and localValueSet were held in the TransientStateHelper, while the local value they qualify stayed in the saved StateHelper. An input which got a local value but never reached updateModel, because its parent turned rendered=false during the same Update Model Values walk, then kept that local value with the flag dropped. Every later updateModel bailed on !isLocalValueSet(), and the input rendered the stale local value forever. Save both flags in the StateHelper again, as in 4.1.3, and restore their INITIAL_STATE_PROPERTIES entries so that a flag back at its default leaves the delta. submittedValue stays transient. Co-Authored-By: Claude Opus 5.5 --- .../java/jakarta/faces/component/UIInput.java | 32 +++-- .../api/shared/EditableValueHolderState.java | 5 +- .../faces/component/UIInputPSSTest.java | 116 ++++++++++++++++++ 3 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 impl/src/test/java/jakarta/faces/component/UIInputPSSTest.java diff --git a/api/src/main/java/jakarta/faces/component/UIInput.java b/api/src/main/java/jakarta/faces/component/UIInput.java index f0ace5047..d31b58c96 100644 --- a/api/src/main/java/jakarta/faces/component/UIInput.java +++ b/api/src/main/java/jakarta/faces/component/UIInput.java @@ -962,17 +962,20 @@ public void setValidatorMessage(String validatorMessage) @Override public boolean isValid() { - TransientStateHelper tsh = getTransientStateHelper(false); - return tsh == null || (Boolean) tsh.getTransient(PropertyKeys.valid, Boolean.TRUE); + Object value = getStateHelper().get(PropertyKeys.valid); + if (value != null) + { + return (Boolean) value; + } + return true; } @Override public void setValid(boolean valid) { - SimpleTransientStateHelper tsh = (SimpleTransientStateHelper) getTransientStateHelper(!valid); - if (tsh != null) + if (getStateHelper().get(PropertyKeys.valid) != null || !valid) { - tsh.setOrRemoveTransient(PropertyKeys.valid, valid ? null : Boolean.FALSE); + getStateHelper().put(PropertyKeys.valid, valid ); } } @@ -985,17 +988,20 @@ public void setValid(boolean valid) @Override public boolean isLocalValueSet() { - TransientStateHelper tsh = getTransientStateHelper(false); - return tsh != null && (Boolean) tsh.getTransient(PropertyKeys.localValueSet, Boolean.FALSE); + Object value = getStateHelper().get(PropertyKeys.localValueSet); + if (value != null) + { + return (Boolean) value; + } + return false; } @Override public void setLocalValueSet(boolean localValueSet) { - SimpleTransientStateHelper tsh = (SimpleTransientStateHelper) getTransientStateHelper(localValueSet); - if (tsh != null) + if (getStateHelper().get(PropertyKeys.localValueSet) != null || localValueSet) { - tsh.setOrRemoveTransient(PropertyKeys.localValueSet, localValueSet ? Boolean.TRUE : null); + getStateHelper().put(PropertyKeys.localValueSet, localValueSet ); } } @@ -1071,7 +1077,11 @@ enum PropertyKeys private static final Object[] INITIAL_STATE_PROPERTIES = new Object[]{ UIOutput.PropertyKeys.value, - null + null, + UIInput.PropertyKeys.localValueSet, + false, + UIInput.PropertyKeys.valid, + true }; @Override diff --git a/api/src/main/java/org/apache/myfaces/core/api/shared/EditableValueHolderState.java b/api/src/main/java/org/apache/myfaces/core/api/shared/EditableValueHolderState.java index 4cf845d4c..740166390 100644 --- a/api/src/main/java/org/apache/myfaces/core/api/shared/EditableValueHolderState.java +++ b/api/src/main/java/org/apache/myfaces/core/api/shared/EditableValueHolderState.java @@ -85,8 +85,8 @@ public void saveState(EditableValueHolder evh) public void restoreState(EditableValueHolder evh) { // UIInput.setValue always calls setLocalValueSet(true) per spec, even when value is null. - // When the snapshot has localValueSet=false that forces a transient-state add followed - // immediately by a remove — wasteful in tight UIRepeat loops. + // When the snapshot has localValueSet=false that forces a StateHelper put followed + // immediately by an overwrite — wasteful in tight UIRepeat loops. // // Optimisation: skip setValue entirely when both the snapshot localValue and the // component's current localValue are null — the StateHelper is already in the correct @@ -115,7 +115,6 @@ public static EditableValueHolderState create(EditableValueHolder evh) public static boolean isEmpty(EditableValueHolder evh) { - // Check transient values first return evh.isValid() && !evh.isLocalValueSet() && evh.getSubmittedValue() == null diff --git a/impl/src/test/java/jakarta/faces/component/UIInputPSSTest.java b/impl/src/test/java/jakarta/faces/component/UIInputPSSTest.java new file mode 100644 index 000000000..75e504259 --- /dev/null +++ b/impl/src/test/java/jakarta/faces/component/UIInputPSSTest.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package jakarta.faces.component; + +import jakarta.el.ValueExpression; +import jakarta.faces.event.PhaseId; + +import org.apache.myfaces.test.base.junit.AbstractJsfTestCase; +import org.apache.myfaces.test.el.MockValueExpression; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * The partial state of a {@link UIInput}: the {@code valid} and {@code localValueSet} flags qualify the local value, + * so they are saved together with it, while the submitted value is per-request state and is not saved. + *

+ * The state is saved during Render Response, as it is for the view state, because outside that phase + * {@link UIComponentBase#saveState} also saves the transient state. + */ +public class UIInputPSSTest extends AbstractJsfTestCase +{ + + @Override + @BeforeEach + public void setUp() throws Exception + { + super.setUp(); + facesContext.setCurrentPhaseId(PhaseId.RENDER_RESPONSE); + } + + /** + * A local value saved with the view must still be pushed to the model and cleared by {@link UIInput#updateModel} + * of a later request, so that the model value is rendered afterwards. + */ + @Test + public void testLocalValueSetIsSavedWithTheLocalValue() + { + UIInput input = createInput(); + input.setValue("stale"); + + UIInput restored = restore(input.saveState(facesContext)); + Assertions.assertTrue(restored.isLocalValueSet()); + + restored.updateModel(facesContext); + Assertions.assertFalse(restored.isLocalValueSet()); + Assertions.assertEquals("stale", facesContext.getExternalContext().getRequestMap().get("value")); + Assertions.assertEquals("stale", restored.getValue()); + Assertions.assertNull(restored.saveState(facesContext)); + } + + @Test + public void testInvalidIsSaved() + { + UIInput input = createInput(); + input.setValid(false); + + UIInput restored = restore(input.saveState(facesContext)); + Assertions.assertFalse(restored.isValid()); + } + + @Test + public void testFlagsBackAtTheirDefaultsAddNothingToTheSavedState() + { + UIInput input = createInput(); + input.setValid(false); + input.setValid(true); + input.setLocalValueSet(true); + input.setLocalValueSet(false); + + Assertions.assertNull(input.saveState(facesContext)); + } + + @Test + public void testSubmittedValueIsNotSaved() + { + UIInput input = createInput(); + input.setSubmittedValue("submitted"); + + UIInput restored = restore(input.saveState(facesContext)); + Assertions.assertNull(restored.getSubmittedValue()); + } + + private UIInput createInput() + { + UIInput input = new UIInput(); + input.setId("input"); + ValueExpression expression = new MockValueExpression("#{requestScope.value}", String.class); + input.setValueExpression("value", expression); + input.markInitialState(); + return input; + } + + private UIInput restore(Object state) + { + UIInput restored = createInput(); + restored.restoreState(facesContext, state); + return restored; + } +}