/*
* Copyright 2014 Matti Tahvonen.
*
* Licensed 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 org.vaadin.viritin.v7;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.validation.ConstraintViolation;
import javax.validation.MessageInterpolator;
import javax.validation.Validation;
import javax.validation.ValidationException;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.NotNull;
import javax.validation.groups.Default;
import javax.validation.metadata.ConstraintDescriptor;
import org.vaadin.viritin.v7.fields.EagerValidateable;
import com.vaadin.v7.data.Property;
import com.vaadin.v7.data.Validator;
import com.vaadin.v7.data.fieldgroup.BeanFieldGroup;
import com.vaadin.v7.event.FieldEvents;
import com.vaadin.v7.event.FieldEvents.TextChangeNotifier;
import com.vaadin.server.AbstractErrorMessage;
import com.vaadin.server.ErrorMessage;
import com.vaadin.server.UserError;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.v7.ui.AbstractField;
import com.vaadin.v7.ui.Field;
/**
* Enhanced version of basic BeanFieldGroup in Vaadin. Supports "eager
* validation" and some enhancements to bean validation support.
*
* @param <T> the type of the bean wrapped by this group
*/
public class MBeanFieldGroup<T> extends BeanFieldGroup<T> implements
Property.ValueChangeListener, FieldEvents.TextChangeListener {
private static final long serialVersionUID = 9027084784300479429L;
protected final Class<T> nonHiddenBeanType;
private boolean validateOnlyBoundFields = true;
private Set<ConstraintViolation<T>> jsr303beanLevelViolations;
private Set<Validator.InvalidValueException> beanLevelViolations;
/**
* Configures fields for some better defaults, like property fields
* annotated with NotNull to be "required" (kind of a special validator in
* Vaadin)
*/
public void configureMaddonDefaults() {
for (Object property : getBoundPropertyIds()) {
final Field<?> field = getField(property);
try {
// Make @NotNull annotated fields "required"
try {
java.lang.reflect.Field declaredField = findDeclaredField(
property, nonHiddenBeanType);
final NotNull notNullAnnotation = declaredField.
getAnnotation(
NotNull.class);
if (notNullAnnotation != null && !field.isReadOnly()) {
field.setRequired(true);
Locale locale = getLocale();
if (locale == null) {
locale = Locale.getDefault();
}
String msg = getJavaxBeanValidatorFactory()
.getMessageInterpolator().interpolate(
notNullAnnotation.message(),
new MessageInterpolator.Context() {
@Override
public ConstraintDescriptor<?> getConstraintDescriptor() {
return null;
}
@Override
public Object getValidatedValue() {
return null;
}
@Override
public <T> T unwrap(Class<T> type) {
return null;
}
}, locale);
getField(property).setRequiredError(msg);
}
} catch (NoSuchFieldException ex) {
Logger.getLogger(MBeanFieldGroup.class.getName()).
log(Level.FINE, null, ex);
} catch (SecurityException ex) {
Logger.getLogger(MBeanFieldGroup.class.getName()).
log(Level.SEVERE, null, ex);
}
} catch (Throwable e) {
if (e instanceof java.lang.ClassNotFoundException) {
Logger.getLogger(MBeanFieldGroup.class.getName()).
log(Level.FINE, "Validation API not available.");
}
}
}
}
protected java.lang.reflect.Field findDeclaredField(Object property,
Class<?> clazz) throws NoSuchFieldException, SecurityException {
try {
java.lang.reflect.Field declaredField = clazz.
getDeclaredField(property.
toString());
return declaredField;
} catch (NoSuchFieldException e) {
if (clazz.getSuperclass() == null) {
throw e;
} else {
return findDeclaredField(property, clazz.getSuperclass());
}
}
}
private final Set<String> fieldsWithInitiallyDisabledValidation = new HashSet<>();
public Set<String> getFieldsWithInitiallyDisabledValidation() {
return Collections.
unmodifiableSet(fieldsWithInitiallyDisabledValidation);
}
/**
* This method hides validation errors on a required fields until the field
* has been changed for the first time. Does pretty much the same as old
* Vaadin Form did with its validationVisibleOnCommit, but eagerly per
* field.
* <p>
* Fields that hide validation errors this way are available in
* getFieldsWithIntiallyDisabledValidation() so they can be emphasized in
* UI.
*/
public void hideInitialEmpyFieldValidationErrors() {
fieldsWithInitiallyDisabledValidation.clear();
for (Field<?> f : getFields()) {
if (f instanceof AbstractField) {
final AbstractField<?> abstractField = (AbstractField<?>) f;
if (abstractField.getErrorMessage() != null && abstractField.
isRequired() && abstractField.
isEmpty() && abstractField.isValidationVisible()) {
final String propertyId = getPropertyId(abstractField).
toString();
abstractField.setValidationVisible(false);
fieldsWithInitiallyDisabledValidation.add(propertyId);
}
}
}
}
/**
* @return constraint violations found in last top level JSR303 validation.
*/
public Set<ConstraintViolation<T>> getConstraintViolations() {
return jsr303beanLevelViolations;
}
/**
* @return constraint violations by MValidator's found in last top level
* validation .
*/
public Set<Validator.InvalidValueException> getBasicConstraintViolations() {
return beanLevelViolations;
}
/**
* A helper method that returns "bean level" validation errors, i.e. errors
* that are not tied to a specific property/field.
*
* @return error messages from "bean level validation"
*/
public Collection<String> getBeanLevelValidationErrors() {
Collection<String> errors = new ArrayList<>();
if (getConstraintViolations() != null) {
for (final ConstraintViolation<T> constraintViolation : getConstraintViolations()) {
final MessageInterpolator.Context context = new MessageInterpolator.Context() {
@Override
public ConstraintDescriptor<?> getConstraintDescriptor() {
return constraintViolation.getConstraintDescriptor();
}
@Override
public Object getValidatedValue() {
return constraintViolation.getInvalidValue();
}
@Override
public <T> T unwrap(Class<T> type) {
throw new ValidationException();
}
};
final String msg = getJavaxBeanValidatorFactory().getMessageInterpolator().interpolate(
constraintViolation.getMessageTemplate(),
context, getLocale());
errors.add(msg);
}
}
if (getBasicConstraintViolations() != null) {
for (Validator.InvalidValueException cv : getBasicConstraintViolations()) {
errors.add(cv.getMessage());
}
}
return errors;
}
// For JSR303 validation at class level
private static ValidatorFactory factory;
private transient javax.validation.Validator javaxBeanValidator;
private Class<?>[] validationGroups;
public Class<?>[] getValidationGroups() {
if (validationGroups == null) {
return new Class<?>[]{Default.class};
}
return validationGroups;
}
/**
* @param validationGroups the JSR 303 bean validation groups that should be
* used to validate the bean. Note, that groups currently only affect
* cross-field/bean-level validation.
*/
public void setValidationGroups(
Class<?>... validationGroups) {
this.validationGroups = validationGroups;
}
protected static ValidatorFactory getJavaxBeanValidatorFactory() {
if (factory == null) {
factory = Validation.buildDefaultValidatorFactory();
}
return factory;
}
protected boolean jsr303ValidateBean(T bean) {
try {
if (javaxBeanValidator == null) {
javaxBeanValidator = getJavaxBeanValidatorFactory().getValidator();
}
} catch (Throwable t) {
// This may happen without JSR303 validation framework
Logger.getLogger(getClass().getName()).fine(
"JSR303 validation failed");
return true;
}
boolean containsAtLeastOneBoundComponentWithError = false;
Set<ConstraintViolation<T>> constraintViolations = new HashSet<>(
javaxBeanValidator.validate(bean, getValidationGroups()));
if (constraintViolations.isEmpty()) {
return true;
}
Iterator<ConstraintViolation<T>> iterator = constraintViolations.
iterator();
while (iterator.hasNext()) {
ConstraintViolation<T> constraintViolation = iterator.next();
Class<? extends Annotation> annotationType = constraintViolation.
getConstraintDescriptor().getAnnotation().
annotationType();
AbstractComponent errortarget = validatorToErrorTarget.get(
annotationType);
if (errortarget != null) {
// user has declared a target component for this constraint
errortarget.setComponentError(new UserError(
constraintViolation.getMessage()));
iterator.remove();
containsAtLeastOneBoundComponentWithError = true;
}
// else leave as "bean level error"
}
this.jsr303beanLevelViolations = constraintViolations;
if (!containsAtLeastOneBoundComponentWithError && isValidateOnlyBoundFields()) {
return true;
}
return false;
}
private Locale getLocale() {
Field<?> firstField = getFields().iterator().next();
return firstField.getLocale();
}
public interface FieldGroupListener<T> extends Serializable {
public void onFieldGroupChange(MBeanFieldGroup<T> beanFieldGroup);
}
/**
* EXPERIMENTAL: The cross field validation support is still experimental
* and its API is likely to change.
* <p>
* A validator executed against the edited bean. Developer can do any
* validation within the validate method, but typically this type of
* validation are used for e.g. cross field validation which is not possible
* with BeanValidation support in Vaadin.
*
* @param <T> the bean type to be validated.
*/
public interface MValidator<T> extends Serializable {
/**
* @param value the bean to be validated
* @throws Validator.InvalidValueException if value is not valid
*/
public void validate(T value) throws Validator.InvalidValueException;
}
@Override
public void valueChange(Property.ValueChangeEvent event) {
if (event != null) {
Property property = event.getProperty();
if (property instanceof Field) {
Field<?> abstractField = (Field<?>) property;
Object propertyId = getPropertyId(abstractField);
if (propertyId != null) {
boolean wasHiddenValidation = fieldsWithInitiallyDisabledValidation.
remove(propertyId.toString());
if (wasHiddenValidation) {
if (abstractField instanceof AbstractField) {
AbstractField<?> abstractField1 = (AbstractField<?>) abstractField;
abstractField1.setValidationVisible(true);
}
}
} else {
Logger.getLogger(getClass().getName()).warning(
"Property id for field was not found.");
}
}
}
setBeanModified(true);
if (listener != null) {
listener.onFieldGroupChange(this);
}
}
private final LinkedHashMap<MValidator<T>, Collection<AbstractComponent>> mValidators = new LinkedHashMap<>();
/**
* EXPERIMENTAL: The cross field validation support is still experimental
* and its API is likely to change.
*
* @param validator a validator that validates the whole bean making cross
* field validation much simpler
* @param fields the ui fields that this validator affects and on which a
* possible error message is shown.
* @return this FieldGroup
*/
public MBeanFieldGroup<T> addValidator(MValidator<T> validator,
AbstractComponent... fields) {
mValidators.put(validator, Arrays.asList(fields));
return this;
}
public MBeanFieldGroup<T> removeValidator(MValidator<T> validator) {
mValidators.remove(validator);
return this;
}
/**
* Removes all MValidators added the MFieldGroup
*
* @return the instance
*/
public MBeanFieldGroup<T> clearValidators() {
mValidators.clear();
return this;
}
private final Map<ErrorMessage, AbstractComponent> mValidationErrors = new HashMap<>();
private final Map<Class<?>, AbstractComponent> validatorToErrorTarget = new HashMap<>();
/**
* Sets the "validation error target", the component on which validation
* errors are shown, for given validator type.
*
* @param validatorType the class of the validator whose errors should be
* targeted
* @param component the component on which the errors should be displayed on
* @return the MBeanFieldGroup instance
*/
public MBeanFieldGroup<T> setValidationErrorTarget(Class validatorType,
AbstractComponent component) {
validatorToErrorTarget.put(validatorType, component);
return this;
}
private void clearMValidationErrors() {
for (AbstractComponent value : mValidationErrors.values()) {
if (value != null) {
value.setComponentError(null);
}
}
mValidationErrors.clear();
for (AbstractComponent ac : validatorToErrorTarget.values()) {
ac.setComponentError(null);
}
}
public boolean isValidateOnlyBoundFields() {
return validateOnlyBoundFields;
}
/**
* Tells that only bound fields from the bean should be validated.
* By default, only bound bean properties are validated.
* If set to false, all bean properties will be validated.
*
* @param validateOnlyBoundFields true if only fields that are actually bound in
* form should be validate. True by default.
*/
public void setValidateOnlyBoundFields(boolean validateOnlyBoundFields) {
this.validateOnlyBoundFields = validateOnlyBoundFields;
}
@Override
public boolean isValid() {
if(validateAllProperties) {
return isValidAllProperties();
} else {
return isValidLegacy();
}
}
private boolean isValidAllProperties() {
// clear all MValidation errors
clearMValidationErrors();
jsr303beanLevelViolations = null;
beanLevelViolations = null;
// first check standard property level validators, but unlike in Vaadin
// core, check them all, don't stop for first error
boolean propertiesValid = true;
try {
for (Field<?> field : getFields()) {
field.validate();
}
} catch (Validator.InvalidValueException e) {
propertiesValid = false;
}
// then crossfield(/bean level) validators, execute them all although
// with per field validation Vaadin checks only until the first failed one
boolean ok = true;
for (MValidator<T> v : mValidators.keySet()) {
try {
v.validate(getItemDataSource().getBean());
} catch (Validator.InvalidValueException e) {
Collection<AbstractComponent> properties = mValidators.
get(v);
if (!properties.isEmpty()) {
for (AbstractComponent field : properties) {
final ErrorMessage em = AbstractErrorMessage.
getErrorMessageForException(e);
mValidationErrors.put(em, field);
field.setComponentError(em);
}
} else {
final ErrorMessage em = AbstractErrorMessage.
getErrorMessageForException(e);
AbstractComponent target = validatorToErrorTarget.get(v.
getClass());
if (target != null) {
target.setComponentError(em);
} else {
// no specific "target component" for validation error
// leave as bean level error
if (beanLevelViolations == null) {
beanLevelViolations = new HashSet<>();
}
beanLevelViolations.add(e);
mValidationErrors.put(em, null);
}
}
ok = false;
}
}
return jsr303ValidateBean(getItemDataSource().getBean()) && ok && propertiesValid;
}
private boolean isValidLegacy() {
// clear all MValidation errors
clearMValidationErrors();
jsr303beanLevelViolations = null;
beanLevelViolations = null;
// first check standard property level validators
final boolean propertiesValid = super.isValid();
// then crossfield(/bean level) validators, execute them all although
// with per field validation Vaadin checks only until the first failed one
if (propertiesValid) {
boolean ok = true;
for (MValidator<T> v : mValidators.keySet()) {
try {
v.validate(getItemDataSource().getBean());
} catch (Validator.InvalidValueException e) {
Collection<AbstractComponent> properties = mValidators.
get(v);
if (!properties.isEmpty()) {
for (AbstractComponent field : properties) {
final ErrorMessage em = AbstractErrorMessage.
getErrorMessageForException(e);
mValidationErrors.put(em, field);
field.setComponentError(em);
}
} else {
final ErrorMessage em = AbstractErrorMessage.
getErrorMessageForException(e);
AbstractComponent target = validatorToErrorTarget.get(v.
getClass());
if (target != null) {
target.setComponentError(em);
} else {
// no specific "target component" for validation error
// leave as bean level error
if (beanLevelViolations == null) {
beanLevelViolations = new HashSet<>();
}
beanLevelViolations.add(e);
mValidationErrors.put(em, null);
}
}
ok = false;
}
}
return jsr303ValidateBean(getItemDataSource().getBean()) && ok;
}
return false;
}
private boolean validateAllProperties = true;
/**
* @param validateAllProperties true if all properties should be validated,
* instead of stopping for the first invalid field (the default in Vaadin)
*/
public void setValidateAllProperties(boolean validateAllProperties) {
this.validateAllProperties = validateAllProperties;
}
@Override
public void textChange(FieldEvents.TextChangeEvent event) {
valueChange(null);
}
private boolean beanModified = false;
private FieldGroupListener<T> listener;
public void setBeanModified(boolean beanModified) {
this.beanModified = beanModified;
}
public boolean isBeanModified() {
return beanModified;
}
@Override
public boolean isModified() {
return super.isModified();
}
public MBeanFieldGroup(Class<T> beanType) {
super(beanType);
this.nonHiddenBeanType = beanType;
setBuffered(false);
}
public MBeanFieldGroup<T> withEagerValidation() {
return withEagerValidation(new FieldGroupListener<T>() {
private static final long serialVersionUID = 2706724523369882782L;
@Override
public void onFieldGroupChange(MBeanFieldGroup<T> beanFieldGroup) {
}
});
}
/**
* Makes all fields "immediate" to trigger eager validation
*
* @param listener a listener that will be notified when a field in the
* group has been modified
* @return the MBeanFieldGroup that can be used for further modifications or
* e.g. commit if buffered
*/
public MBeanFieldGroup<T> withEagerValidation(FieldGroupListener<T> listener) {
this.listener = listener;
for (Field<?> field : getFields()) {
// ((AbstractComponent) field).setImmediate(true);
field.addValueChangeListener(this);
if (field instanceof EagerValidateable) {
EagerValidateable ev = (EagerValidateable) field;
ev.setEagerValidation(true);
}
if (field instanceof TextChangeNotifier) {
final TextChangeNotifier abstractTextField = (TextChangeNotifier) field;
abstractTextField.addTextChangeListener(this);
}
}
return this;
}
/**
* Removes all listeners from the bound fields and unbinds properties.
*/
public void unbind() {
// wrap in array list to avoid CME
for (Field<?> field : new ArrayList<>(getFields())) {
field.removeValueChangeListener(this);
if (field instanceof TextChangeNotifier) {
final TextChangeNotifier abstractTextField = (TextChangeNotifier) field;
abstractTextField.removeTextChangeListener(this);
}
unbind(field);
}
fieldsWithInitiallyDisabledValidation.clear();
}
/**
* Viritin does not support buffering. Use at own risk, using this method
* might cause odd issues with certain features.
* @see <a href="https://github.com/viritin/viritin/issues/186">Issue 186</a>
* @throws com.vaadin.v7.data.fieldgroup.FieldGroup.CommitException if commit fails
* @deprecated
*/
@Override
@Deprecated
public void commit() throws CommitException {
Logger.getLogger(getClass().getName()).
log(Level.WARNING, "Viritin doesn't support buffering, this "
+ "method might not work as expected.");
super.commit();
}
/**
* Viritin does not support buffering. Use at own risk, using this method
* might cause odd issues with certain features.
* @see <a href="https://github.com/viritin/viritin/issues/186">Issue 186</a>
* @deprecated
*/
@Override
@Deprecated
public void discard() {
Logger.getLogger(getClass().getName()).
log(Level.WARNING, "Viritin doesn't support buffering, this "
+ "method might not work as expected.");
super.discard();
}
/**
* Viritin does not support buffering. Use at own risk, using this method
* might cause odd issues with certain features.
* @see <a href="https://github.com/viritin/viritin/issues/186">Issue 186</a>
* @deprecated
*/
@Override
@Deprecated
public void setBuffered(boolean buffered) {
if (buffered == true) {
Logger.getLogger(getClass().getName()).
log(Level.WARNING, "Viritin doesn't support buffering, this "
+ "method might not work as expected.");
}
super.setBuffered(buffered);
}
/**
* Configures a field with the settings set for this FieldBinder.
* <p>
* By default this updates the buffered, read only and enabled state of the
* field. Also adds validators when applicable. Fields with read only data
* source are always configured as read only.
* <p>
* Unlike the default implementation in FieldGroup, MBeanFieldGroup only
* makes field read only based on the property's hint, not the opposite.
* This way developer can in form code choose to make some fields read only.
*
* @param field The field to update
*/
@Override
protected void configureField(Field<?> field) {
boolean readOnlyStatus = isReadOnly() || field.getPropertyDataSource().isReadOnly();
super.configureField(field);
// reset the badly set readOnlyStatus
field.setReadOnly(readOnlyStatus);
}
private static final String NO_BUFFERING_SUPPORT = "Buffering is not supported by Viritin. "
+ "Please, see https://github.com/viritin/viritin/issues/186 for details.";
}