All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jaxx.runtime.validator.BeanValidator Maven / Gradle / Ivy

There is a newer version: 3.0-alpha-6
Show newest version
/*
 * *##% 
 * JAXX Runtime
 * Copyright (C) 2008 - 2009 CodeLutin
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 *
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * .
 * ##%*
 */
package jaxx.runtime.validator;

import java.beans.EventSetDescriptor;
import org.apache.commons.beanutils.ConversionException;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuiton.util.ConverterUtil;

import java.beans.Introspector;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import javax.swing.event.EventListenerList;

/**
 *
 * A customized validator for a given bean.
 *
 * Note: The bean must be listenable on properyChange events (means
 * must have public addPropertychangeListener and removePropertyChangeListener methods).
 * 
 * @param  type of the bean to validate.
 * 
 * @author chemit
 */
public class BeanValidator {

    /** la nom de la propriété bean */
    static public final String BEAN_PROERTY = "bean";
    /** la nom de la propriété contextName */
    static public final String CONTEXT_NAME_PROPERTY = "contextName";
    /** la nom de l'état valid */
    static public final String VALID_PROERTY = "valid";
    /** la nom de l'état changed */
    static public final String CHANGED_PROERTY = "changed";
    /** to use log facility, just put in your code: log.info(\"...\"); */
    static protected final Log log = LogFactory.getLog(BeanValidator.class);
    /** the type of bean to watch */
    protected final Class beanClass;
    /** the validation named context (can be null) */
    protected String contextName;
    /** to chain to a prent validator */
    protected BeanValidator parentValidator;
    /** state to indicate that validator has changed since the last time bean was setted */
    protected boolean changed = false;
    /** state of the validator (is true if no errors of error scope is found) */
    protected boolean valid = true;
    /** bean to be watched */
    protected B bean = null;
    /** to add and remove PropertyChangeListener on watched beans */
    protected EventSetDescriptor beanEventDescriptor;
    /** list of fields watched by this validator */
    protected Set> fields;
    /** map of conversion errors detected by this validator */
    protected Map conversionErrors;
    /** xworks scope validator **/
    protected EnumMap> validators;
    /** listener that listens on bean modification */
    protected PropertyChangeListener l;
    /** delegate property change support */
    protected PropertyChangeSupport pcs;
    /** A list of event listeners for this validators */
    protected EventListenerList listenerList = new EventListenerList();

    public BeanValidator(Class beanClass, String contextName) {
        this.beanClass = beanClass;
        this.pcs = new PropertyChangeSupport(this);
        this.conversionErrors = new TreeMap();
        this.validators = new EnumMap>(BeanValidatorScope.class);

        setContextName(contextName);

        l = new PropertyChangeListener() {

            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                doValidate();
            }
        };
    }

    public Class getBeanClass() {
        return beanClass;
    }

    public BeanValidator getParentValidator() {
        return parentValidator;
    }

    public String getContextName() {
        return contextName;
    }

    public Set> getFields() {
        return fields;
    }

    public Set getScopes() {
        return new java.util.HashSet(validators.keySet());
    }

    /**
     * Retourne vrai si l'objet bean a ete modifie depuis le dernier
     * {@link #setBean}
     *
     * @return true if bean was modify since last {@link #setBean(Object)} invocation
     */
    public boolean isChanged() {
        return changed;
    }

    public boolean isValid() {
        return valid;
    }

    public B getBean() {
        return bean;
    }

    public BeanValidatorField getField(String fieldName) {
        for (BeanValidatorField field : fields) {
            if (fieldName.equals(field.getName())) {
                return field;
            }
        }
        return null;
    }

    public boolean hasErrors() {
        for (BeanValidatorField field : fields) {
            if (field.hasErrors()) {
                return true;
            }
        }
        return false;
    }

    public boolean hasWarnings() {
        for (BeanValidatorField field : fields) {
            if (field.hasWarnings()) {
                return true;
            }
        }
        return false;
    }

    public boolean hasInfos() {
        for (BeanValidatorField field : fields) {
            if (field.hasInfos()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Test a the validator contains the field given his name
     *
     * @param fieldName the name of the searched field
     * @return true if validator contaisn this field, false otherwise
     */
    public boolean containsField(String fieldName) {
        BeanValidatorField field = getField(fieldName);
        return field != null;
    }

    public boolean isValid(String fieldName) {
        BeanValidatorField field = getField(fieldName);
        if (field == null) {
            throw new IllegalArgumentException("could not find a validator field " + fieldName);
        }
        return field.isValid();
    }

    /**
     * Permet de force la remise a false de l'etat de changement du bean
     *
     * @param changed flag to force reset of property {@link #changed}
     */
    public void setChanged(boolean changed) {
        this.changed = changed;
        // force the property to be fired (never pass the older value)
        pcs.firePropertyChange(CHANGED_PROERTY, null, changed);
    }

    public void setValid(boolean valid) {
        this.valid = valid;
        // force the property to be fired (never pass the older value)
        pcs.firePropertyChange(VALID_PROERTY, null, valid);
    }

    public void setBean(B bean) {
        B oldBean = this.bean;
        if (log.isDebugEnabled()) {
            log.debug(this + " : " + bean);
        }

        // clean conversions of previous bean
        conversionErrors.clear();

        if (oldBean != null) {
            try {
                getBeanEventDescriptor(oldBean).getRemoveListenerMethod().invoke(oldBean, l);
            } catch (Exception eee) {
                log.info("Can't register as listener for bean " + beanClass + " for reason " + eee.getMessage(), eee);
            }
        }
        this.bean = bean;

        if (bean == null) {

            // remove all messages for all fields of the validator

            for (BeanValidatorField f : fields) {

                f.updateMessages(this, null, null);
            }

        } else {
            try {
                getBeanEventDescriptor(bean).getAddListenerMethod().invoke(bean, l);
            } catch (Exception eee) {
                log.info("Can't register as listener for bean " + beanClass + " for reason " + eee.getMessage(), eee);
            }
            validate();
        }
        setChanged(false);
        setValid(!hasErrors());
        pcs.firePropertyChange(BEAN_PROERTY, oldBean, bean);
    }

    public void setContextName(String contextName) {
        String oldValidationContextName = this.contextName;
        this.contextName = contextName;
        // changing contextName could change fields definition
        // so dettach bean, must rebuild the fields
        if (bean != null) {
            setBean(null);
        }
        // rebuild the fields
        initFields();
        pcs.firePropertyChange(CONTEXT_NAME_PROPERTY, oldValidationContextName, contextName);
    }

    public void setParentValidator(BeanValidator parentValidator) {
        this.parentValidator = parentValidator;
    }

    /**
     * Convert a value.
     * 

* If an error occurs, then add an error in validator. * * @param the type of conversion * @param fieldName the name of the bean property * @param value the value to convert * @param valueClass the type of converted value * @return the converted value, or null if conversion was not ok */ @SuppressWarnings({"unchecked"}) public T convert(String fieldName, String value, Class valueClass) { if (fieldName == null) { throw new IllegalArgumentException("fieldName can not be null"); } if (valueClass == null) { throw new IllegalArgumentException("valueClass can not be null"); } // on ne convertit pas si il y a un bean et que le resultat de la validation // pourra etre affiche quelque part if (!canValidate() || value == null) { return null; } // remove the previous conversion error for the field conversionErrors.remove(fieldName); T result; try { Converter converter = ConverterUtil.getConverter(valueClass); if (converter == null) { throw new RuntimeException("could not find converter for the type " + valueClass); } result = (T) converter.convert(valueClass, value); /* Why this test ? if (result != null && !value.equals(result.toString())) { conversionErrors.put(fieldName, "error.convertor." + Introspector.decapitalize(valueClass.getSimpleName())); result = null; validate(); }*/ } catch (ConversionException e) { // get conversionErrors.put(fieldName, "error.convertor." + Introspector.decapitalize(valueClass.getSimpleName())); result = null; validate(); } return result; } /** * Methode pour forcer la revalidation d'un bean en mettant a jour les etats * internes. * * La méthode appelle {@link #validate()} puis met à jour les etats internes * {@link #valid} et {@link #changed}. * * @since 1.5 */ public void doValidate() { validate(); setValid(!hasErrors()); setChanged(true); } /** * il faut eviter le code re-intrant (durant une validation, une autre est * demandee). Pour cela on fait la validation dans un thread, et tant * que la premiere validation n'est pas fini, on ne repond pas aux * solicitations. * Cette method est public pour permettre de force une validation par * programmation, ce qui est utile par exemple si le bean ne supporte * pas les {@link PropertyChangeListener} * * TODO la methode devra repasser en protected et on utilise la * methode {@link #doValidate()} car {@link #validate()} ne modifie pas * les etats internes et cela en rend son utilisation delicate (le * validateur entre dans un etat incoherent par rapport aux messages envoyés). */ public void validate() { // on ne valide que si il y a un bean et que le resultat de la validation // pourra etre affiche quelque part if (!canValidate()) { return; } for (BeanValidatorScope scope : validators.keySet()) { XWorkBeanValidator validator = validators.get(scope); Map> newMessages = validator.validate(bean); if (scope == BeanValidatorScope.ERROR) { // treate conversion errors // reinject them for (Entry entry : conversionErrors.entrySet()) { // remove from validation, errors occurs on this field List errors = newMessages.get(entry.getKey()); String conversionError = entry.getValue(); if (errors != null) { errors.clear(); errors.add(conversionError); } else { errors = java.util.Collections.singletonList(conversionError); if (newMessages == XWorkBeanValidator.EMPTY_RESULT) { newMessages = new java.util.HashMap>(); } // add the concrete conversion error newMessages.put(entry.getKey(), errors); } } } // for each field, update his list of messages for (BeanValidatorField field : fields) { List messagesForField = newMessages.get(field.getName()); if (field.getScopes().contains(scope)) { field.updateMessages(this, scope, messagesForField); } } } if (parentValidator != null) { // chained validation // the parent validator should not be changed from this validation boolean wasModified = parentValidator.isChanged(); parentValidator.doValidate(); if (!wasModified) { // push back old state parentValidator.setChanged(false); } } } @Override public String toString() { return super.toString() + ""; } public void addBeanValidatorListener(BeanValidatorListener listener) { listenerList.add(BeanValidatorListener.class, listener); } public void removeBeanValidatorListener(BeanValidatorListener listener) { listenerList.remove(BeanValidatorListener.class, listener); } public BeanValidatorListener[] getBeanValidatorListeners() { return listenerList.getListeners(BeanValidatorListener.class); } public void addPropertyChangeListener(PropertyChangeListener listener) { pcs.addPropertyChangeListener(listener); } public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { pcs.addPropertyChangeListener(propertyName, listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { pcs.removePropertyChangeListener(listener); } public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { pcs.removePropertyChangeListener(propertyName, listener); } /** @return true if validation is enabled, false otherwise. */ protected boolean canValidate() { return !(bean == null || fields.isEmpty()); } protected void fireFieldChanged(BeanValidatorField field, BeanValidatorScope scope, String[] toAdd, String[] toDelete) { BeanValidatorEvent evt = new BeanValidatorEvent(this, field, scope, toAdd, toDelete); for (BeanValidatorListener listener : listenerList.getListeners(BeanValidatorListener.class)) { listener.onFieldChanged(evt); } } protected synchronized void initFields() { Set detectedFieldNames = new java.util.HashSet(); EnumMap> tmp = new EnumMap>(BeanValidatorScope.class); Set> detectedFields = new java.util.HashSet>(); validators.clear(); for (BeanValidatorScope scope : BeanValidatorScope.values()) { String scopeContext = (contextName == null ? "" : contextName + "-") + scope.name().toLowerCase(); XWorkBeanValidator newValidator = new XWorkBeanValidator(beanClass, scopeContext, false); Set fieldNames = newValidator.getFieldNames(); if (log.isDebugEnabled()) { log.debug("detected validators for scope " + scopeContext + " : " + fieldNames); } if (!fieldNames.isEmpty()) { // fields detected in this validator, keep it validators.put(scope, newValidator); detectedFieldNames.addAll(fieldNames); tmp.put(scope, fieldNames); } } List scopes = new ArrayList(); for (String fieldName : detectedFieldNames) { scopes.clear(); // detect scopes for the field for (BeanValidatorScope scope : BeanValidatorScope.values()) { if (tmp.containsKey(scope) && tmp.get(scope).contains(fieldName)) { scopes.add(scope); } } BeanValidatorField f = new BeanValidatorField(beanClass, fieldName, scopes); detectedFields.add(f); } tmp.clear(); detectedFieldNames.clear(); this.fields = java.util.Collections.unmodifiableSet(detectedFields); } protected EventSetDescriptor getBeanEventDescriptor(B bean) { if (beanEventDescriptor == null) { // check that the bean is listenable, otherwise, can't use the validator on it this.beanEventDescriptor = BeanValidatorUtil.getPropertyChangeListenerDescriptor(bean.getClass()); } return beanEventDescriptor; } }