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; + } +}