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

com.sun.faces.ext.component.WholeBeanValidator Maven / Gradle / Ivy

The newest version!
/*
 * 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 com.sun.faces.ext.component;

import static com.sun.faces.ext.component.MessageFactory.getLabel;
import static com.sun.faces.ext.component.MessageFactory.getMessage;
import static com.sun.faces.util.BeanValidation.getBeanValidator;
import static com.sun.faces.util.ReflectionUtils.setProperties;
import static com.sun.faces.util.Util.getValueExpressionNullSafe;
import static com.sun.faces.util.copier.CopierUtils.getCopier;
import static jakarta.faces.component.visit.VisitContext.createVisitContext;
import static jakarta.faces.component.visit.VisitResult.ACCEPT;
import static jakarta.faces.validator.BeanValidator.MESSAGE_ID;
import static jakarta.faces.validator.BeanValidator.VALIDATOR_ID;
import static java.util.Collections.emptyMap;

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;

import jakarta.el.ValueExpression;
import jakarta.el.ValueReference;
import jakarta.faces.FacesException;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.EditableValueHolder;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIForm;
import jakarta.faces.component.UIInput;
import jakarta.faces.component.visit.VisitCallback;
import jakarta.faces.component.visit.VisitContext;
import jakarta.faces.component.visit.VisitResult;
import jakarta.faces.context.FacesContext;
import jakarta.faces.validator.Validator;
import jakarta.faces.validator.ValidatorException;
import jakarta.validation.ConstraintViolation;

class WholeBeanValidator implements Validator {
    private static final Logger LOGGER = Logger.getLogger("jakarta.faces.validator", "jakarta.faces.LogStrings");

    private static final String ERROR_MISSING_FORM = "f:validateWholeBean must be nested directly in an UIForm.";

    static final String MULTI_FIELD_VALIDATION_CANDIDATES = VALIDATOR_ID + ".MULTI_FIELD_VALIDATION_CANDIDATES";

    /**
     * 

* Special value to indicate the proposed value for a property failed field-level validation. This prevents any attempt * to perform class level validation. *

*/ static final String FAILED_FIELD_LEVEL_VALIDATION = VALIDATOR_ID + ".FAILED_FIELD_LEVEL_VALIDATION"; @Override public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException { validate(context, (UIValidateWholeBean) component, value); } public void validate(FacesContext context, UIValidateWholeBean component, Object value) throws ValidatorException { // Get parent and check if the parent of this f:validateWholeBean is a form UIForm form = getParentForm(component); ValueExpression wholeBeanVE = getValueExpressionNullSafe(component, "value"); // The "whole" bean that we're going to validate at the class level Object wholeBean = wholeBeanVE.getValue(context.getELContext()); // A shortened or fully qualified class name, or EL expression pointing // to a type that copies the target bean for validation String copierType = (String) component.getAttributes().get("copierType"); // Inspect the status of field level validation if (hasAnyBeanPropertyFailedValidation(context, wholeBean)) { return; } AddRemainingCandidateFieldsCallback addRemainingCandidateFieldsCallback = new AddRemainingCandidateFieldsCallback(context, wholeBean); form.visitTree(createVisitContext(context), addRemainingCandidateFieldsCallback); Map> validationCandidate = addRemainingCandidateFieldsCallback.getCandidate(); if (validationCandidate.isEmpty()) { return; } // Perform the actual bean validation on a copy of the whole bean Set> violations = doBeanValidation(getBeanValidator(context), copyBeanAndPopulateWithCandidateValues(context, wholeBeanVE, wholeBean, copierType, validationCandidate), component.getValidationGroupsArray(), wholeBeanVE); // If there are any violations, transform them into a Faces validator exception 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); } // Mark the components as invalid to prevent them from receiving // values during updateModelValues for (Entry> validationCandidateEntry : validationCandidate.entrySet()) { invalidateComponent(validationCandidateEntry); } throw toThrow; } } private UIForm getParentForm(UIComponent component) { UIComponent parent = component.getParent(); if (!(parent instanceof UIForm)) { throw new IllegalArgumentException(ERROR_MISSING_FORM); } return (UIForm) parent; } private boolean isFailedFieldLevelValidation(Entry> wholeBeanPropertyEntry) { return FAILED_FIELD_LEVEL_VALIDATION.equals(wholeBeanPropertyEntry.getValue().get("value")); } private void invalidateComponent(Entry> wholeBeanPropertyEntry) { ((EditableValueHolder) wholeBeanPropertyEntry.getValue().get("component")).setValid(false); } private boolean hasAnyBeanPropertyFailedValidation(FacesContext context, Object wholeBean) { Map>> validationCandidates = getMultiFieldValidationCandidates(context, false); if (context.isValidationFailed()) { return true; } if (!validationCandidates.isEmpty() && validationCandidates.containsKey(wholeBean)) { // Verify that none of the field level properties failed validation for (Entry> wholeBeanPropertyEntry : validationCandidates.get(wholeBean).entrySet()) { if (isFailedFieldLevelValidation(wholeBeanPropertyEntry)) { return true; } } } return false; } private Object copyBeanAndPopulateWithCandidateValues(FacesContext context, ValueExpression wholeBeanVE, Object wholeBean, String copierType, Map> candidate) { // Populate the bean copy with the validated values from the candidate Map propertiesToSet = new HashMap<>(); for (Entry> propertyEntry : candidate.entrySet()) { propertiesToSet.put(propertyEntry.getKey(), propertyEntry.getValue().get("value")); } // Copy the whole bean so that class-level validation can be performed // without corrupting the real whole bean Object wholeBeanCopy = getCopier(context, copierType).copy(wholeBean); if (wholeBeanCopy == null) { throw new FacesException("Unable to copy bean from " + wholeBeanVE.getExpressionString()); } setProperties(wholeBeanCopy, propertiesToSet); return wholeBeanCopy; } private Set> doBeanValidation(jakarta.validation.Validator beanValidator, Object wholeBeanCopy, Class[] validationGroupArray, ValueExpression wholeBeanVE) { @SuppressWarnings("rawtypes") Set violationsRaw = null; try { violationsRaw = beanValidator.validate(wholeBeanCopy, validationGroupArray); } catch (IllegalArgumentException iae) { LOGGER.fine("Unable to validate expression " + wholeBeanVE.getExpressionString() + " using Bean Validation. Unable to get value of expression. " + " Message from Bean Validation: " + iae.getMessage()); } @SuppressWarnings("unchecked") Set> violations = violationsRaw; return violations; } private static class AddRemainingCandidateFieldsCallback implements VisitCallback { private final FacesContext context; private final Object base; private final Map> candidate = new HashMap<>(); public AddRemainingCandidateFieldsCallback(final FacesContext context, final Object base) { this.context = context; this.base = base; } final Map> getCandidate() { return candidate; } @Override public VisitResult visit(VisitContext visitContext, UIComponent component) { if (component instanceof EditableValueHolder && component.isRendered() && !(component instanceof UIValidateWholeBean)) { ValueExpression valueExpression = component.getValueExpression("value"); if (valueExpression != null) { ValueReference valueReference = valueExpression.getValueReference(context.getELContext()); if (valueReference != null && valueReference.getBase().equals(base)) { Map tuple = new HashMap<>(); tuple.put("component", component); tuple.put("value", getComponentValue(component)); candidate.put(valueReference.getProperty().toString(), tuple); } } } return ACCEPT; } private static Object getComponentValue(UIComponent component) { UIInput inputComponent = (UIInput) component; return inputComponent.getSubmittedValue() != null ? inputComponent.getSubmittedValue() : inputComponent.getLocalValue(); } } /* *

Returns a data structure that stores the information necessary to perform class-level * validation by <f:validateWholeBean > components elsewhere in the tree. The lifetime of this data * structure does not extend beyond the current {@code FacesContext}. The data structure must conform to the following * specification.

* *
* *
    * *
  • It is a non-thread-safe {@code Map}.

  • * *
  • Keys are CDI bean instances that are referenced by the {@code value} attribute of * <f:validateWholeBean > components.

  • * *
  • * *

    Values are {@code Map}s that represent the properties to be stored on the CDI bean instance that is the current * key. The inner {@code Map} must conform to the following specification.

    * *
      * *
    • It is a non-thread-safe {@code Map}.

    • * *
    • Keys are property names.

    • * *
    • Values are {@code Map} instances. In this innermost map, the following keys are supported.

      * *

      component: Object that is the EditableValueHolder

      value: Object that is the value of the property

      * *
    • * *
    * *
  • * * * *
* *
* * @param context the {@link FacesContext} for this request * * @param create if {@code true}, the data structure must be created if not present. If {@code false} the data structure * must not be created and {@code Collections.emptyMap()} must be returned. * * @return the data structure representing the multi-field validation candidates * * @since 2.3 */ static Map>> getMultiFieldValidationCandidates(FacesContext context, boolean create) { Map attrs = context.getAttributes(); @SuppressWarnings("unchecked") Map>> multiFieldValidationCandidates = (Map>>) attrs.get(MULTI_FIELD_VALIDATION_CANDIDATES); if (multiFieldValidationCandidates == null) { if (create) { multiFieldValidationCandidates = new HashMap<>(); attrs.put(MULTI_FIELD_VALIDATION_CANDIDATES, multiFieldValidationCandidates); } else { multiFieldValidationCandidates = emptyMap(); } } return multiFieldValidationCandidates; } }