/* * 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.taghandler; import static java.lang.Boolean.TRUE; import static javax.faces.event.PhaseId.RESTORE_VIEW; import static org.omnifaces.util.Components.hasInvokedSubmit; import static org.omnifaces.util.Events.subscribeToRequestAfterPhase; import static org.omnifaces.util.Events.subscribeToViewEvent; import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.el.ValueExpression; import javax.faces.component.UICommand; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.component.behavior.ClientBehaviorHolder; import javax.faces.context.FacesContext; import javax.faces.event.PostValidateEvent; import javax.faces.event.PreValidateEvent; import javax.faces.event.SystemEvent; import javax.faces.event.SystemEventListener; import javax.faces.validator.Validator; import javax.faces.view.facelets.ComponentHandler; import javax.faces.view.facelets.FaceletContext; import javax.faces.view.facelets.TagConfig; import javax.faces.view.facelets.TagHandler; import org.omnifaces.component.validator.ValidateMultipleFields; import org.omnifaces.util.Callback; /** * <p> * The <code><o:skipValidators></code> taghandler allows the developer to entirely skip validation when * executing an {@link UICommand} or {@link ClientBehaviorHolder} action. This taghandler must be placed inside an * {@link UICommand} or {@link ClientBehaviorHolder} component (client behavior holder components are those components * supporting <code><f:ajax></code>). * * <h3>Usage</h3> * <p> * For example, when adding a new row to the data table, you'd like to not immediately validate all empty rows. * <pre> * <h:form> * <h:dataTable value="#{bean.items}" var="item"> * <h:column> * <h:inputText value="#{item.value}" required="true" /> * </h:column> * </h:dataTable> * <h:commandButton value="add new row" action="#{bean.add}"> * <o:skipValidators /> * </h:commandButton> * <h:commandButton value="save all data" action="#{bean.save}" /> * <h:messages /> * </h:form> * </pre> * <p> * Note that converters will still run and that model values will still be updated. This behavior is by design. * * @author Michele Mariotti * @author Bauke Scholtz * @since 2.3 */ public class SkipValidators extends TagHandler { // Constants ------------------------------------------------------------------------------------------------------ private static final String ERROR_INVALID_PARENT = "Parent component of o:skipValidators must be an instance of UICommand or ClientBehaviorHolder."; // Constructors --------------------------------------------------------------------------------------------------- /** * The tag constructor. * @param config The tag config. */ public SkipValidators(TagConfig config) { super(config); } // Actions -------------------------------------------------------------------------------------------------------- /** * If the parent component is an instance of {@link UICommand} or {@link ClientBehaviorHolder}, and is new, and * we're in the restore view phase of a postback, then delegate to {@link #processSkipValidators(UIComponent)}. * @throws IllegalStateException When the parent component is not an instance of {@link UICommand} or * {@link ClientBehaviorHolder}. */ @Override public void apply(FaceletContext context, final UIComponent parent) throws IOException { if (!(parent instanceof UICommand || parent instanceof ClientBehaviorHolder)) { throw new IllegalStateException(ERROR_INVALID_PARENT); } FacesContext facesContext = context.getFacesContext(); if (!(ComponentHandler.isNew(parent) && facesContext.isPostback() && facesContext.getCurrentPhaseId() == RESTORE_VIEW)) { return; } // We can't use hasInvokedSubmit() before the component is added to view, because the client ID isn't available. // Hence, we subscribe this check to after phase of restore view. subscribeToRequestAfterPhase(RESTORE_VIEW, new Callback.Void() { @Override public void invoke() { processSkipValidators(parent); }}); } /** * Check if the given component has been invoked during the current request and if so, then register the skip * validators event listener which removes the validators during {@link PreValidateEvent} and restores them during * {@link PostValidateEvent}. * @param parent The parent component of this tag. */ protected void processSkipValidators(UIComponent parent) { if (!hasInvokedSubmit(parent)) { return; } SkipValidatorsEventListener listener = new SkipValidatorsEventListener(); subscribeToViewEvent(PreValidateEvent.class, listener); subscribeToViewEvent(PostValidateEvent.class, listener); } /** * Remove validators during prevalidate and restore them during postvalidate. */ static class SkipValidatorsEventListener implements SystemEventListener { private Map<String, Object> attributes = new HashMap<>(); private Map<String, Validator[]> allValidators = new HashMap<>(); @Override public boolean isListenerForSource(Object source) { return source instanceof UIInput || source instanceof ValidateMultipleFields; } @Override public void processEvent(SystemEvent event) { UIComponent source = (UIComponent) event.getSource(); if (source instanceof UIInput) { processEventForUIInput(event, (UIInput) source); } else if (source instanceof ValidateMultipleFields) { processEventForValidateMultipleFields(event, (ValidateMultipleFields) source); } } private void processEventForUIInput(SystemEvent event, UIInput input) { String clientId = input.getClientId(); if (event instanceof PreValidateEvent) { ValueExpression requiredExpression = input.getValueExpression("required"); attributes.put(clientId, (requiredExpression != null) ? requiredExpression : input.isRequired()); input.setRequired(false); Validator[] validators = input.getValidators(); allValidators.put(clientId, validators); for (Validator validator : validators) { input.removeValidator(validator); } } else if (event instanceof PostValidateEvent) { for (Validator validator : allValidators.remove(clientId)) { input.addValidator(validator); } Object requiredValue = attributes.remove(clientId); if (requiredValue instanceof ValueExpression) { input.setValueExpression("required", (ValueExpression) requiredValue); } else { input.setRequired(TRUE.equals(requiredValue)); } } } private void processEventForValidateMultipleFields(SystemEvent event, ValidateMultipleFields validator) { String clientId = validator.getClientId(); if (event instanceof PreValidateEvent) { ValueExpression disabledExpression = validator.getValueExpression("disabled"); attributes.put(clientId, (disabledExpression != null) ? disabledExpression : validator.isDisabled()); validator.setDisabled(true); } else if (event instanceof PostValidateEvent) { Object disabledValue = attributes.remove(clientId); if (disabledValue instanceof ValueExpression) { validator.setValueExpression("disabled", (ValueExpression) disabledValue); } else { validator.setDisabled(TRUE.equals(disabledValue)); } } } } }