blob: 40a9461898d3e36d03c3bc8fb032a0f157bc2ef3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 javax.faces.validator;
import org.apache.myfaces.core.api.shared.MessageUtils;
import org.apache.myfaces.core.api.shared.ExternalSpecifications;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.LinkedHashMap;
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 javax.el.ELContext;
import javax.el.ELResolver;
import javax.el.FunctionMapper;
import javax.el.ValueExpression;
import javax.el.ValueReference;
import javax.el.VariableMapper;
import javax.faces.FacesException;
import javax.faces.application.FacesMessage;
import javax.faces.component.PartialStateHolder;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.el.CompositeComponentExpressionHolder;
import javax.validation.ConstraintViolation;
import javax.validation.MessageInterpolator;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import javax.validation.groups.Default;
import javax.validation.metadata.BeanDescriptor;
import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFJspProperty;
import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFProperty;
import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFValidator;
import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFWebConfigParam;
/**
* <p>
* <strong>BeanValidator</strong> is a {@link javax.faces.validator.Validator}
* that doesn't do any validation itself, but delegates validation logic to
* Bean Validation.
* </p>
*
* @since 2.0
*/
@JSFValidator(
name="f:validateBean",
bodyContent="empty")
@JSFJspProperty(
name = "binding",
returnType = "javax.faces.validator.BeanValidator",
longDesc = "A ValueExpression that evaluates to a BeanValidator.")
public class BeanValidator implements Validator, PartialStateHolder
{
private static final Logger log = Logger.getLogger(BeanValidator.class.getName());
/**
* Converter ID, as defined by the JSF 2.0 specification.
*/
public static final String VALIDATOR_ID = "javax.faces.Bean";
/**
* The message ID for this Validator in the message bundles.
*/
public static final String MESSAGE_ID = "javax.faces.validator.BeanValidator.MESSAGE";
/**
* If this init parameter is present, no Bean Validators should be added to an UIInput by default.
* Explicitly adding a BeanValidator to an UIInput is possible though.
*/
@JSFWebConfigParam(defaultValue="true", expectedValues="true, false", since="2.0", group="validation")
public static final String DISABLE_DEFAULT_BEAN_VALIDATOR_PARAM_NAME
= "javax.faces.validator.DISABLE_DEFAULT_BEAN_VALIDATOR";
/**
* The key in the ServletContext where the Bean Validation Factory can be found.
* In a managed Java EE 6 environment, the container initializes the ValidatorFactory
* and stores it in the ServletContext under this key.
* If not present, the manually instantiated ValidatorFactory is stored in the ServletContext
* under this key for caching purposes.
*/
public static final String VALIDATOR_FACTORY_KEY = "javax.faces.validator.beanValidator.ValidatorFactory";
/**
* This is used as a separator so multiple validation groups can be specified in one String.
*/
public static final String VALIDATION_GROUPS_DELIMITER = ",";
/**
* This regular expression is used to match for empty validation groups.
* Currently, a string containing only whitespace is classified as empty.
*/
public static final String EMPTY_VALIDATION_GROUPS_PATTERN = "^[\\W,]*$";
/**
* Enable f:validateWholeBean use.
*/
@JSFWebConfigParam(since="2.3", defaultValue = "false", expectedValues = "true, false", group="validation")
public static final String ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME =
"javax.faces.validator.ENABLE_VALIDATE_WHOLE_BEAN";
private static final Class<?>[] DEFAULT_VALIDATION_GROUPS_ARRAY = new Class<?>[] { Default.class };
private static final String DEFAULT_VALIDATION_GROUP_NAME = "javax.validation.groups.Default";
private static final String CANDIDATE_COMPONENT_VALUES_MAP = "oam.WBV.candidatesMap";
private static final String BEAN_VALIDATION_FAILED = "oam.WBV.validationFailed";
private String validationGroups;
private Class<?>[] validationGroupsArray;
private boolean isTransient = false;
private boolean _initialStateMarked = false;
/**
* {@inheritDoc}
*/
@Override
public void validate(final FacesContext context, final UIComponent component, final Object value)
{
if (context == null)
{
throw new NullPointerException("context");
}
if (component == null)
{
throw new NullPointerException("component");
}
ValueExpression valueExpression = component.getValueExpression("value");
if (valueExpression == null)
{
log.warning("cannot validate component with empty value: "
+ component.getClientId(context));
return;
}
// Obtain a reference to the to-be-validated object and the property name.
ValueReference reference = getValueReference(valueExpression, context);
if (reference == null)
{
return;
}
Object base = reference.getBase();
if (base == null)
{
return;
}
Class<?> valueBaseClass = base.getClass();
if (valueBaseClass == null)
{
return;
}
Object referenceProperty = reference.getProperty();
if (!(referenceProperty instanceof String))
{
// if the property is not a String, the ValueReference does not
// point to a bean method, but e.g. to a value in a Map, thus we
// can exit bean validation here
return;
}
String valueProperty = (String) referenceProperty;
// Initialize Bean Validation.
ValidatorFactory validatorFactory = createValidatorFactory(context);
javax.validation.Validator validator = createValidator(validatorFactory, context);
BeanDescriptor beanDescriptor = validator.getConstraintsForClass(valueBaseClass);
if (!beanDescriptor.isBeanConstrained())
{
return;
}
// Note that validationGroupsArray was initialized when createValidator was called
Class[] validationGroupsArray = this.validationGroupsArray;
// JSF 2.3: If the ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME application parameter is enabled and this Validator
// instance has validation groups other than or in addition to the Default group
boolean containsOtherValidationGroup = false;
if (validationGroupsArray != null && validationGroupsArray.length > 0)
{
for (Class<?> clazz : validationGroupsArray)
{
if (!Default.class.equals(clazz))
{
containsOtherValidationGroup = true;
break;
}
}
}
// Delegate to Bean Validation.
Set constraintViolations = validator.validateValue(valueBaseClass, valueProperty, value, validationGroupsArray);
if (!constraintViolations.isEmpty())
{
Set<FacesMessage> messages = new LinkedHashSet<>(constraintViolations.size());
for (Object violation : constraintViolations)
{
ConstraintViolation constraintViolation = (ConstraintViolation) violation;
String message = constraintViolation.getMessage();
Object[] args = new Object[]{ message, MessageUtils.getLabel(context, component) };
FacesMessage msg = MessageUtils.getErrorMessage(context, MESSAGE_ID, args);
messages.add(msg);
}
if (isValidateWholeBeanEnabled(context) && containsOtherValidationGroup)
{
// JSF 2.3: 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
// ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME is set, throw the new exception.
context.getViewRoot().getTransientStateHelper().putTransient(BEAN_VALIDATION_FAILED, Boolean.TRUE);
}
throw new ValidatorException(messages);
}
else
{
// JSF 2.3: If the returned Set is empty, the ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME application parameter
// is enabled and this Validator instance has validation groups other than or in addition to the
// Default group
if (isValidateWholeBeanEnabled(context) && containsOtherValidationGroup)
{
// 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.
Map<String, Object> candidatesMap = (Map<String, Object>) context.getViewRoot()
.getTransientStateHelper().getTransient(CANDIDATE_COMPONENT_VALUES_MAP);
if (candidatesMap == null)
{
candidatesMap = new LinkedHashMap<>();
context.getViewRoot().getTransientStateHelper().putTransient(
CANDIDATE_COMPONENT_VALUES_MAP, candidatesMap);
}
candidatesMap.put(component.getClientId(context), value);
}
}
}
private boolean isValidateWholeBeanEnabled(FacesContext facesContext)
{
Boolean value = (Boolean) facesContext.getAttributes().get(ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME);
if (value == null)
{
String enabled = facesContext.getExternalContext().getInitParameter(ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME);
if (enabled == null)
{
value = Boolean.FALSE;
}
else
{
value = Boolean.valueOf(enabled);
}
}
return Boolean.TRUE.equals(value);
}
private javax.validation.Validator createValidator(final ValidatorFactory validatorFactory, FacesContext context)
{
// Set default validation group when setValidationGroups has not been called.
// The null check is there to prevent it from happening twice.
if (validationGroupsArray == null)
{
postSetValidationGroups();
}
return validatorFactory //
.usingContext() //
.messageInterpolator(new FacesMessageInterpolator(
validatorFactory.getMessageInterpolator(), context)) //
.getValidator();
}
/**
* Get the ValueReference from the ValueExpression.
*
* @param valueExpression The ValueExpression for value.
* @param context The FacesContext.
* @return A ValueReferenceWrapper with the necessary information about the ValueReference.
*/
private ValueReference getValueReference(
final ValueExpression valueExpression, final FacesContext context)
{
ELContext elCtx = context.getELContext();
return _ValueReferenceResolver.resolve(valueExpression, elCtx);
}
/**
* This method creates ValidatorFactory instances or retrieves them from the container.
*
* Once created, ValidatorFactory instances are stored in the container under the key
* VALIDATOR_FACTORY_KEY for performance.
*
* @param context The FacesContext.
* @return The ValidatorFactory instance.
* @throws FacesException if no ValidatorFactory can be obtained because: a) the
* container is not a Servlet container or b) because Bean Validation is not available.
*/
private ValidatorFactory createValidatorFactory(FacesContext context)
{
Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
Object attr = applicationMap.get(VALIDATOR_FACTORY_KEY);
if (attr instanceof ValidatorFactory)
{
return (ValidatorFactory) attr;
}
else
{
synchronized (this)
{
if (ExternalSpecifications.isBeanValidationAvailable())
{
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
applicationMap.put(VALIDATOR_FACTORY_KEY, factory);
return factory;
}
else
{
throw new FacesException("Bean Validation is not present");
}
}
}
}
/**
* Fully initialize the validation groups if needed.
* If no validation groups are specified, the Default validation group is used.
*/
private void postSetValidationGroups()
{
if (this.validationGroups == null || this.validationGroups.matches(EMPTY_VALIDATION_GROUPS_PATTERN))
{
this.validationGroupsArray = DEFAULT_VALIDATION_GROUPS_ARRAY;
}
else
{
String[] classes = this.validationGroups.split(VALIDATION_GROUPS_DELIMITER);
List<Class<?>> validationGroupsList = new ArrayList<>(classes.length);
for (String clazz : classes)
{
clazz = clazz.trim();
if (!clazz.isEmpty())
{
Class<?> theClass = null;
ClassLoader cl = null;
if (System.getSecurityManager() != null)
{
try
{
cl = (ClassLoader) AccessController.doPrivileged(
(PrivilegedExceptionAction) () -> Thread.currentThread().getContextClassLoader());
}
catch (PrivilegedActionException pae)
{
throw new FacesException(pae);
}
}
else
{
cl = Thread.currentThread().getContextClassLoader();
}
try
{
// Try WebApp ClassLoader first
theClass = Class.forName(clazz,false,cl);
}
catch (ClassNotFoundException ignore)
{
try
{
// fallback: Try ClassLoader for BeanValidator (i.e. the myfaces.jar lib)
theClass = Class.forName(clazz,false, BeanValidator.class.getClassLoader());
}
catch (ClassNotFoundException e)
{
throw new RuntimeException("Could not load validation group", e);
}
}
// the class was found
validationGroupsList.add(theClass);
}
}
this.validationGroupsArray = validationGroupsList.toArray(new Class[validationGroupsList.size()]);
}
}
/** {@inheritDoc} */
@Override
public Object saveState(final FacesContext context)
{
if (context == null)
{
throw new NullPointerException("context");
}
if (!initialStateMarked())
{
//Full state saving.
return this.validationGroups;
}
else if (DEFAULT_VALIDATION_GROUP_NAME.equals(this.validationGroups))
{
// default validation groups can be saved as null.
return null;
}
else
{
// Save it fully. Remember that by MYFACES-2528
// validationGroups needs to be stored into the state
// because this value is often susceptible to use in "combo"
return this.validationGroups;
}
}
/** {@inheritDoc} */
@Override
public void restoreState(final FacesContext context, final Object state)
{
if (context == null)
{
throw new NullPointerException("context");
}
if (state != null)
{
this.validationGroups = (String) state;
}
else
{
// When the value is being validated, postSetValidationGroups() sets
// validationGroups to javax.validation.groups.Default.
this.validationGroups = null;
}
// Only the String is saved, recalculate the Class[] on state restoration.
//postSetValidationGroups();
}
/**
* Get the Bean Validation validation groups.
* @return The validation groups String.
*/
@JSFProperty
public String getValidationGroups()
{
return validationGroups;
}
/**
* Set the Bean Validation validation groups.
* @param validationGroups The validation groups String, separated by
* {@link BeanValidator#VALIDATION_GROUPS_DELIMITER}.
*/
public void setValidationGroups(final String validationGroups)
{
this.validationGroups = validationGroups;
this.clearInitialState();
//postSetValidationGroups();
}
@JSFProperty
@SuppressWarnings("unused")
private Boolean isDisabled()
{
return null;
}
@JSFProperty
@SuppressWarnings("unused")
private String getFor()
{
return null;
}
/** {@inheritDoc} */
@Override
public boolean isTransient()
{
return isTransient;
}
/** {@inheritDoc} */
@Override
public void setTransient(final boolean isTransient)
{
this.isTransient = isTransient;
}
/** {@inheritDoc} */
@Override
public void clearInitialState()
{
_initialStateMarked = false;
}
/** {@inheritDoc} */
@Override
public boolean initialStateMarked()
{
return _initialStateMarked;
}
/** {@inheritDoc} */
@Override
public void markInitialState()
{
_initialStateMarked = true;
}
/**
* Note: Before 2.1.5/2.0.11 there was another strategy for this point to minimize
* the instances used, but after checking this with a profiler, it is more expensive to
* call FacesContext.getCurrentInstance() than create this object for bean validation.
*
* Standard MessageInterpolator, as described in the JSR-314 spec.
*/
private static class FacesMessageInterpolator implements MessageInterpolator
{
private final FacesContext facesContext;
private final MessageInterpolator interpolator;
public FacesMessageInterpolator(final MessageInterpolator interpolator, final FacesContext facesContext)
{
this.interpolator = interpolator;
this.facesContext = facesContext;
}
@Override
public String interpolate(final String s, final Context context)
{
Locale locale = facesContext.getViewRoot().getLocale();
return interpolator.interpolate(s, context, locale);
}
@Override
public String interpolate(final String s, final Context context, final Locale locale)
{
return interpolator.interpolate(s, context, locale);
}
}
}
final class _ValueReferenceResolver
{
/**
* This method can be used to extract the ValueReference from the given ValueExpression.
*
* @param valueExpression The ValueExpression to resolve.
* @param elCtx The ELContext, needed to parse and execute the expression.
* @return The ValueReferenceWrapper.
*/
public static ValueReference resolve(ValueExpression valueExpression, final ELContext elCtx)
{
ValueReference valueReference = valueExpression.getValueReference(elCtx);
while (valueReference != null && valueReference.getBase() instanceof CompositeComponentExpressionHolder)
{
valueExpression = ((CompositeComponentExpressionHolder) valueReference.getBase())
.getExpression((String) valueReference.getProperty());
if(valueExpression == null)
{
break;
}
valueReference = valueExpression.getValueReference(elCtx);
}
return valueReference;
}
}
/**
* This ELContext is used to hook into the EL handling, by decorating the
* ELResolver chain with a custom ELResolver.
*/
final class _ELContextDecorator extends ELContext
{
private final ELContext ctx;
private final ELResolver interceptingResolver;
/**
* Only used by ValueExpressionResolver.
*
* @param elContext The standard ELContext. All method calls, except getELResolver, are delegated to it.
* @param interceptingResolver The ELResolver to be returned by getELResolver.
*/
_ELContextDecorator(final ELContext elContext, final ELResolver interceptingResolver)
{
this.ctx = elContext;
this.interceptingResolver = interceptingResolver;
}
/**
* This is the important one, it returns the passed ELResolver.
* @return The ELResolver passed into the constructor.
*/
@Override
public ELResolver getELResolver()
{
return interceptingResolver;
}
// ############################ Standard delegating implementations ############################
@Override
public FunctionMapper getFunctionMapper()
{
return ctx.getFunctionMapper();
}
@Override
public VariableMapper getVariableMapper()
{
return ctx.getVariableMapper();
}
@Override
public void setPropertyResolved(final boolean resolved)
{
ctx.setPropertyResolved(resolved);
}
@Override
public boolean isPropertyResolved()
{
return ctx.isPropertyResolved();
}
@Override
public void putContext(final Class key, Object contextObject)
{
ctx.putContext(key, contextObject);
}
@Override
public Object getContext(final Class key)
{
return ctx.getContext(key);
}
@Override
public Locale getLocale()
{
return ctx.getLocale();
}
@Override
public void setLocale(final Locale locale)
{
ctx.setLocale(locale);
}
}