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