/* * 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.component.validator; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.lang.String.format; import static org.omnifaces.util.Components.getLabel; import static org.omnifaces.util.Components.getValue; import static org.omnifaces.util.Components.isEditable; import static org.omnifaces.util.Components.validateHasNoChildren; import static org.omnifaces.util.Components.validateHasParent; import static org.omnifaces.util.Messages.addError; import static org.omnifaces.util.Messages.addGlobalError; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import javax.faces.component.FacesComponent; import javax.faces.component.UIComponent; import javax.faces.component.UIForm; import javax.faces.component.UIInput; import javax.faces.component.UISelectBoolean; import javax.faces.context.FacesContext; import org.omnifaces.config.OmniFaces; import org.omnifaces.util.State; import org.omnifaces.validator.MultiFieldValidator; /** * Base class which is to be shared between all multi field validators. The implementors have to call the super * constructor with the default message. The implementors have to override the * {@link #validateValues(FacesContext, List, List)} method to perform the actual validation. * <hr> * <h3>General usage of all multiple field validators</h3> * <p> * This validator must be placed inside the same <code>UIForm</code> as the <code>UIInput</code> components in question. * The <code>UIInput</code> components must be referenced by a space separated collection of their client IDs in the * <code>components</code> attribute. This validator can be placed anywhere in the form, but keep in mind that the * components will be validated in the order as they appear in the form. So if this validator is been placed before all * of the components, then it will be executed before any of the component's own validators. If this validator fails, * then the component's own validators will not be fired. If this validator is been placed after all of the components, * then it will be executed after any of the component's own validators. If any of them fails, then this validator * will not be exeucted. It is not recommended to put this validator somewhere in between the referenced components as * the resulting behaviour may be confusing. Put this validator either before or after all of the components, depending * on how you would like to prioritize the validation. * <pre> * <o:validateMultipleFields id="myId" components="foo bar baz" /> * <h:message for="myId" /> * <h:inputText id="foo" /> * <h:inputText id="bar" /> * <h:inputText id="baz" /> * </pre> * <p> * By default, in an invalidating case, all of the referenced components will be marked invalid and a faces message will * be added on the client ID of this validator component. The default message can be changed by the <code>message</code> * attribute. Any "{0}" placeholder in the message will be substituted with a comma separated string of labels of the * referenced input components. * <pre> * <o:validateMultipleFields components="foo bar baz" message="{0} are wrong!" /> * </pre> * <p> * You can also change the default message in the message bundle file as identified by * <code><application><message-bundle></code> in <code>faces-config.xml</code>. The message key is just * the component type as identified by <code>COMPONENT_TYPE</code> constant of the validator component. For example, * {@link ValidateAll} has a {@link ValidateAll#COMPONENT_TYPE} value of * <code>org.omnifaces.component.validator.ValidateAll</code>. Use exactly this value as message bundle key: * <pre> * org.omnifaces.component.validator.ValidateAll = {0} are wrong! * </pre> * <p> * You can use <code>invalidateAll="false"</code> to mark only those components which are actually invalid as invalid. * In case of for example "input all" or "input all or none" validation, that would be only the fields which are left * empty. * <pre> * <o:validateMultipleFields components="foo bar baz" message="{0} are wrong!" invalidateAll="false" /> * </pre> * <p> * The faces message can also be shown for all of the referenced components using <code>showMessageFor="@all"</code>. * <pre> * <o:validateMultipleFields components="foo bar baz" message="This is wrong!" showMessageFor="@all" /> * <h:inputText id="foo" /> * <h:message for="foo" /> * <h:inputText id="bar" /> * <h:message for="bar" /> * <h:inputText id="baz" /> * <h:message for="baz" /> * </pre> * <p> * The faces message can also be shown for only the invalidated components using <code>showMessageFor="@invalid"</code>. * <pre> * <o:validateMultipleFields components="foo bar baz" message="This is wrong!" showMessageFor="@invalid" /> * </pre> * <p> * The faces message can also be shown as global message using <code>showMessageFor="@global"</code>. * <pre> * <o:validateMultipleFields components="foo bar baz" message="This is wrong!" showMessageFor="@global" /> * </pre> * <p> * The faces message can also be shown for specific components referenced by a space separated collection of their * client IDs in <code>showMessageFor</code> attribute. * <pre> * <o:validateMultipleFields components="foo bar baz" message="This is wrong!" showMessageFor="foo baz" /> * </pre> * <p> * The <code>showMessageFor</code> attribute defaults to <code>@this</code>. * <p> * The validator can be disabled by the <code>disabled</code> attribute. It accepts a request based EL expression. * <pre> * <o:validateMultipleFields components="foo bar baz" disabled="#{param.validationDisabled}" /> * </pre> * <p> * There is a read-only <code>validationFailed</code> attribute which can be used to determine if the validation by * this component has failed. * <pre> * <o:validateMultipleFields id="myId" binding="#{myId}" components="foo bar baz" /> * <h:panelGroup rendered="#{myId.validationFailed}"> * Validation has failed! <h:message for="myId" /> * </h:panelGroup> * </pre> * <p> * TODO: support for immediate="true". * * @author Bauke Scholtz */ public abstract class ValidateMultipleFields extends ValidatorFamily implements MultiFieldValidator { // Private constants ---------------------------------------------------------------------------------------------- private static final String DEFAULT_SHOWMESSAGEFOR = "@this"; private static final Boolean DEFAULT_INVALIDATEALL = TRUE; private static final Boolean DEFAULT_DISABLED = FALSE; private static final String ERROR_MISSING_COMPONENTS = "%s attribute 'components' must be specified."; private static final String ERROR_UNKNOWN_COMPONENT = "%s attribute '%s' must refer existing client IDs. Client ID '%s' cannot be found."; private static final String ERROR_INVALID_COMPONENT = "%s attribute '%s' must refer UIInput client IDs. Client ID '%s' is of type '%s'."; private enum PropertyKeys { // Cannot be uppercased. They have to exactly match the attribute names. components, invalidateAll, message, showMessageFor, disabled; } // Variables ------------------------------------------------------------------------------------------------------ private final State state = new State(getStateHelper()); // Properties ----------------------------------------------------------------------------------------------------- private String defaultMessage; private boolean validationFailed; // Constructors --------------------------------------------------------------------------------------------------- /** * The default constructor sets the default message and sets the renderer type to <code>null</code>. */ public ValidateMultipleFields() { defaultMessage = OmniFaces.getMessage(getClass().getAnnotation(FacesComponent.class).value()); setRendererType(null); } // Actions -------------------------------------------------------------------------------------------------------- /** * Validate our component hierarchy. * @throws IllegalStateException When there is no parent of type {@link UIForm}, or when there are any children. */ @Override protected void validateHierarchy() { validateHasParent(this, UIForm.class); validateHasNoChildren(this); } /** * If the validation is not disabled, collect the components, if it is not empty, then collect their values and * delegate to {@link #validateValues(FacesContext, List, List)}. If it returns <code>false</code>, then mark all * inputs and the faces context invalid and finally delegate to {@link #showMessage(FacesContext, List)} to show * the message. */ @Override protected void validateComponents(FacesContext context) { if (isDisabled()) { return; } List<UIInput> inputs = collectComponents(); if (inputs.isEmpty()) { return; } List<Object> values = collectValues(inputs); if (!validateValues(context, inputs, values)) { int i = 0; for (UIInput input : inputs) { input.setValid(!(isInvalidateAll() || shouldInvalidateInput(context, input, values.get(i)))); i++; } validationFailed = true; context.validationFailed(); showMessage(context, inputs); context.renderResponse(); } } /** * Collect the input components. Only those which are an instance of {@link UIInput}, are rendered, not disabled nor * readonly will be returned. If at least one of them has already been validated and is been marked invalid, then an * empty collection will be returned. * @return The input components. * @throws IllegalArgumentException When the <code>components</code> attribute is missing, or when it references an * non-existing component, or when it references a non-input component. */ protected List<UIInput> collectComponents() { String components = getComponents(); if (components.isEmpty()) { throw new IllegalArgumentException(format( ERROR_MISSING_COMPONENTS, getClass().getSimpleName())); } UIComponent namingContainerParent = getNamingContainer(); List<UIInput> inputs = new ArrayList<>(); for (String clientId : components.split("\\s+")) { UIInput input = findInputComponent(namingContainerParent, clientId, PropertyKeys.components); if (!isEditable(input)) { continue; } if (!input.isValid()) { return Collections.emptyList(); } inputs.add(input); } return Collections.unmodifiableList(inputs); } /** * Collect the values of the given input components. * @param inputs The input components to collect values from. * @return The values of the given input components. */ protected List<Object> collectValues(List<UIInput> inputs) { List<Object> values = new ArrayList<>(inputs.size()); for (UIInput input : inputs) { Object value = getValue(input); if (input instanceof UISelectBoolean && Boolean.FALSE.equals(value)) { value = null; } values.add(value); } return Collections.unmodifiableList(values); } /** * Returns whether in in an invalidating case the given input component should be marked invalid. The default * implementation returns <code>true</code>, meaning that <strong>all</strong> input components should be * invalidated in an invalidating case. The overriding implementation may choose to return <code>false</code> for * example when the value is empty, such as {@link ValidateAllOrNone}. * @param context The faces context to work with. * @param input The input component which may need to be invalidated. * @param value The value of the input component. * @return Whether in in an invalidating case the given input component should be marked invalid * @since 1.7 */ protected boolean shouldInvalidateInput(FacesContext context, UIInput input, Object value) { return true; } /** * Show the message at the desired place(s) depending on the value of the <code>showMessageFor</code> attribute. * <ul> * <li><code>@this</code>: message will be added to the <code><h:message></code> for this component. * <li><code>@all</code>: message will be added to all components as specified in <code>components</code> attribute. * <li>Any other value in a space separated collection will be treated as client ID of {@link UIInput} component. * </ul> * @param context The faces context to work with. * @param inputs The validated input components. */ protected void showMessage(FacesContext context, List<UIInput> inputs) { final StringBuilder labels = new StringBuilder(); for (Iterator<UIInput> iterator = inputs.iterator(); iterator.hasNext();) { labels.append(getLabel(iterator.next())); if (iterator.hasNext()) { labels.append(", "); } } addErrorMessage(context, inputs, labels, getMessage(), getShowMessageFor()); } private UIInput findInputComponent(UIComponent parent, String clientId, PropertyKeys property) { UIComponent found = parent.findComponent(clientId); if (found == null) { throw new IllegalArgumentException(format( ERROR_UNKNOWN_COMPONENT, getClass().getSimpleName(), property, clientId)); } else if (!(found instanceof UIInput)) { throw new IllegalArgumentException(format( ERROR_INVALID_COMPONENT, getClass().getSimpleName(), property, clientId, found.getClass().getName())); } return (UIInput) found; } private void addErrorMessage(FacesContext context, List<UIInput> inputs, final StringBuilder labels, String message, String showMessageFor) { if ("@this".equals(showMessageFor)) { addError(getClientId(context), message, labels); } else if ("@all".equals(showMessageFor)) { for (UIInput input : inputs) { addError(input.getClientId(context), message, labels); } } else if ("@invalid".equals(showMessageFor)) { for (UIInput input : inputs) { if (!input.isValid()) { addError(input.getClientId(context), message, labels); } } } else if ("@global".equals(showMessageFor)) { addGlobalError(message, labels); } else { UIComponent namingContainerParent = getNamingContainer(); for (String clientId : showMessageFor.split("\\s+")) { UIInput input = findInputComponent(namingContainerParent, clientId, PropertyKeys.showMessageFor); addError(input.getClientId(context), message, labels); } } } // Attribute getters/setters -------------------------------------------------------------------------------------- /** * Returns the client identifiers of components which are to be validated. * @return The client identifiers of components which are to be validated. */ public String getComponents() { return state.get(PropertyKeys.components, ""); } /** * Sets the client identifiers of components which are to be validated. * @param components The client identifiers of components which are to be validated. */ public void setComponents(String components) { state.put(PropertyKeys.components, components); } /** * Returns whether to invalidate all fields or only those which are actually invalid as per * {@link #shouldInvalidateInput(FacesContext, UIInput, Object)} * @return Whether to invalidate all fields or only those which are actually invalid. * @since 1.7 */ public boolean isInvalidateAll() { return state.get(PropertyKeys.invalidateAll, DEFAULT_INVALIDATEALL); } /** * Sets whether to invalidate all fields or only those which are actually invalid as per * {@link #shouldInvalidateInput(FacesContext, UIInput, Object)} * @param invalidateAll Whether to invalidate all fields or only those which are actually invalid. * @since 1.7 */ public void setInvalidateAll(boolean invalidateAll) { state.put(PropertyKeys.invalidateAll, invalidateAll); } /** * Returns the validation message to be shown. * @return The validation message to be shown. */ public String getMessage() { return state.get(PropertyKeys.message, defaultMessage); } /** * Sets the validation message to be shown. * @param message The validation message to be shown. */ public void setMessage(String message) { state.put(PropertyKeys.message, message); } /** * Returns the client identifiers to show the validation message for. * @return The client identifiers to show the validation message for. */ public String getShowMessageFor() { return state.get(PropertyKeys.showMessageFor, DEFAULT_SHOWMESSAGEFOR); } /** * Sets the client identifiers to show the validation message for. * @param showMessageFor The client identifiers to show the validation message for. */ public void setShowMessageFor(String showMessageFor) { state.put(PropertyKeys.showMessageFor, showMessageFor); } /** * Returns whether the validation should be disabled or not. * @return Whether the validation should be disabled or not. */ public boolean isDisabled() { return state.get(PropertyKeys.disabled, DEFAULT_DISABLED); } /** * Sets whether the validation should be disabled or not. * @param disabled Whether the validation should be disabled or not. */ public void setDisabled(boolean disabled) { state.put(PropertyKeys.disabled, disabled); } /** * Returns whether the validation has failed or not. * @return Whether the validation has failed or not. * @since 1.3 */ public boolean isValidationFailed() { return validationFailed; } }