/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2016 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
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<Object> {
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<String, Map<String, Object>> validationCandidate = addRemainingCandidateFieldsCallback.getCandidate();
if (validationCandidate.isEmpty()) {
return;
}
// Perform the actual bean validation on a copy of the whole bean
Set<ConstraintViolation<?>> 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<FacesMessage> 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<String, Map<String, Object>> 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<String, Map<String, Object>> wholeBeanPropertyEntry) {
return FAILED_FIELD_LEVEL_VALIDATION.equals(wholeBeanPropertyEntry.getValue().get("value"));
}
private void invalidateComponent(Entry<String, Map<String, Object>> wholeBeanPropertyEntry) {
((EditableValueHolder) wholeBeanPropertyEntry.getValue().get("component")).setValid(false);
}
private boolean hasAnyBeanPropertyFailedValidation(FacesContext context, Object wholeBean) {
Map<Object, Map<String, Map<String, Object>>> 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<String, Map<String, Object>> wholeBeanPropertyEntry : validationCandidates.get(wholeBean).entrySet()) {
if (isFailedFieldLevelValidation(wholeBeanPropertyEntry)) {
return true;
}
}
}
return false;
}
private Object copyBeanAndPopulateWithCandidateValues(FacesContext context, ValueExpression wholeBeanVE,
Object wholeBean, String copierType, Map<String, Map<String, Object>> candidate) {
// Populate the bean copy with the validated values from the candidate
Map<String, Object> propertiesToSet = new HashMap<>();
for (Entry<String, Map<String, Object>> 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<ConstraintViolation<?>> 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<ConstraintViolation<?>> violations = violationsRaw;
return violations;
}
private static class AddRemainingCandidateFieldsCallback implements VisitCallback {
private final FacesContext context;
private final Object base;
private final Map<String, Map<String, Object>> candidate = new HashMap<>();
public AddRemainingCandidateFieldsCallback(final FacesContext context, final Object base) {
this.context = context;
this.base = base;
}
final Map<String, Map<String, Object>> 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<String, Object> 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();
}
}
}