All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jakarta.faces.validator.BeanValidator Maven / Gradle / Ivy

/*
 * Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package jakarta.faces.validator;

import static jakarta.faces.validator.MessageFactory.getLabel;
import static jakarta.faces.validator.MessageFactory.getMessage;
import static jakarta.faces.validator.MultiFieldValidationUtils.FAILED_FIELD_LEVEL_VALIDATION;
import static jakarta.faces.validator.MultiFieldValidationUtils.getMultiFieldValidationCandidates;
import static jakarta.faces.validator.MultiFieldValidationUtils.wholeBeanValidationEnabled;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import jakarta.el.ValueExpression;
import jakarta.faces.FacesException;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.PartialStateHolder;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.MessageInterpolator;
import jakarta.validation.Validation;
import jakarta.validation.ValidationException;
import jakarta.validation.ValidatorContext;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.groups.Default;

/**
 * 

* A Validator that delegates validation of the * bean property to the Bean Validation API. *

* * @since 2.0 */ public class BeanValidator implements Validator, PartialStateHolder { private static final Logger LOGGER = Logger.getLogger("jakarta.faces.validator", "jakarta.faces.LogStrings"); private String validationGroups; private transient Class[] cachedValidationGroups; /** *

* The standard validator id for this validator, as defined by the Jakarta Server Face specification. *

*/ public static final String VALIDATOR_ID = "jakarta.faces.Bean"; /** *

* The message identifier of the {@link jakarta.faces.application.FacesMessage} to be created if a constraint failure is * found. The message format string for this message may optionally include the following placeholders: *

    *
  • {0} replaced by the interpolated message from Bean Validation.
  • *
  • {1} replaced by a String whose value is the label of the input component that produced * this message.
  • *
*

* The message format string provided by the default implementation should be a the placeholder {0}, thus fully * delegating the message handling to Bean Validation. A developer can override this message format string to make it * conform to other Jakarta Server Face validator messages (i.e., by including the component label) *

*/ public static final String MESSAGE_ID = "jakarta.faces.validator.BeanValidator.MESSAGE"; /** *

* The name of the Jakarta Servlet context attribute which holds the object used by Jakarta Server Faces to obtain * Validator instances. If the Jakarta Servlet context attribute is missing or contains a null value, Jakarta Server * Faces is free to use this Jakarta Servlet context attribute to store the ValidatorFactory bootstrapped by this * validator. *

*/ public static final String VALIDATOR_FACTORY_KEY = "jakarta.faces.validator.beanValidator.ValidatorFactory"; /** *

* The delimiter that is used to separate the list of fully-qualified group names as strings. *

*/ public static final String VALIDATION_GROUPS_DELIMITER = ","; /** *

* The regular expression pattern that identifies an empty list of validation groups. *

*/ public static final String EMPTY_VALIDATION_GROUPS_PATTERN = "^[\\W" + VALIDATION_GROUPS_DELIMITER + "]*$"; /** *

* If this param is defined, and calling toLowerCase().equals(“true”) on a String * representation of its value returns true, the runtime must not automatically add the validator with * validator-id equal to the value of the symbolic constant {@link #VALIDATOR_ID} to the list of default validators. * Setting this parameter to true will have the effect of disabling the automatic installation of Bean * Validation to every input component in every view in the application, though manual installation is still possible. *

* */ public static final String DISABLE_DEFAULT_BEAN_VALIDATOR_PARAM_NAME = "jakarta.faces.validator.DISABLE_DEFAULT_BEAN_VALIDATOR"; /** *

* If this param is set, and calling toLowerCase().equals("true") on a String representation of its value returns * {@code true} take the additional actions relating to <validateWholeBean /> specified in * {@link #validate}. *

* * @since 2.3 */ public static final String ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME = "jakarta.faces.validator.ENABLE_VALIDATE_WHOLE_BEAN"; // ----------------------------------------------------------- multi-field validation /** *

* A comma-separated list of validation groups which are used to filter which validations get checked by this validator. * If the validationGroupsArray attribute is omitted or is empty, the validation groups will be inherited from the * branch defaults or, if there are no branch defaults, the {@link jakarta.validation.groups.Default} group will be * used. *

* * @param validationGroups comma-separated list of validation groups (string with only spaces and commas treated as * null) */ public void setValidationGroups(String validationGroups) { clearInitialState(); String newValidationGroups = validationGroups; // treat empty list as null if (newValidationGroups != null && newValidationGroups.matches(EMPTY_VALIDATION_GROUPS_PATTERN)) { newValidationGroups = null; } // only clear cache of validation group classes if value is changing if (newValidationGroups == null && validationGroups != null) { cachedValidationGroups = null; } if (newValidationGroups != null && validationGroups != null && !newValidationGroups.equals(validationGroups)) { cachedValidationGroups = null; } if (newValidationGroups != null && validationGroups == null) { cachedValidationGroups = null; } this.validationGroups = newValidationGroups; } /** *

* Return the validation groups passed to the Validation API when checking constraints. If the validationGroupsArray * attribute is omitted or empty, the validation groups will be inherited from the branch defaults, or if there are no * branch defaults, the {@link jakarta.validation.groups.Default} group will be used. *

* * @return the value of the {@code validatinGroups} attribute. */ public String getValidationGroups() { return validationGroups; } /** *

* Verify that the value is valid according to the Bean Validation * constraints. *

* *
* *

* Obtain a {@link ValidatorFactory} instance by calling * {@link jakarta.validation.Validation#buildDefaultValidatorFactory}. *

* *

* Let validationGroupsArray be a Class [] representing validator groups set on the component by * the tag handler for this validator. The first search component terminates the search for the validation groups value. * If no such value is found use the class name of {@link jakarta.validation.groups.Default} as the value of the * validation groups. *

* *

* Let valueExpression be the return from calling {@link UIComponent#getValueExpression} on the argument * component, passing the literal string “value” (without the quotes) as an argument. If this * application is running in an environment with Jakarta Expression Language, obtain the * ValueReference from valueExpression and let valueBaseClase be the return from calling * ValueReference.getBase() and valueProperty be the return from calling * ValueReference.getProperty(). If an earlier version of Jakarta Expression Language is present, use the * appropriate methods to inspect valueExpression and derive values for valueBaseClass and * valueProperty. *

* *

* If no ValueReference can be obtained, take no action and return. *

* *

* If ValueReference.getBase() return null, take no action and return. *

* *

* Obtain the {@link ValidatorContext} from the {@link ValidatorFactory}. *

* *

* Decorate the {@link MessageInterpolator} returned from {@link ValidatorFactory#getMessageInterpolator} with one that * leverages the Locale returned from {@link jakarta.faces.component.UIViewRoot#getLocale}, and store it in * the ValidatorContext using {@link ValidatorContext#messageInterpolator}. *

* *

* Obtain the {@link jakarta.validation.Validator} instance from the validatorContext. *

* *

* Obtain a jakarta.validation.BeanDescriptor from the jakarta.validation.Validator. If * hasConstraints() on the BeanDescriptor returns false, take no action and return. Otherwise * proceed. *

* *

* Call {@link jakarta.validation.Validator#validateValue}, passing valueBaseClass, valueProperty, the * value argument, and validatorGroupsArray as arguments. *

* *

* If the returned Set<{@link * ConstraintViolation}> is non-empty, for each element in the Set, create a {@link FacesMessage} * where the summary and detail are the return from calling {@link ConstraintViolation#getMessage}. Capture all such * FacesMessage instances into a Collection and pass them to * {@link ValidatorException#ValidatorException(java.util.Collection)}. If the * {@link #ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME} application parameter is enabled and this {@code Validator} instance * has validation groups other than or in addition to the {@code Default} group, record the fact that this field failed * validation so that any <f:validateWholeBean /> component later in the tree is able to skip * class-level validation for the bean for which this particular field is a property. Regardless of whether or not * {@link #ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME} is set, throw the new exception. *

* *

* If the returned {@code Set} is empty, the {@link #ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME} application parameter is * enabled and this {@code Validator} instance has validation groups other than or in addition to the {@code Default} * group, record the fact that this field passed validation so that any <f:validateWholeBean /> * component later in the tree is able to allow class-level validation for the bean for which this particular field is a * property. *

* *
* * @param context {@inheritDoc} * @param component {@inheritDoc} * @param value {@inheritDoc} * * @throws ValidatorException {@inheritDoc} */ @Override public void validate(FacesContext context, UIComponent component, Object value) { if (context == null) { throw new NullPointerException(); } if (component == null) { throw new NullPointerException(); } ValueExpression valueExpression = component.getValueExpression("value"); if (valueExpression == null) { return; } jakarta.validation.Validator beanValidator = getBeanValidator(context); Class[] validationGroupsArray = parseValidationGroups(getValidationGroups()); // PENDING(rlubke, driscoll): When Jakarta Expression Language 1.3 is present, we won't need // this. ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(valueExpression); ValueReference valueReference = expressionAnalyzer.getReference(context.getELContext()); if (valueReference == null) { return; } if (isResolvable(valueReference, valueExpression)) { @SuppressWarnings("rawtypes") Set violationsRaw = null; try { violationsRaw = beanValidator.validateValue(valueReference.getBaseClass(), valueReference.getProperty(), value, validationGroupsArray); } catch (IllegalArgumentException iae) { LOGGER.fine("Unable to validate expression " + valueExpression.getExpressionString() + " using Bean Validation. Unable to get value of expression. " + " Message from Bean Validation: " + iae.getMessage()); } @SuppressWarnings("unchecked") Set> violations = violationsRaw; if (violations != null && !violations.isEmpty()) { ValidatorException toThrow; if (violations.size() == 1) { ConstraintViolation violation = violations.iterator().next(); toThrow = new ValidatorException(getMessage(context, MESSAGE_ID, violation.getMessage(), getLabel(context, component))); } else { Set messages = new LinkedHashSet<>(violations.size()); for (ConstraintViolation violation : violations) { messages.add(getMessage(context, MESSAGE_ID, violation.getMessage(), getLabel(context, component))); } toThrow = new ValidatorException(messages); } // Record the fact that this field failed validation, so that multi-field // validation is not attempted. if (wholeBeanValidationEnabled(context, validationGroupsArray)) { recordValidationResult(context, component, valueReference.getBase(), valueReference.getProperty(), FAILED_FIELD_LEVEL_VALIDATION); } throw toThrow; } } // Record the fact that this field passed validation, so that multi-field // validation can be performed if desired if (wholeBeanValidationEnabled(context, validationGroupsArray)) { recordValidationResult(context, component, valueReference.getBase(), valueReference.getProperty(), value); } } private boolean isResolvable(ValueReference valueReference, ValueExpression valueExpression) { Boolean resolvable = null; String failureMessage = null; if (valueExpression == null) { failureMessage = "Unable to validate expression using Bean " + "Validation. Expression must not be null."; resolvable = false; } else if (valueReference == null) { failureMessage = "Unable to validate expression " + valueExpression.getExpressionString() + " using Bean Validation. Unable to get value of expression."; resolvable = false; } else { Class baseClass = valueReference.getBaseClass(); // case 1, base classes of Map, List, or Array are not resolvable if (baseClass != null) { if (Map.class.isAssignableFrom(baseClass) || Collection.class.isAssignableFrom(baseClass) || Array.class.isAssignableFrom(baseClass)) { failureMessage = "Unable to validate expression " + valueExpression.getExpressionString() + " using Bean Validation. Expression evaluates to a Map, List or array."; resolvable = false; } } } resolvable = null != resolvable ? resolvable : true; if (!resolvable) { LOGGER.fine(failureMessage); } return resolvable; } private Class[] parseValidationGroups(String validationGroupsStr) { if (cachedValidationGroups != null) { return cachedValidationGroups; } if (validationGroupsStr == null) { cachedValidationGroups = new Class[] { Default.class }; return cachedValidationGroups; } List> validationGroupsList = new ArrayList<>(); String[] classNames = validationGroupsStr.split(VALIDATION_GROUPS_DELIMITER); for (String className : classNames) { className = className.trim(); if (className.length() == 0) { continue; } if (className.equals(Default.class.getName())) { validationGroupsList.add(Default.class); } else { try { validationGroupsList.add(Class.forName(className, false, Thread.currentThread().getContextClassLoader())); } catch (ClassNotFoundException e1) { try { validationGroupsList.add(Class.forName(className)); } catch (ClassNotFoundException e2) { throw new FacesException("Validation group not found: " + className); } } } } cachedValidationGroups = validationGroupsList.toArray(new Class[validationGroupsList.size()]); return cachedValidationGroups; } // ----------------------------------------------------- StateHolder Methods @Override public Object saveState(FacesContext context) { if (context == null) { throw new NullPointerException(); } if (!initialStateMarked()) { Object values[] = new Object[1]; values[0] = validationGroups; return values; } return null; } @Override public void restoreState(FacesContext context, Object state) { if (context == null) { throw new NullPointerException(); } if (state != null) { Object values[] = (Object[]) state; validationGroups = (String) values[0]; } } private boolean initialState; @Override public void markInitialState() { initialState = true; } @Override public boolean initialStateMarked() { return initialState; } @Override public void clearInitialState() { initialState = false; } private boolean transientValue; @Override public boolean isTransient() { return transientValue; } @Override public void setTransient(boolean transientValue) { this.transientValue = transientValue; } // ----------------------------------------------------- Private helper methods for bean validation // MOJARRA IMPLEMENTATION NOTE: identical code exists in Mojarra's com.sun.faces.util.BeanValidation private static jakarta.validation.Validator getBeanValidator(FacesContext context) { ValidatorFactory validatorFactory = getValidatorFactory(context); ValidatorContext validatorContext = validatorFactory.usingContext(); MessageInterpolator jsfMessageInterpolator = new JsfAwareMessageInterpolator(context, validatorFactory.getMessageInterpolator()); validatorContext.messageInterpolator(jsfMessageInterpolator); return validatorContext.getValidator(); } private static ValidatorFactory getValidatorFactory(FacesContext context) { ValidatorFactory validatorFactory = null; Object cachedObject = context.getExternalContext().getApplicationMap().get(VALIDATOR_FACTORY_KEY); if (cachedObject instanceof ValidatorFactory) { validatorFactory = (ValidatorFactory) cachedObject; } else { try { validatorFactory = Validation.buildDefaultValidatorFactory(); } catch (ValidationException e) { throw new FacesException("Could not build a default Bean Validator factory", e); } context.getExternalContext().getApplicationMap().put(VALIDATOR_FACTORY_KEY, validatorFactory); } return validatorFactory; } private static class JsfAwareMessageInterpolator implements MessageInterpolator { private FacesContext context; private MessageInterpolator delegate; public JsfAwareMessageInterpolator(FacesContext context, MessageInterpolator delegate) { this.context = context; this.delegate = delegate; } @Override public String interpolate(String message, MessageInterpolator.Context context) { Locale locale = this.context.getViewRoot().getLocale(); if (locale == null) { locale = Locale.getDefault(); } return delegate.interpolate(message, context, locale); } @Override public String interpolate(String message, MessageInterpolator.Context context, Locale locale) { return delegate.interpolate(message, context, locale); } } // ----------------------------------------------------- Private helper methods for whole bean validation private void recordValidationResult(FacesContext context, UIComponent component, Object wholeBean, String propertyName, Object propertyValue) { Map>> multiFieldCandidates = getMultiFieldValidationCandidates(context, true); Map> candidate = multiFieldCandidates.getOrDefault(wholeBean, new HashMap<>()); Map tuple = new HashMap<>(); // new ComponentValueTuple((EditableValueHolder) component, value); tuple.put("component", component); tuple.put("value", propertyValue); candidate.put(propertyName, tuple); multiFieldCandidates.putIfAbsent(wholeBean, candidate); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy