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

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

There is a newer version: 4.1.1
Show newest version
/*
 * Copyright (c) 1997, 2018 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.ext.component.MultiFieldValidationUtils.FAILED_FIELD_LEVEL_VALIDATION;
import static com.sun.faces.ext.component.MultiFieldValidationUtils.getMultiFieldValidationCandidates;
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 javax.faces.component.visit.VisitContext.createVisitContext;
import static javax.faces.component.visit.VisitResult.ACCEPT;
import static javax.faces.validator.BeanValidator.MESSAGE_ID;

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 javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.application.FacesMessage;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIForm;
import javax.faces.component.UIInput;
import javax.faces.component.visit.VisitCallback;
import javax.faces.component.visit.VisitContext;
import javax.faces.component.visit.VisitResult;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
import javax.validation.ConstraintViolation;

class WholeBeanValidator implements Validator {

    private static final Logger LOGGER
            = Logger.getLogger("javax.faces.validator", "javax.faces.LogStrings");    
    
    private static final String ERROR_MISSING_FORM
            = "f:validateWholeBean must be nested directly in an UIForm.";
    
    @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 JSF 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(javax.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 = new ValueExpressionAnalyzer(valueExpression)
                                                            .getReference(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(), tuple);
                    }
                }
            }
            
            return ACCEPT;
        }
        
        private static Object getComponentValue(UIComponent component) {
            UIInput inputComponent = (UIInput) component;
            
            return inputComponent.getSubmittedValue() != null ? inputComponent.getSubmittedValue() : inputComponent.getLocalValue();
        }
    }

}