You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by ta...@apache.org on 2026/01/15 21:20:00 UTC

(myfaces) branch main updated: Spec #1940 - Add clearModel to f:resetValues

This is an automated email from the ASF dual-hosted git repository.

tandraschko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/myfaces.git


The following commit(s) were added to refs/heads/main by this push:
     new dbd5bf5df Spec #1940 - Add clearModel to f:resetValues
dbd5bf5df is described below

commit dbd5bf5dffa971fc274f759c0d4ee748d85b092f
Author: tandraschko <ta...@apache.org>
AuthorDate: Thu Jan 15 22:19:48 2026 +0100

    Spec #1940 - Add clearModel to f:resetValues
---
 .../java/jakarta/faces/component/UIViewRoot.java   | 154 ++++++++++++++-------
 .../faces/component/behavior/AjaxBehavior.java     |  46 ++++++
 .../context/servlet/PartialViewContextImpl.java    |  19 ++-
 .../view/facelets/tag/faces/core/AjaxHandler.java  |   6 +
 .../core/ResetValuesActionListenerHandler.java     |  27 ++--
 5 files changed, 193 insertions(+), 59 deletions(-)

diff --git a/api/src/main/java/jakarta/faces/component/UIViewRoot.java b/api/src/main/java/jakarta/faces/component/UIViewRoot.java
index d1a8ab219..b5bb263ab 100644
--- a/api/src/main/java/jakarta/faces/component/UIViewRoot.java
+++ b/api/src/main/java/jakarta/faces/component/UIViewRoot.java
@@ -25,19 +25,10 @@
 import org.apache.myfaces.core.api.shared.lang.LocaleUtils;
 import java.io.IOException;
 import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import java.util.stream.Collectors;
 
 import jakarta.el.MethodExpression;
 import jakarta.el.ValueExpression;
@@ -144,7 +135,7 @@ public class UIViewRoot extends UIComponentBase implements UniqueIdVendor
     }
 
     // todo: is it right to save the state of _events and _phaseListeners?
-    private List<FacesEvent> _events;
+    private Map<PhaseId, List<FacesEvent>> _events;
 
     /**
      * Map containing view scope objects. 
@@ -962,10 +953,15 @@ public void queueEvent(FacesEvent event)
         Assert.notNull(event, "event");
         if (_events == null)
         {
-            _events = new ArrayList<FacesEvent>();
+            _events = new EnumMap<>(PhaseId.class);
         }
 
-        _events.add(event);
+        List<FacesEvent> eventsByPhaseId = _events.get(event.getPhaseId());
+        if (eventsByPhaseId == null) {
+            eventsByPhaseId = new ArrayList<>(5);
+            _events.put(event.getPhaseId(), eventsByPhaseId);
+        }
+        eventsByPhaseId.add(event);
     }
 
     @Override
@@ -1457,11 +1453,20 @@ public Map<String, Object> getAttributes()
     }
 
     /**
-     * @since 2.2
-     * @param context
-     * @param clientIds
+     * <p class="changed_added_5_0">
+     * Use {@link #visitTree} to visit the clientIds with the given visit hints, if any, and, if the node is an instance of {@link EditableValueHolder}, call its
+     * {@link EditableValueHolder#resetValue} method, and, if {@code clearModel} is true, then invoke {@link EditableValueHolder#setValue(Object)} with value of {@code null},
+     * followed by {@link UIInput#updateModel(FacesContext)}.
+     * </p>
+     *
+     * @since 5.0
+     *
+     * @param context the {@link FacesContext} for the request we are processing.
+     * @param clientIds The client ids to be visited, on which the described action will be taken.
+     * @param clearModel Whether to clear the model values as well.
+     * @param visitHints Any visit hints you wish to apply to the visit.
      */
-    public void resetValues(FacesContext context, java.util.Collection<java.lang.String> clientIds, VisitHint... visitHints)
+    public void resetValues(FacesContext context, Collection<String> clientIds, boolean clearModel, VisitHint... visitHints)
     {
         if (clientIds == null || clientIds.isEmpty())
         {
@@ -1489,7 +1494,7 @@ public void resetValues(FacesContext context, java.util.Collection<java.lang.Str
             else {
                 // lazy init
                 if (contextCallback == null) {
-                    contextCallback = new ResetInputContextCallback(visitContext);
+                    contextCallback = new ResetInputContextCallback(visitContext, clearModel);
                 }
 
                 this.invokeOnComponent(context, clientId, contextCallback);
@@ -1497,6 +1502,16 @@ public void resetValues(FacesContext context, java.util.Collection<java.lang.Str
         }
     }
 
+    /**
+     * @since 2.2
+     * @param context
+     * @param clientIds
+     */
+    public void resetValues(FacesContext context, java.util.Collection<java.lang.String> clientIds, VisitHint... visitHints)
+    {
+        resetValues(context, clientIds, false, visitHints);
+    }
+
     /**
      * Indicates if the component is created when facelets builds the view and
      * is caused by the presence of a ResourceDependency annotation.
@@ -1790,34 +1805,47 @@ private void _processValidatorsDefault(FacesContext context)
         super.processValidators(context);
     }
 
+    /**
+     * <p class="changed_added_5_0">
+     * Returns an unmodifiable map of unmodifiable lists of faces events that have been queued so far.
+     * </p>
+     * @return an unmodifiable map of unmodifiable lists of faces events that have been queued so far.
+     * @since 5.0
+     */
+    public Map<PhaseId, List<FacesEvent>> getQueuedEvents()
+    {
+        if (_events == null) {
+            return Collections.emptyMap();
+        }
+
+        return _events.entrySet().stream().collect(
+                Collectors.toUnmodifiableMap(
+                        Map.Entry::getKey,
+                        entry -> Collections.unmodifiableList(entry.getValue())));
+    }
+
     /**
      * Gathers all event for current and ANY phase
      * @param phaseId current phase id
      */
     private Events _getEvents(PhaseId phaseId)
     {
-        // Gather the events and purge the event list to prevent concurrent modification during broadcasting
-        int size = _events.size();
-        List<FacesEvent> anyPhase = new ArrayList<>(size);
-        List<FacesEvent> onPhase = new ArrayList<>(size);
+        // Gather events for ANY_PHASE
+        List<FacesEvent> anyPhaseList = _events.get(PhaseId.ANY_PHASE);
+        List<FacesEvent> anyPhase = anyPhaseList != null ? new ArrayList<>(anyPhaseList) : new ArrayList<>(0);
 
-        for (int i = 0; i < size; i++)
+        // Gather events for the specific phase
+        List<FacesEvent> onPhaseList = _events.get(phaseId);
+        List<FacesEvent> onPhase = onPhaseList != null ? new ArrayList<>(onPhaseList) : new ArrayList<>(0);
+
+        // Clear the retrieved events from the map to prevent concurrent modification during broadcasting
+        if (anyPhaseList != null)
         {
-            FacesEvent event = _events.get(i);
-            if (event.getPhaseId().equals(PhaseId.ANY_PHASE))
-            {
-                anyPhase.add(event);
-                _events.remove(i);
-                size--;
-                i--;
-            }
-            else if (event.getPhaseId().equals(phaseId))
-            {
-                onPhase.add(event);
-                _events.remove(i);
-                size--;
-                i--;
-            }
+            anyPhaseList.clear();
+        }
+        if (onPhaseList != null)
+        {
+            onPhaseList.clear();
         }
 
         return new Events(anyPhase, onPhase);
@@ -1836,7 +1864,7 @@ private Map<PhaseId, boolean[]> _getListenerSuccessMap()
     {
         if (listenerSuccessMap == null)
         {
-            listenerSuccessMap = new HashMap<>(PhaseId.VALUES.size(), 1f);
+            listenerSuccessMap = new EnumMap<>(PhaseId.class);
         }
         return listenerSuccessMap;
     }
@@ -1958,17 +1986,19 @@ public List<FacesEvent> getOnPhase()
         }
     }
 
-    public static class ResetInputContextCallback implements ContextCallback {
+    private static class ResetInputContextCallback implements ContextCallback {
 
         private VisitContext visitContext;
+        private boolean clearModel;
 
         /**
          * Constructs a new ResetInputContextCallback with the given {@link VisitContext}.
          *
          * @param visitContext the visit context to be used for visiting component trees
          */
-        public ResetInputContextCallback(VisitContext visitContext) {
+        public ResetInputContextCallback(VisitContext visitContext, boolean clearModel) {
             this.visitContext = visitContext;
+            this.clearModel = clearModel;
         }
 
         /**
@@ -1981,24 +2011,48 @@ public ResetInputContextCallback(VisitContext visitContext) {
          */
         @Override
         public void invokeContextCallback(FacesContext fc, UIComponent component) {
-            if (component instanceof EditableValueHolder) {
-                ((EditableValueHolder) component).resetValue();
+            if (component instanceof EditableValueHolder evh) {
+                evh.resetValue();
+
+                if (clearModel) {
+                    evh.setValue(null);
+                    if (evh instanceof UIInput input) {
+                        input.updateModel(visitContext.getFacesContext());
+                    }
+                }
             }
             else {
-                component.visitTree(visitContext, ResetInputVisitCallback.INSTANCE);
+                component.visitTree(visitContext, ResetInputVisitCallback.getInstance(clearModel));
             }
         }
     }
 
-    public static class ResetInputVisitCallback implements VisitCallback {
+    private static class ResetInputVisitCallback implements VisitCallback {
+
+        private static final ResetInputVisitCallback INSTANCE = new ResetInputVisitCallback(false);
+        private static final ResetInputVisitCallback INSTANCE_CLEAR_MODEL = new ResetInputVisitCallback(true);
+
+        public static ResetInputVisitCallback getInstance(boolean clearModel) {
+            return clearModel ? INSTANCE_CLEAR_MODEL : INSTANCE;
+        }
+
+        private final boolean clearModel;
 
-        public static final ResetInputVisitCallback INSTANCE = new ResetInputVisitCallback();
+        public ResetInputVisitCallback(boolean clearModel) {
+            this.clearModel = clearModel;
+        }
 
         @Override
         public VisitResult visit(VisitContext context, UIComponent target) {
-            if (target instanceof EditableValueHolder) {
-                EditableValueHolder input = (EditableValueHolder) target;
-                input.resetValue();
+            if (target instanceof EditableValueHolder evh) {
+                evh.resetValue();
+
+                if (clearModel) {
+                    evh.setValue(null);
+                    if (evh instanceof UIInput input) {
+                        input.updateModel(context.getFacesContext());
+                    }
+                }
             }
 
             return VisitResult.ACCEPT;
diff --git a/api/src/main/java/jakarta/faces/component/behavior/AjaxBehavior.java b/api/src/main/java/jakarta/faces/component/behavior/AjaxBehavior.java
index 430e876e1..329dae8ab 100644
--- a/api/src/main/java/jakarta/faces/component/behavior/AjaxBehavior.java
+++ b/api/src/main/java/jakarta/faces/component/behavior/AjaxBehavior.java
@@ -52,6 +52,7 @@ public class AjaxBehavior extends ClientBehaviorBase
     private static final String ATTR_DISABLED = "disabled";
     private static final String ATTR_IMMEDIATE = "immediate";
     private static final String ATTR_DELAY = "delay";
+    private static final String ATTR_CLEAR_MODEL = "clearModel";
     private static final String ATTR_RESET_VALUES = "resetValues";
 
     /**
@@ -237,6 +238,51 @@ public void setDelay(String delay)
         getStateHelper().put(ATTR_DELAY, delay);
     }
 
+    /**
+     * <p class="changed_added_5_0">
+     * Return the clearModel status of this behavior.
+     * </p>
+     *
+     * @return the clearModel status.
+     *
+     * @since 5.0
+     */
+    public boolean isClearModel()
+    {
+        Boolean result = (Boolean) getStateHelper().eval(ATTR_CLEAR_MODEL);
+        return result != null ? result : false;
+    }
+
+    /**
+     * <p class="changed_added_5_0">
+     * Set the clearModel status of this behavior.
+     * </p>
+     *
+     * @param clearModel the clearModel status.
+     *
+     * @since 5.0
+     */
+    public void setClearModel(boolean clearModel)
+    {
+        getStateHelper().put(ATTR_CLEAR_MODEL, clearModel);
+    }
+
+    /**
+     * <p class="changed_added_5_0">
+     * Tests whether the clearModel attribute is specified. Returns true if the clearModel attribute is specified, either
+     * as a locally set property or as a value expression.
+     * </p>
+     *
+     * @return the flag whether the clearModel attribute is specified.
+     *
+     * @since 5.0
+     */
+    public boolean isClearModelSet()
+    {
+        return (getStateHelper().get(ATTR_CLEAR_MODEL) != null) ||
+                (getValueExpression(ATTR_CLEAR_MODEL) != null);
+    }
+
     @Override
     public Set<ClientBehaviorHint> getHints() 
     {
diff --git a/impl/src/main/java/org/apache/myfaces/context/servlet/PartialViewContextImpl.java b/impl/src/main/java/org/apache/myfaces/context/servlet/PartialViewContextImpl.java
index 904639011..c2ee06ea1 100644
--- a/impl/src/main/java/org/apache/myfaces/context/servlet/PartialViewContextImpl.java
+++ b/impl/src/main/java/org/apache/myfaces/context/servlet/PartialViewContextImpl.java
@@ -33,6 +33,7 @@
 import jakarta.faces.component.UIComponent;
 import jakarta.faces.component.UIViewParameter;
 import jakarta.faces.component.UIViewRoot;
+import jakarta.faces.component.behavior.AjaxBehavior;
 import jakarta.faces.component.behavior.ClientBehaviorContext;
 import jakarta.faces.component.visit.VisitCallback;
 import jakarta.faces.component.visit.VisitContext;
@@ -44,6 +45,7 @@
 import jakarta.faces.context.PartialResponseWriter;
 import jakarta.faces.context.PartialViewContext;
 import jakarta.faces.context.ResponseWriter;
+import jakarta.faces.event.BehaviorEvent;
 import jakarta.faces.event.PhaseId;
 import jakarta.faces.lifecycle.ClientWindow;
 import jakarta.faces.render.RenderKit;
@@ -90,6 +92,7 @@ public class PartialViewContextImpl extends PartialViewContext
     private VisitContextFactory visitContextFactory = null;
     private Boolean _resetValues = null;
     private List<String> _evalScripts = new ArrayList<>();
+    private List<AjaxBehavior> queuedAjaxBehaviors;
 
     public PartialViewContextImpl(FacesContext context)
     {
@@ -387,6 +390,17 @@ public void processPartial(PhaseId phaseId)
                 || phaseId == PhaseId.PROCESS_VALIDATIONS
                 || phaseId == PhaseId.UPDATE_MODEL_VALUES)
         {
+            if (phaseId == PhaseId.APPLY_REQUEST_VALUES)
+            {
+                queuedAjaxBehaviors = viewRoot.getQueuedEvents().values().stream().flatMap(List::stream)
+                        .filter(BehaviorEvent.class::isInstance)
+                        .map(BehaviorEvent.class::cast)
+                        .map(BehaviorEvent::getBehavior)
+                        .filter(AjaxBehavior.class::isInstance)
+                        .map(AjaxBehavior.class::cast)
+                        .toList();
+            }
+
             processPartialExecute(viewRoot, phaseId);
         }
         else if (phaseId == PhaseId.RENDER_RESPONSE)
@@ -441,7 +455,9 @@ private void processPartialRendering(UIViewRoot viewRoot, PhaseId phaseId)
             
             if (isResetValues())
             {
-                viewRoot.resetValues(context, getRenderIds(), RESET_VALUES_HINTS);
+                boolean clearModel = queuedAjaxBehaviors != null
+                        && queuedAjaxBehaviors.stream().anyMatch(AjaxBehavior::isClearModel);
+                viewRoot.resetValues(context, getRenderIds(), clearModel, RESET_VALUES_HINTS);
             }
 
             if (pvc.isRenderAll())
@@ -685,6 +701,7 @@ public void release()
         _partialRequest = null;
         _renderAll = null;
         context = null;
+        queuedAjaxBehaviors = null;
         _released = true;
     }
     
diff --git a/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java b/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java
index d3ecf1e20..8958365d3 100644
--- a/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java
+++ b/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java
@@ -138,6 +138,10 @@ public class AjaxHandler extends TagHandler implements
     @JSFFaceletAttribute(name = "resetValues", className = "jakarta.el.ValueExpression",
             deferredValueType = "java.lang.Boolean")
     private final TagAttribute resetValues;
+
+    @JSFFaceletAttribute(name = "clearModel", className = "jakarta.el.ValueExpression",
+            deferredValueType = "java.lang.Boolean")
+    private final TagAttribute clearModel;
     
     private final boolean wrappingMode;
 
@@ -154,6 +158,7 @@ public AjaxHandler(TagConfig config)
         render = getAttribute("render");
         delay = getAttribute("delay");
         resetValues = getAttribute("resetValues");
+        clearModel = getAttribute("clearModel");
 
         // According to the spec, this tag works in two different ways:
         // 1. Apply an ajax behavior for a selected component in this way
@@ -365,6 +370,7 @@ else if (!cvh.getEventNames().contains(eventName))
         setAttribute(faceletContext, ajaxBehavior, render, Object.class);
         setAttribute(faceletContext, ajaxBehavior, delay, String.class, (v) -> ajaxBehavior.setDelay(v));
         setAttribute(faceletContext, ajaxBehavior, resetValues, Boolean.class, (v) -> ajaxBehavior.setResetValues(v));
+        setAttribute(faceletContext, ajaxBehavior, clearModel, Boolean.class, (v) -> ajaxBehavior.setClearModel(v));
         if (listener != null)
         {
             MethodExpression expr = listener.getMethodExpression(
diff --git a/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/ResetValuesActionListenerHandler.java b/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/ResetValuesActionListenerHandler.java
index c9593ab78..1640f3d1d 100644
--- a/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/ResetValuesActionListenerHandler.java
+++ b/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/ResetValuesActionListenerHandler.java
@@ -62,6 +62,10 @@ public class ResetValuesActionListenerHandler extends TagHandler implements Acti
     @JSFFaceletAttribute(name = "render", className = "jakarta.el.ValueExpression",
                          deferredValueType = "java.lang.Object")
     private final TagAttribute _render;
+
+    @JSFFaceletAttribute(name = "clearModel", className = "jakarta.el.ValueExpression",
+            deferredValueType = "java.lang.Object")
+    private final TagAttribute _clearModel;
     
     private final Collection<String> _clientIds;
 
@@ -86,6 +90,7 @@ public ResetValuesActionListenerHandler(TagConfig config)
         {
             _clientIds = null;
         }
+        _clearModel = getAttribute("clearModel");
     }
 
     /*
@@ -163,7 +168,8 @@ public void applyAttachedObject(FacesContext context, UIComponent parent, boolea
             listener = new LiteralResetValuesActionListener(_clientIds,
                 topParentComponent != null
                         ? (Location) topParentComponent.getAttributes().get(CompositeComponentELUtils.LOCATION_KEY)
-                        : null);
+                        : null,
+                    _render.getBoolean(faceletContext));
         }
         else
         {
@@ -171,7 +177,8 @@ public void applyAttachedObject(FacesContext context, UIComponent parent, boolea
                     _render.getValueExpression(faceletContext, Object.class),
                     topParentComponent != null
                             ? (Location) topParentComponent.getAttributes().get(CompositeComponentELUtils.LOCATION_KEY)
-                            : null);
+                            : null,
+                    _render.getBoolean(faceletContext));
         }
         as.addActionListener(listener);
     }
@@ -195,15 +202,17 @@ private final static class ResetValuesActionListener implements ActionListener,
 
         private ValueExpression renderExpression;
         private Location topCompositeComponentReference;
+        private boolean clearModel;
         
         private ResetValuesActionListener()
         {
         }
         
-        public ResetValuesActionListener(ValueExpression renderExpression, Location location)
+        public ResetValuesActionListener(ValueExpression renderExpression, Location location, boolean clearModel)
         {
             this.renderExpression = renderExpression;
             this.topCompositeComponentReference = location;
+            this.clearModel = clearModel;
         }
 
         @Override
@@ -261,14 +270,14 @@ else if (value instanceof String string)
                 contextComponent = event.getComponent();
             }
 
-            List<String> list = new ArrayList<>();
+            List<String> list = new ArrayList<>(clientIds.size());
             for (String id : clientIds)
             {
                 list.add(getComponentId(faces, contextComponent, id));
             }
             
             // Call resetValues
-            root.resetValues(faces, list);
+            root.resetValues(faces, list, clearModel);
         }
     }
     
@@ -278,15 +287,17 @@ private final static class LiteralResetValuesActionListener implements ActionLis
 
         private Collection<String> clientIds;
         private Location topCompositeComponentReference;
+        private boolean clearModel;
         
         private LiteralResetValuesActionListener()
         {
         }
         
-        public LiteralResetValuesActionListener(Collection<String> clientIds, Location location)
+        public LiteralResetValuesActionListener(Collection<String> clientIds, Location location, boolean clearModel)
         {
             this.clientIds = clientIds;
             this.topCompositeComponentReference = location;
+            this.clearModel = clearModel;
         }
 
         @Override
@@ -324,13 +335,13 @@ public void processAction(ActionEvent event) throws AbortProcessingException
                 contextComponent = event.getComponent();
             }
 
-            List<String> list = new ArrayList<String>();
+            List<String> list = new ArrayList<>(clientIds.size());
             for (String id : clientIds)
             {
                 list.add(getComponentId(faces, contextComponent, id));
             }
             
-            root.resetValues(faces, list);
+            root.resetValues(faces, list, clearModel);
         }
     }