/* * Copyright 2017 OmniFaces * * Licensed 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 org.omnifaces.eventlistener; import static javax.faces.component.visit.VisitContext.createVisitContext; import java.util.Collection; import java.util.EnumSet; import java.util.Set; import javax.faces.component.EditableValueHolder; import javax.faces.component.UIComponent; import javax.faces.component.visit.VisitCallback; import javax.faces.component.visit.VisitContext; import javax.faces.component.visit.VisitHint; import javax.faces.component.visit.VisitResult; import javax.faces.context.FacesContext; import javax.faces.context.PartialViewContext; import javax.faces.event.ActionEvent; import javax.faces.event.ActionListener; import javax.faces.event.AjaxBehaviorListener; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.SystemEventListener; import org.omnifaces.util.Hacks; /** * <p> * The {@link ResetInputAjaxActionListener} will reset input fields which are not executed during ajax submit, but which * are rendered/updated during ajax response. This will prevent those input fields to remain in an invalidated state * because of a validation failure during a previous request. This is very useful for cases where you need to update one * form from another form by for example a modal dialog, or when you need a cancel/clear button. * <p> * How does it work? First, here are some JSF facts: * <ul> * <li>When JSF validation succeeds for a particular input component during the validations phase, then the submitted * value is set to <code>null</code> and the validated value is set as local value of the input component. * <li>When JSF validation fails for a particular input component during the validations phase, then the submitted * value is kept in the input component. * <li>When at least one input component is invalid after the validations phase, then JSF will not update the model * values for any of the input components. JSF will directly proceed to render response phase. * <li>When JSF renders input components, then it will first test if the submitted value is not <code>null</code> and * then display it, else if the local value is not <code>null</code> and then display it, else it will display the * model value. * <li>As long as you're interacting with the same JSF view, you're dealing with the same component state. * </ul> * <p> * So, when the validation has failed for a particular form submit and you happen to need to update the values of input * fields by a different ajax action or even a different ajax form (e.g. populating a field depending on a dropdown * selection or the result of some modal dialog form, etc), then you basically need to reset the target input * components in order to get JSF to display the model value which was edited during invoke action. Otherwise JSF will * still display its local value as it was during the validation failure and keep them in an invalidated state. * <p> * The {@link ResetInputAjaxActionListener} is designed to solve exactly this problem. There are basically three ways * to configure and use it: * <ul> * <li><p>Register it as <code><phase-listener></code> in <code>faces-config.xml</code>. It'll be applied * to <strong>every single</strong> ajax action throughout the webapp, on both <code>UIInput</code> and * <code>UICommand</code> components. * <pre> * <lifecycle> * <phase-listener>org.omnifaces.eventlistener.ResetInputAjaxActionListener</phase-listener> * </lifecycle> * </pre> * <li><p><i>Or</i> register it as <code><action-listener></code> in <code>faces-config.xml</code>. It'll * <strong>only</strong> be applied to ajax actions which are invoked by an <code>UICommand</code> component such as * <code><h:commandButton></code> and <code><h:commandLink></code>. * <pre> * <application> * <action-listener>org.omnifaces.eventlistener.ResetInputAjaxActionListener</action-listener> * </application> * </pre> * <li><p><i>Or</i> register it as <code><f:actionListener></code> on the invidivual <code>UICommand</code> * components where this action listener is absolutely necessary to solve the concrete problem. Note that it isn't * possible to register it on the individual <code>UIInput</code> components using the standard JSF tags. * <pre> * <h:commandButton value="Update" action="#{bean.updateOtherInputs}"> * <f:ajax execute="currentInputs" render="otherInputs" /> * <f:actionListener type="org.omnifaces.eventlistener.ResetInputAjaxActionListener" /> * </h:commandButton> * </pre> * </ul> * <p> * This works with standard JSF, PrimeFaces and RichFaces actions. Only for RichFaces there's a reflection hack, * because its <code>ExtendedPartialViewContextImpl</code> <i>always</i> returns an empty collection for render IDs. * See also <a href="https://issues.jboss.org/browse/RF-11112">RF issue 11112</a>. * <p> * Design notice: being a phase listener was mandatory in order to be able to hook on every single ajax action as * standard JSF API does not (seem to?) offer any ways to register some kind of {@link AjaxBehaviorListener} in an * application wide basis, let alone on a per <code><f:ajax></code> tag basis, so that it also get applied to * ajax actions in <code>UIInput</code> components. There are ways with help of {@link SystemEventListener}, but it * ended up to be too clumsy. * * <p><strong>See also</strong>: * <br><a href="http://java.net/jira/browse/JAVASERVERFACES_SPEC_PUBLIC-1060">JSF spec issue 1060</a> * * @author Bauke Scholtz */ public class ResetInputAjaxActionListener extends DefaultPhaseListener implements ActionListener { // Constants ------------------------------------------------------------------------------------------------------ private static final long serialVersionUID = -5317382021715077662L; private static final Set<VisitHint> VISIT_HINTS = EnumSet.of(VisitHint.SKIP_TRANSIENT, VisitHint.SKIP_UNRENDERED); private static final VisitCallback VISIT_CALLBACK = new VisitCallback() { @Override public VisitResult visit(VisitContext context, UIComponent target) { FacesContext facesContext = context.getFacesContext(); if (facesContext.getPartialViewContext().getExecuteIds().contains(target.getClientId(facesContext))) { return VisitResult.REJECT; } if (target instanceof EditableValueHolder) { ((EditableValueHolder) target).resetValue(); } else if (context.getIdsToVisit() != VisitContext.ALL_IDS) { // Render ID didn't specifically point an EditableValueHolder. Visit all children as well. target.visitTree(createVisitContext(facesContext, null, context.getHints()), VISIT_CALLBACK); } return VisitResult.ACCEPT; } }; // Variables ------------------------------------------------------------------------------------------------------ private transient ActionListener wrapped; // Constructors --------------------------------------------------------------------------------------------------- /** * Construct a new reset input ajax action listener. This constructor will be used when specifying the action * listener by <code><f:actionListener></code> or when registering as <code><phase-listener></code> in * <code>faces-config.xml</code>. */ public ResetInputAjaxActionListener() { this(null); } /** * Construct a new reset input ajax action listener around the given wrapped action listener. This constructor * will be used when registering as <code><action-listener></code> in <code>faces-config.xml</code>. * @param wrapped The wrapped action listener. */ public ResetInputAjaxActionListener(ActionListener wrapped) { super(PhaseId.INVOKE_APPLICATION); this.wrapped = wrapped; } // Actions -------------------------------------------------------------------------------------------------------- /** * Delegate to the {@link #processAction(ActionEvent)} method when this action listener is been registered as a * phase listener so that it get applied on <strong>all</strong> ajax requests. * @see #processAction(ActionEvent) */ @Override public void beforePhase(PhaseEvent event) { processAction(null); } /** * Handle the reset input action as follows, only and only if the current request is an ajax request and the * {@link PartialViewContext#getRenderIds()} does not return an empty collection nor is the same as * {@link PartialViewContext#getExecuteIds()}: find all {@link EditableValueHolder} components based on * {@link PartialViewContext#getRenderIds()} and if the component is not covered by * {@link PartialViewContext#getExecuteIds()}, then invoke {@link EditableValueHolder#resetValue()} on the * component. * @throws IllegalArgumentException When one of the client IDs resolved to a <code>null</code> component. This * would however indicate a bug in the concrete {@link PartialViewContext} implementation which is been used. */ @Override public void processAction(ActionEvent event) { FacesContext context = FacesContext.getCurrentInstance(); PartialViewContext partialViewContext = context.getPartialViewContext(); if (partialViewContext.isAjaxRequest()) { Collection<String> renderIds = getRenderIds(partialViewContext); if (!renderIds.isEmpty() && !partialViewContext.getExecuteIds().containsAll(renderIds)) { context.getViewRoot().visitTree(createVisitContext(context, renderIds, VISIT_HINTS), VISIT_CALLBACK); } } if (wrapped != null && event != null) { wrapped.processAction(event); } } // Helpers -------------------------------------------------------------------------------------------------------- /** * Helper method with RichFaces4 hack to return the proper render IDs from the given partial view context. * @param partialViewContext The partial view context to return the render IDs for. * @return The render IDs. */ private static Collection<String> getRenderIds(PartialViewContext partialViewContext) { Collection<String> renderIds = partialViewContext.getRenderIds(); // WARNING: START OF HACK! ------------------------------------------------------------------------------------ // HACK for RichFaces4 because its ExtendedPartialViewContextImpl class doesn't return its componentRenderIds // property on getRenderIds() call when the action is executed using a RichFaces-specific command button/link. // See also https://issues.jboss.org/browse/RF-11112 if (renderIds.isEmpty() && Hacks.isRichFacesInstalled()) { renderIds = Hacks.getRichFacesRenderIds(); } // END OF HACK ------------------------------------------------------------------------------------------------ return renderIds; } }