org.vaadin.viritin.v7.MBeanFieldGroup.java Source code

Java tutorial

Introduction

Here is the source code for org.vaadin.viritin.v7.MBeanFieldGroup.java

Source

/*
 * 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.";

}