blob: be35f360e1416d4efc9cf6e802c5dbd3be5b8d1e [file] [log] [blame]
package org.apache.wicket.bean.validation;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import jakarta.validation.groups.Default;
import jakarta.validation.metadata.ConstraintDescriptor;
import org.apache.wicket.Component;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.validation.INullAcceptingValidator;
import org.apache.wicket.validation.IValidatable;
/**
* Validator that delegates to the bean validation framework. The integration has to be first
* configured using {@link BeanValidationConfiguration}.
*
* <p>
* The validator must be provided a {@link Property}, unless one can be resolved from the component
* implicitly. By default the configuration contains the {@link DefaultPropertyResolver} so
* {@link PropertyModel}s are supported out of the box - when attached to a component with a
* property model the property does not need to be specified explicitly.
* </p>
*
* <p>
* The validator will set the required flag on the form component it is attached to based on the
* presence of the @NotNull annotation. Notice, the required flag will only be set to {@code true},
* components with the required flag already set to {@code true} will not have the flag set to
* {@code false} by this validator.
* </p>
*
* <p>
* The validator will allow {@link ITagModifier}s configured in {@link BeanValidationConfiguration}
* to mutate the markup tag of the component it is attached to.
* </p>
*
* <p>
* The validator specifies default error messages in the {@code PropertyValidator.properties} file.
* These values can be overridden in the application subclass' property files globally or in the
* page or panel properties locally. See this file for the default messages supported.
* </p>
*
* @author igor
*
* @param <T>
*/
public class PropertyValidator<T> extends Behavior implements INullAcceptingValidator<T>
{
private static final Class<?>[] EMPTY = new Class<?>[0];
private FormComponent<T> component;
// the trailing underscore means that these members should not be used
// directly. ALWAYS use the respective getter instead.
private Property property_;
private final IModel<Class<?>[]> groups_;
/**
* A flag indicating whether the component has been configured at least once.
*/
private boolean requiredFlagSet;
public PropertyValidator(Class<?>... groups)
{
this(null, groups);
}
public PropertyValidator(IModel<Class<?>[]> groups)
{
this(null, groups);
}
public PropertyValidator(Property property, Class<?>... groups)
{
this(property, new GroupsModel(groups));
}
public PropertyValidator(Property property, IModel<Class<?>[]> groups)
{
this.property_ = property;
this.groups_ = groups;
}
/**
* To support debugging, trying to provide useful information where possible
*
* @return
*/
private String createUnresolvablePropertyMessage(FormComponent<T> component)
{
String baseMessage = "Could not resolve Bean Property from component: " + component
+ ". (Hints:) Possible causes are a typo in the PropertyExpression, a null reference or a model that does not work in combination with a "
+ IPropertyResolver.class.getSimpleName() + ".";
IModel<?> model = ValidationModelResolver.resolvePropertyModelFrom(component);
if (model != null)
{
baseMessage += " Model : " + model;
}
return baseMessage;
}
private Property getProperty()
{
if (property_ == null)
{
BeanValidationContext config = BeanValidationConfiguration.get();
property_ = config.resolveProperty(component);
if (property_ == null)
{
throw new IllegalStateException(createUnresolvablePropertyMessage(component));
}
}
return property_;
}
private Class<?>[] getGroups()
{
if (groups_ == null)
{
return EMPTY;
}
return groups_.getObject();
}
@SuppressWarnings("unchecked")
@Override
public void bind(Component component)
{
if (this.component != null)
{
throw new IllegalStateException( //
"This validator has already been added to component: " + this.component
+ ". This validator does not support reusing instances, please create a new one");
}
if (!(component instanceof FormComponent))
{
throw new IllegalStateException(
getClass().getSimpleName() + " can only be added to FormComponents");
}
// TODO add a validation key that appends the type so we can have
// different messages for
// @Size on String vs Collection - done but need to add a key for each
// superclass/interface
this.component = (FormComponent<T>)component;
}
@Override
public void onConfigure(Component component)
{
super.onConfigure(component);
if (requiredFlagSet == false)
{
// "Required" flag is calculated upon component's model property, so
// we must ensure,
// that model object is accessible (i.e. component is already added
// in a page).
requiredFlagSet = true;
if (isRequired())
{
this.component.setRequired(true);
}
}
}
@Override
public void detach(Component component)
{
super.detach(component);
if (groups_ != null)
{
groups_.detach();
}
}
/**
* Should this property make the owning component required.
*
* @return <code>true</code> if required
*
* @see BeanValidationContext#isRequiredConstraint(ConstraintDescriptor)
*/
protected boolean isRequired()
{
BeanValidationContext config = BeanValidationConfiguration.get();
HashSet<Class<?>> groups = new HashSet<Class<?>>(Arrays.asList(getGroups()));
Iterator<ConstraintDescriptor<?>> it = new ConstraintIterator(config.getValidator(), getProperty());
while (it.hasNext())
{
ConstraintDescriptor<?> constraint = it.next();
if (config.isRequiredConstraint(constraint))
{
if (canApplyToDefaultGroup(constraint) && groups.size() == 0)
{
return true;
}
for (Class<?> constraintGroup : constraint.getGroups())
{
if (groups.contains(constraintGroup))
{
return true;
}
}
}
}
return false;
}
private boolean canApplyToDefaultGroup(ConstraintDescriptor<?> constraint)
{
Set<Class<?>> groups = constraint.getGroups();
//the constraint can be applied to default group either if its group array is empty
//or if it contains jakarta.validation.groups.Default
return groups.size() == 0 || groups.contains(Default.class);
}
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public void onComponentTag(Component component, ComponentTag tag)
{
super.onComponentTag(component, tag);
BeanValidationContext config = BeanValidationConfiguration.get();
Validator validator = config.getValidator();
Property property = getProperty();
// find any tag modifiers that apply to the constraints of the property
// being validated
// and allow them to modify the component tag
Iterator<ConstraintDescriptor<?>> it = new ConstraintIterator(validator, property,
getGroups());
while (it.hasNext())
{
ConstraintDescriptor<?> desc = it.next();
ITagModifier modifier = config.getTagModifier(desc.getAnnotation().annotationType());
if (modifier != null)
{
modifier.modify((FormComponent<?>)component, tag, desc.getAnnotation());
}
}
}
@SuppressWarnings("unchecked")
@Override
public void validate(IValidatable<T> validatable)
{
BeanValidationContext config = BeanValidationConfiguration.get();
Validator validator = config.getValidator();
Property property = getProperty();
// validate the value using the bean validator
Set<?> violations = validator.validateValue(property.getOwner(), property.getName(),
validatable.getValue(), getGroups());
// iterate over violations and report them
for (ConstraintViolation<?> violation : (Set<ConstraintViolation<?>>)violations)
{
validatable.error(config.getViolationTranslator().convert(violation));
}
}
}