Author: tchemit Date: 2011-01-23 23:00:53 +0100 (Sun, 23 Jan 2011) New Revision: 2034 Url: http://nuiton.org/repositories/revision/nuiton-utils/2034 Log: reimplements the BeanValidator based on NuitonValidator and NuitonValidatorResult Added: trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/ trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidator.java trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorEvent.java trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorListener.java trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorMessage.java trunk/nuiton-validator/src/test/java/org/nuiton/validator/bean/ trunk/nuiton-validator/src/test/java/org/nuiton/validator/bean/BeanValidatorTest.java trunk/nuiton-validator/src/test/java/org/nuiton/validator/bean/SimpleBean.java trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/ trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-error-validation.xml trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-fatal-validation.xml trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-info-validation.xml trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-simple-validation.xml trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-warning-validation.xml Modified: trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidatorResult.java Modified: trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidatorResult.java =================================================================== --- trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidatorResult.java 2011-01-23 17:28:26 UTC (rev 2033) +++ trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidatorResult.java 2011-01-23 22:00:53 UTC (rev 2034) @@ -121,6 +121,23 @@ } } + + public void setMessagesForScope(NuitonValidatorScope scope, + String field, + List<String> messages) { + + if (this.messages==null) { + this.messages = new EnumMap<NuitonValidatorScope, FieldMap<List<String>>>(NuitonValidatorScope.class); + } + + FieldMap<List<String>> fieldMap = this.messages.get(scope); + if (fieldMap==null) { + fieldMap = new FieldMap<List<String>>(); + this.messages.put(scope,fieldMap); + } + fieldMap.put(field,messages); + } + public List<String> getMessagesForScope(NuitonValidatorScope scope) { List<String> result = new ArrayList<String>(); @@ -215,11 +232,17 @@ return result; } - protected EnumMap<NuitonValidatorScope, FieldMap<List<String>>> getMessages() { + public void clearMessagesForScope(NuitonValidatorScope scope) { + if (messages != null) { + messages.remove(scope); + } + } + + public EnumMap<NuitonValidatorScope, FieldMap<List<String>>> getMessages() { return messages; } - protected Map<String, FieldMap<Object>> getTagValues() { + public Map<String, FieldMap<Object>> getTagValues() { return tagValues; } } Added: trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidator.java =================================================================== --- trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidator.java (rev 0) +++ trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidator.java 2011-01-23 22:00:53 UTC (rev 2034) @@ -0,0 +1,598 @@ +package org.nuiton.validator.bean; + +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.beans.BeanUtil; +import org.nuiton.util.converter.ConverterUtil; +import org.nuiton.validator.NuitonValidator; +import org.nuiton.validator.NuitonValidatorModel; +import org.nuiton.validator.NuitonValidatorProvider; +import org.nuiton.validator.NuitonValidatorResult; +import org.nuiton.validator.NuitonValidatorScope; + +import javax.swing.event.EventListenerList; +import java.beans.Introspector; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Validator for a javaBean object. + * <p/> + * A such validator is designed to validate to keep the validation of a bean, + * means the bean is attached to the validator (field {@link #bean}. + * <p/> + * <b>Note:</b> The {@code BeanValidator} should never be used for + * validation in a service approch since it needs to keep a reference to the + * bean to validate. + * + * @author tchemit <chemit@codelutin.com> + * @since 2.0 + */ +public class BeanValidator<O> { + + /** + * Name of the bounded property {@link #bean}. + * + * @see #bean + * @see #getBean() + * @see #setBean(Object) + */ + public static final String BEAN_PROPERTY = "bean"; + + /** + * Name of the bounded property {@code context}. + * + * @see #bean + * @see #getContext() + * @see #setContext(String) + */ + public static final String CONTEXT_PROPERTY = "context"; + + + /** + * Name of the bounded property {@link #valid}. + * + * @see #valid + * @see #isValid() + * @see #setValid(boolean) + */ + public static final String VALID_PROPERTY = "valid"; + + /** + * Name of the bounded property {@link #changed}. + * + * @see #changed + * @see #isChanged() + * @see #setChanged(boolean) + */ + public static final String CHANGED_PROPERTY = "changed"; + + /** Logger. */ + protected static final Log log = LogFactory.getLog(BeanValidator.class); + + /** The bean to validate. */ + protected O bean; + + /** The delegate validator used to validate the bean. */ + protected NuitonValidator<O> delegate; + + /** State of validation */ + protected NuitonValidatorResult messages; + + /** + * State to indicate that validator has changed since the last time bean was + * setted. + */ + protected boolean changed; + + /** state of the validator (is true if no errors of error scope is found) */ + protected boolean valid = true; + + /** + * State to know if the validator can be used (we keep this state for + * performance reasons : do not want to compute this value each time a + * validation is asked...). + */ + protected boolean canValidate = true; + + /** map of conversion errors detected by this validator */ + protected Map<String, String> conversionErrors; + + /** 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(); + + protected final NuitonValidatorProvider validatorProvider; + + protected final NuitonValidatorModel<O> initialValidatorModel; + + protected NuitonValidatorModel<O> validatorModel; + + public BeanValidator(NuitonValidatorProvider validatorProvider, + Class<O> beanClass, + String context) { + this(validatorProvider, beanClass, + context, + NuitonValidatorScope.values() + ); + } + + public BeanValidator(NuitonValidatorProvider validatorProvider, + Class<O> beanClass, + String context, + NuitonValidatorScope... filterScopes) { + this.validatorProvider = validatorProvider; + pcs = new PropertyChangeSupport(this); + conversionErrors = new TreeMap<String, String>(); + + // initial model of validation (keep it when need to change + // the validator model : we have all we need inside this one (type, + // scopes...)) + initialValidatorModel = validatorProvider.getModel(beanClass, + context, + filterScopes + ); + + // at the begin, validator model is exactly the initial validator model + validatorModel = initialValidatorModel; + + pcs.firePropertyChange(CONTEXT_PROPERTY, + null, + context + ); + + l = new PropertyChangeListener() { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + // chaque modification lance la validation + doValidate(); + } + }; + } + + /** + * Retourne vrai si l'objet bean a ete modifie depuis le dernier {@link + * #setBean} + * + * @return <code>true</code> if bean was modify since last {@link + * #setBean(Object)} invocation + */ + public boolean isChanged() { + return changed; + } + + /** + * 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_PROPERTY, null, changed); + } + + + public boolean isCanValidate() { + return canValidate; + } + + public void setCanValidate(boolean canValidate) { + this.canValidate = canValidate; + } + + public boolean isValid() { + return valid; + } + + public void setValid(boolean valid) { + this.valid = valid; + + // force the property to be fired (never pass the older value) + pcs.firePropertyChange(VALID_PROPERTY, null, valid); + } + + public O getBean() { + return bean; + } + + public void setBean(O bean) { + O oldBean = this.bean; + if (log.isDebugEnabled()) { + log.debug(this + " : " + bean); + } + + // clean conversions of previous bean + conversionErrors.clear(); + + if (oldBean != null) { + try { + BeanUtil.removePropertyChangeListener(l, oldBean); + } catch (Exception eee) { + if (log.isInfoEnabled()) { + log.info("Can't register as listener for bean " + oldBean.getClass() + + " for reason " + eee.getMessage(), eee); + } + } + } + this.bean = bean; + + setCanValidate(!getDelegate().getEffectiveFields().isEmpty() && bean != null); + + if (bean == null) { + + // remove all messages for all fields of the validator + + mergeMessages(null); + + } else { + try { + + BeanUtil.addPropertyChangeListener(l, bean); + } catch (Exception eee) { + if (log.isInfoEnabled()) { + log.info("Can't register as listener for bean " + bean.getClass() + + " for reason " + eee.getMessage(), eee); + } + } + validate(); + } + setChanged(false); + setValid(!messages.hasFatalMessages() && !messages.hasErrorMessagess()); + pcs.firePropertyChange(BEAN_PROPERTY, oldBean, bean); + } + + public String getContext() { + return validatorModel == null ? null : validatorModel.getContext(); + } + + public void setContext(String context) { + + String oldContext = getContext(); + + // compute the new validator model + NuitonValidatorScope[] scopes = new NuitonValidatorScope[initialValidatorModel.getScopes().size()]; + validatorModel = validatorProvider.getModel(initialValidatorModel.getType(), context, scopes); + + // remove old delegate validator + delegate = null; + + // changing context could change fields definition + // so dettach bean, must rebuild the fields + if (bean != null) { + setBean(null); + } + + pcs.firePropertyChange(CONTEXT_PROPERTY, + oldContext, + context + ); + + } + + /** + * Convert a value. + * <p/> + * If an error occurs, then add an error in validator. + * + * @param <T> 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> T convert(String fieldName, String value, Class<T> 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 (!isCanValidate() || 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 + String s = Introspector.decapitalize(valueClass.getSimpleName()); + conversionErrors.put(fieldName, "error.convertor." + s); + result = null; + validate(); + } + return result; + } + + public void doValidate() { + validate(); + setValid(!messages.hasFatalMessages() && !messages.hasErrorMessagess()); + 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} + * <p/> + * <b>Note:</b> la methode est protected et on utilise la methode + * {@link #doValidate()} car la méthode 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 (!isCanValidate()) { + return; + } + + NuitonValidatorResult result = getDelegate().validate(bean); + + // treate conversion errors + // reinject them + for (Map.Entry<String, String> entry : conversionErrors.entrySet()) { + + + // remove from validation, errors occurs on this field + String field = entry.getKey(); + + + List<String> errors = result.getErrorMessages(field); + + String conversionError = entry.getValue(); + if (errors != null) { + errors.clear(); + errors.add(conversionError); + } else { + errors = Collections.singletonList(conversionError); + } + + result.setMessagesForScope(NuitonValidatorScope.ERROR, field, errors); + } + + mergeMessages(result); + +// 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() + "<beanClass:" + initialValidatorModel.getType() + + ", context:" + getContext() + ">"; + } + + 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); + } + + protected void fireFieldChanged(String field, + NuitonValidatorScope 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 void mergeMessages(NuitonValidatorResult newMessages) { + + if (newMessages == null && messages == null) { + + // no messages ever registred and ask to delete them, so nothing + // to do + return; + } + + Set<NuitonValidatorScope> scopes = getDelegate().getEffectiveScopes(); + + for (NuitonValidatorScope scope : scopes) { + + mergeMessages(scope, newMessages); + + } + + if (newMessages != null) { + + //TODO tchemit 2011-01-23 Perharps it will necessary to clear the messages for memory performance ? + + // finally keep the new messages as the current messages + messages = newMessages; + } + } + + protected void mergeMessages(NuitonValidatorScope scope, + NuitonValidatorResult newMessages) { + + if (newMessages == null) { + + // special case to empty all messages + + List<String> fieldsForScope = messages.getFieldsForScope(scope); + + for (String field : fieldsForScope) { + List<String> messagesForScope = messages.getMessagesForScope(field, scope); + fireFieldChanged(field, scope, null, messagesForScope.toArray(new String[messagesForScope.size()])); + } + + // suppress all messages for this scope + messages.clearMessagesForScope(scope); + + + } else { + + List<String> newFields = newMessages.getFieldsForScope(scope); + + if (messages == null) { + + // first time of a merge, just add new messages + + for (String field : newFields) { + List<String> messagesForScope = newMessages.getMessagesForScope(field, scope); + fireFieldChanged(field, scope, messagesForScope.toArray(new String[messagesForScope.size()]), null); + } + + // nothing else to do + return; + } + + List<String> oldFields = messages.getFieldsForScope(scope); + + Set<String> mergedFields = new HashSet<String>(); + + Iterator<String> itr; + + // detects field with only new messages + itr = newFields.iterator(); + while (itr.hasNext()) { + String newField = itr.next(); + + if (!oldFields.contains(newField)) { + + // this fields has now messages but not before : new messages + List<String> messagesForScope = newMessages.getMessagesForScope(newField, scope); + fireFieldChanged(newField, scope, messagesForScope.toArray(new String[messagesForScope.size()]), null); + + // treated field + itr.remove(); + } + } + + // detects fields with only obsolete messages + itr = oldFields.iterator(); + while (itr.hasNext()) { + String oldField = itr.next(); + + if (!newFields.contains(oldField)) { + + // this fields has no more messages + List<String> messagesForScope = messages.getMessagesForScope(oldField, scope); + fireFieldChanged(oldField, scope, null, messagesForScope.toArray(new String[messagesForScope.size()])); + + // treated field + itr.remove(); + } else { + + // merged field + mergedFields.add(oldField); + } + } + + for (String field : newFields) { + + List<String> newMessagesForScope = newMessages.getMessagesForScope(field, scope); + List<String> oldMessagesForScope = messages.getMessagesForScope(field, scope); + + // get old obsoletes messages to delete + Set<String> toDelete = new HashSet<String>(oldMessagesForScope); + toDelete.removeAll(newMessagesForScope); + + // get new messages to add + Set<String> toAdd = new HashSet<String>(newMessagesForScope); + toAdd.removeAll(oldMessagesForScope); + + fireFieldChanged( + field, + scope, + toAdd.isEmpty() ? null : toAdd.toArray(new String[toAdd.size()]), + toDelete.isEmpty() ? null : toDelete.toArray(new String[toDelete.size()]) + ); + + } + + + } + } + + protected NuitonValidator<O> getDelegate() { + if (delegate == null) { + delegate = validatorProvider.newValidator(validatorModel); + } + return delegate; + } + +} Property changes on: trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidator.java ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision HeadURL Added: svn:eol-style + native Copied: trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorEvent.java (from rev 2027, trunk/nuiton-validator/src/main/java/org/nuiton/validator/legacy/BeanValidatorEvent.java) =================================================================== --- trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorEvent.java (rev 0) +++ trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorEvent.java 2011-01-23 22:00:53 UTC (rev 2034) @@ -0,0 +1,84 @@ +/* + * #%L + * Nuiton Utils :: Nuiton Validator + * + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 CodeLutin, Tony Chemit + * %% + * 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 + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ +package org.nuiton.validator.bean; + +import org.nuiton.validator.NuitonValidatorScope; + +import java.util.EventObject; + +/** + * The definition of an event on {@link BeanValidatorListener} + * to be fired by a {@link BeanValidator}. + * + * @author tchemit <chemit@codelutin.com> + * @since 2.0 + */ +public class BeanValidatorEvent extends EventObject { + + private static final long serialVersionUID = 1L; + + /** the field impacted by the validator */ + protected String field; + + /** the scope impacted by the event */ + protected NuitonValidatorScope scope; + + protected String[] messagestoAdd; + + protected String[] messagestoDelete; + + public BeanValidatorEvent(BeanValidator<?> source, + String field, + NuitonValidatorScope scope, + String[] messagestoAdd, + String[] messagestoDelete) { + super(source); + this.field = field; + this.scope = scope; + this.messagestoAdd = messagestoAdd; + this.messagestoDelete = messagestoDelete; + } + + @Override + public BeanValidator<?> getSource() { + return (BeanValidator<?>) super.getSource(); + } + + public String[] getMessagesToAdd() { + return messagestoAdd; + } + + public String[] getMessagesToDelete() { + return messagestoDelete; + } + + public NuitonValidatorScope getScope() { + return scope; + } + + public String getField() { + return field; + } +} Copied: trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorListener.java (from rev 2027, trunk/nuiton-validator/src/main/java/org/nuiton/validator/legacy/BeanValidatorListener.java) =================================================================== --- trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorListener.java (rev 0) +++ trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorListener.java 2011-01-23 22:00:53 UTC (rev 2034) @@ -0,0 +1,44 @@ +/* + * #%L + * Nuiton Utils :: Nuiton Validator + * + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 CodeLutin, Tony Chemit + * %% + * 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 + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ +package org.nuiton.validator.bean; + + +import java.util.EventListener; + +/** + * The listener contract to be used on {@link BeanValidator}. + * + * @author tchemit <chemit@codelutin.com> + * @since 2.0 + */ +public interface BeanValidatorListener extends EventListener { + + /** + * Invoked when a validator detects some changes on a field. + * + * @param event the event + */ + void onFieldChanged(BeanValidatorEvent event); +} Copied: trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorMessage.java (from rev 2027, trunk/nuiton-validator/src/main/java/org/nuiton/validator/legacy/BeanValidatorMessage.java) =================================================================== --- trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorMessage.java (rev 0) +++ trunk/nuiton-validator/src/main/java/org/nuiton/validator/bean/BeanValidatorMessage.java 2011-01-23 22:00:53 UTC (rev 2034) @@ -0,0 +1,142 @@ +/* + * #%L + * Nuiton Utils :: Nuiton Validator + * + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 CodeLutin, Tony Chemit + * %% + * 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 + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ +package org.nuiton.validator.bean; + +import org.nuiton.validator.NuitonValidatorScope; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import static org.nuiton.i18n.I18n._; + +/** + * The object to box a validation message. + * + * @param <E> type of message (use for override {@link #compareTo(Object)} + * method. + * @author tchemit <chemit@codelutin.com> + * @since 2.0 + */ +public class BeanValidatorMessage<E extends BeanValidatorMessage<?>> implements Comparable<E> { + + /** the validator that produce the message */ + protected BeanValidator<?> validator; + + /** the field that produce the message */ + protected String field; + + /** the label of the message (to be displayed somewhere) */ + protected String message; + + /** the scope of the message */ + protected NuitonValidatorScope scope; + + public BeanValidatorMessage(BeanValidator<?> validator, + String field, + String message, + NuitonValidatorScope scope) { + this.field = field; + this.validator = validator; + this.message = message == null ? null : message.trim(); + this.scope = scope; + } + + public BeanValidator<?> getValidator() { + return validator; + } + + public String getField() { + return field; + } + + public NuitonValidatorScope getScope() { + return scope; + } + + public String getMessage() { + return message; + } + + @Override + public int compareTo(E o) { + // sort on scope + int result = getScope().compareTo(o.getScope()); + if (result == 0) { + // sort on field name + result = field.compareTo(o.field); + if (result == 0) { + // sort on message + result = message.compareTo(o.message); + } + } + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BeanValidatorMessage<?> that = (BeanValidatorMessage<?>) o; + + return field.equals(that.field) && + message.equals(that.message) && + scope == that.scope; + } + + @Override + public int hashCode() { + int result = field.hashCode(); + result = 31 * result + message.hashCode(); + result = 31 * result + scope.hashCode(); + return result; + } + + @Override + public String toString() { + return scope + " - " + getI18nError(message); + } + + public String getI18nError(String error) { + String text; + if (!error.contains("##")) { + text = _(error); + } else { + StringTokenizer stk = new StringTokenizer(error, "##"); + String errorName = stk.nextToken(); + List<String> args = new ArrayList<String>(); + while (stk.hasMoreTokens()) { + args.add(stk.nextToken()); + } + text = _(errorName, args.toArray()); + } + return text; + } +} Copied: trunk/nuiton-validator/src/test/java/org/nuiton/validator/bean/BeanValidatorTest.java (from rev 2027, trunk/nuiton-validator/src/test/java/org/nuiton/validator/legacy/BeanValidatorTest.java) =================================================================== --- trunk/nuiton-validator/src/test/java/org/nuiton/validator/bean/BeanValidatorTest.java (rev 0) +++ trunk/nuiton-validator/src/test/java/org/nuiton/validator/bean/BeanValidatorTest.java 2011-01-23 22:00:53 UTC (rev 2034) @@ -0,0 +1,295 @@ +/* + * #%L + * Nuiton Utils :: Nuiton Validator + * + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 CodeLutin, Tony Chemit + * %% + * 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 + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ +package org.nuiton.validator.bean; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.nuiton.validator.NuitonValidatorFactory; +import org.nuiton.validator.NuitonValidatorProvider; +import org.nuiton.validator.NuitonValidatorScope; +import org.nuiton.validator.xwork2.XWork2NuitonValidatorProvider; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** @author tchemit <chemit@codelutin.com> */ +public class BeanValidatorTest { + + /** Logger */ + static protected final Log log = LogFactory.getLog(BeanValidatorTest.class); + + protected BeanValidator<SimpleBean> validator; + + protected SimpleBean bean; + + BeanValidatorListenerImpl fatalListener; + + BeanValidatorListenerImpl errorListener; + + BeanValidatorListenerImpl warningListener; + + BeanValidatorListenerImpl infoListener; + + @Before + public void setUp() { + NuitonValidatorProvider provider = NuitonValidatorFactory.getProvider(XWork2NuitonValidatorProvider.PROVIDER_NAME); + bean = new SimpleBean(); + validator = new BeanValidator<SimpleBean>(provider, SimpleBean.class, null); + validator.addBeanValidatorListener(fatalListener = new BeanValidatorListenerImpl(NuitonValidatorScope.FATAL)); + validator.addBeanValidatorListener(errorListener = new BeanValidatorListenerImpl(NuitonValidatorScope.ERROR)); + validator.addBeanValidatorListener(warningListener = new BeanValidatorListenerImpl(NuitonValidatorScope.WARNING)); + validator.addBeanValidatorListener(infoListener = new BeanValidatorListenerImpl(NuitonValidatorScope.INFO)); + } + + @After + public void tearDown() { + bean = null; + if (validator != null) { + validator.setBean(null); + validator = null; + } + } + + private static final String STRING_VALUE_FATAL = "stringValue.fatal"; + + private static final String STRING_VALUE_ERROR = "stringValue.error"; + + private static final String STRING_VALUE_WARNING = "stringValue.warning"; + + private static final String INT_VALUE_FATAL = "intValue.fatal"; + + private static final String INT_VALUE_ERROR = "intValue.error"; + + private static final String INT_VALUE_INFO = "intValue.info"; + + @Test + public void testValidate() { + + assertMessages(fatalListener); + assertMessages(errorListener); + assertMessages(warningListener); + assertMessages(infoListener); + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + validator.setBean(bean); + + assertMessages(fatalListener, STRING_VALUE_FATAL, INT_VALUE_FATAL); + assertMessages(errorListener, STRING_VALUE_ERROR, INT_VALUE_ERROR); + assertMessages(warningListener, STRING_VALUE_WARNING); + assertMessages(infoListener, INT_VALUE_INFO); + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + bean.setStringValue("one"); + + assertMessages(fatalListener, STRING_VALUE_FATAL, INT_VALUE_FATAL); + assertMessages(errorListener, INT_VALUE_ERROR); + assertMessages(warningListener, STRING_VALUE_WARNING); + assertMessages(infoListener, INT_VALUE_INFO); + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + bean.setStringValue("oneone"); + + assertMessages(fatalListener, STRING_VALUE_FATAL, INT_VALUE_FATAL); + assertMessages(errorListener, INT_VALUE_ERROR); + assertMessages(warningListener); + assertMessages(infoListener, INT_VALUE_INFO); + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + bean.setIntValue(1); + + assertMessages(fatalListener, STRING_VALUE_FATAL, INT_VALUE_FATAL); + assertMessages(errorListener); + assertMessages(warningListener); + assertMessages(infoListener, INT_VALUE_INFO); + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + bean.setIntValue(10); + + assertMessages(fatalListener, STRING_VALUE_FATAL, INT_VALUE_FATAL); + assertMessages(errorListener); + assertMessages(warningListener); + assertMessages(infoListener); + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + + bean.setStringValue(null); + bean.setIntValue(0); + + assertMessages(fatalListener, STRING_VALUE_FATAL, INT_VALUE_FATAL); + assertMessages(errorListener, STRING_VALUE_ERROR, INT_VALUE_ERROR); + assertMessages(warningListener, STRING_VALUE_WARNING); + assertMessages(infoListener, INT_VALUE_INFO); + + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + + bean.setStringValue("5"); + bean.setIntValue(5); + assertMessages(fatalListener); + assertMessages(errorListener); + assertMessages(warningListener, STRING_VALUE_WARNING); + assertMessages(infoListener, INT_VALUE_INFO); + } + + @Test + public void testConvert() { + + + assertMessages(errorListener); + assertMessages(warningListener); + assertMessages(infoListener); + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + + validator.setBean(bean); + + assertMessages(fatalListener, STRING_VALUE_FATAL, INT_VALUE_FATAL); + assertMessages(errorListener, STRING_VALUE_ERROR, INT_VALUE_ERROR); + assertMessages(warningListener, STRING_VALUE_WARNING); + assertMessages(infoListener, INT_VALUE_INFO); + + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + + Object value = validator.convert("intValue", "abc", Class.class); + + Assert.assertNull(value); + + assertMessages(fatalListener, STRING_VALUE_FATAL, INT_VALUE_FATAL); + assertMessages(errorListener, STRING_VALUE_ERROR, "error.convertor.class"); + assertMessages(warningListener, STRING_VALUE_WARNING); + assertMessages(infoListener, INT_VALUE_INFO); + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + bean.setStringValue("one"); + + assertMessages(fatalListener, STRING_VALUE_FATAL, INT_VALUE_FATAL); + assertMessages(errorListener, "error.convertor.class"); + assertMessages(warningListener, STRING_VALUE_WARNING); + assertMessages(infoListener, INT_VALUE_INFO); + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + + value = validator.convert("intValue", "3", Integer.class); + + bean.setIntValue((Integer) value); + + assertMessages(fatalListener, STRING_VALUE_FATAL, INT_VALUE_FATAL); + assertMessages(errorListener); + assertMessages(warningListener, STRING_VALUE_WARNING); + assertMessages(infoListener, INT_VALUE_INFO); + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + + bean.setIntValue(-1); + assertMessages(fatalListener, STRING_VALUE_FATAL, INT_VALUE_FATAL); + assertMessages(errorListener, INT_VALUE_ERROR); + assertMessages(warningListener, STRING_VALUE_WARNING); + assertMessages(infoListener, INT_VALUE_INFO); + + if (log.isInfoEnabled()) { + log.info("-----------------------------------------------"); + } + } + + void assertMessages(BeanValidatorListenerImpl listener, + String... expected) { + List<String> actual = listener.getMessages(); + Assert.assertEquals(" shoudl have " + + Arrays.toString(expected) + " but had " + actual, + expected.length, actual.size()); + for (String m : expected) { + Assert.assertEquals("could not find " + m + " in " + actual, + true, actual.contains(m)); + } + } + + class BeanValidatorListenerImpl implements BeanValidatorListener { + + final NuitonValidatorScope scope; + + public BeanValidatorListenerImpl(NuitonValidatorScope scope) { + this.scope = scope; + } + + List<String> messages = new ArrayList<String>(); + + public List<String> getMessages() { + return messages; + } + + @Override + public void onFieldChanged(BeanValidatorEvent event) { + if (scope != event.getScope()) { + return; + } + String[] messagesToDelete = event.getMessagesToDelete(); + if (messagesToDelete != null && messagesToDelete.length > 0) { + if (log.isInfoEnabled()) { + log.info(event.getScope() + " messages to delete : " + Arrays.toString(messagesToDelete)); + } + for (String m : messagesToDelete) { + messages.remove(m); + } + } + String[] messagesToAdd = event.getMessagesToAdd(); + if (messagesToAdd != null && messagesToAdd.length > 0) { + if (log.isInfoEnabled()) { + log.info(event.getScope() + " messages to add : " + Arrays.toString(messagesToAdd)); + } + messages.addAll(Arrays.asList(messagesToAdd)); + } + } + } +} Copied: trunk/nuiton-validator/src/test/java/org/nuiton/validator/bean/SimpleBean.java (from rev 2027, trunk/nuiton-validator/src/test/java/org/nuiton/validator/legacy/SimpleBean.java) =================================================================== --- trunk/nuiton-validator/src/test/java/org/nuiton/validator/bean/SimpleBean.java (rev 0) +++ trunk/nuiton-validator/src/test/java/org/nuiton/validator/bean/SimpleBean.java 2011-01-23 22:00:53 UTC (rev 2034) @@ -0,0 +1,79 @@ +/* + * #%L + * Nuiton Utils :: Nuiton Validator + * + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 CodeLutin, Tony Chemit + * %% + * 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 + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ +package org.nuiton.validator.bean; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; + +public class SimpleBean { + + protected int intValue; + + protected String stringValue; + + final PropertyChangeSupport p; + + public SimpleBean() { + p = new PropertyChangeSupport(this); + } + + public int getIntValue() { + return intValue; + } + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + String old = this.stringValue; + this.stringValue = stringValue; + p.firePropertyChange("stringValue", old, stringValue); + } + + public void setIntValue(int intValue) { + int old = this.intValue; + this.intValue = intValue; + p.firePropertyChange("intValue", old, intValue); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + p.addPropertyChangeListener(listener); + } + + public void addPropertyChangeListener(String propertyName, + PropertyChangeListener listener) { + p.addPropertyChangeListener(propertyName, listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + p.removePropertyChangeListener(listener); + } + + public void removePropertyChangeListener(String propertyName, + PropertyChangeListener listener) { + p.removePropertyChangeListener(propertyName, listener); + } +} \ No newline at end of file Copied: trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-error-validation.xml (from rev 2027, trunk/nuiton-validator/src/test/resources/org/nuiton/validator/legacy/SimpleBean-error-validation.xml) =================================================================== --- trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-error-validation.xml (rev 0) +++ trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-error-validation.xml 2011-01-23 22:00:53 UTC (rev 2034) @@ -0,0 +1,43 @@ +<!-- + #%L + Nuiton Utils :: Nuiton Validator + + $Id$ + $HeadURL$ + %% + Copyright (C) 2011 CodeLutin, Tony Chemit + %% + 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 + <http://www.gnu.org/licenses/lgpl-3.0.html>. + #L% + --> +<!DOCTYPE validators PUBLIC + "-//OpenSymphony Group//XWork Validator 1.0.2//EN" + "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd"> +<validators> + + <field name="stringValue"> + <field-validator type="requiredstring"> + <message>stringValue.error</message> + </field-validator> + </field> + + <field name="intValue"> + <field-validator type="int"> + <param name="min">1</param> + <message>intValue.error</message> + </field-validator> + </field> + +</validators> \ No newline at end of file Copied: trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-fatal-validation.xml (from rev 2027, trunk/nuiton-validator/src/test/resources/org/nuiton/validator/legacy/SimpleBean-fatal-validation.xml) =================================================================== --- trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-fatal-validation.xml (rev 0) +++ trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-fatal-validation.xml 2011-01-23 22:00:53 UTC (rev 2034) @@ -0,0 +1,47 @@ +<!-- + #%L + Nuiton Utils :: Nuiton Validator + + $Id$ + $HeadURL$ + %% + Copyright (C) 2011 CodeLutin, Tony Chemit + %% + 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 + <http://www.gnu.org/licenses/lgpl-3.0.html>. + #L% + --> +<!DOCTYPE validators PUBLIC + "-//OpenSymphony Group//XWork Validator 1.0.2//EN" + "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd"> +<validators> + + <field name="stringValue"> + <field-validator type="fieldexpression"> + <param name="expression"> + <![CDATA[ stringValue != null && stringValue == "5"]]></param> + <message>stringValue.fatal</message> + </field-validator> + </field> + + + <field name="intValue"> + <field-validator type="int"> + <param name="min">5</param> + <param name="max">5</param> + <message>intValue.fatal</message> + </field-validator> + </field> + +</validators> \ No newline at end of file Copied: trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-info-validation.xml (from rev 2027, trunk/nuiton-validator/src/test/resources/org/nuiton/validator/legacy/SimpleBean-info-validation.xml) =================================================================== --- trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-info-validation.xml (rev 0) +++ trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-info-validation.xml 2011-01-23 22:00:53 UTC (rev 2034) @@ -0,0 +1,37 @@ +<!-- + #%L + Nuiton Utils :: Nuiton Validator + + $Id$ + $HeadURL$ + %% + Copyright (C) 2011 CodeLutin, Tony Chemit + %% + 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 + <http://www.gnu.org/licenses/lgpl-3.0.html>. + #L% + --> +<!DOCTYPE validators PUBLIC + "-//OpenSymphony Group//XWork Validator 1.0.2//EN" + "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd"> +<validators> + + <field name="intValue"> + <field-validator type="int"> + <param name="min">10</param> + <message>intValue.info</message> + </field-validator> + </field> + +</validators> \ No newline at end of file Copied: trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-simple-validation.xml (from rev 2027, trunk/nuiton-validator/src/test/resources/org/nuiton/validator/legacy/SimpleBean-simple-validation.xml) =================================================================== --- trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-simple-validation.xml (rev 0) +++ trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-simple-validation.xml 2011-01-23 22:00:53 UTC (rev 2034) @@ -0,0 +1,43 @@ +<!-- + #%L + Nuiton Utils :: Nuiton Validator + + $Id$ + $HeadURL$ + %% + Copyright (C) 2011 CodeLutin, Tony Chemit + %% + 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 + <http://www.gnu.org/licenses/lgpl-3.0.html>. + #L% + --> +<!DOCTYPE validators PUBLIC + "-//OpenSymphony Group//XWork Validator 1.0.2//EN" + "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd"> +<validators> + + <field name="stringValue"> + <field-validator type="requiredstring"> + <message>stringValue.null</message> + </field-validator> + </field> + + <field name="intValue"> + <field-validator type="int"> + <param name="min">1</param> + <message>intValue.null</message> + </field-validator> + </field> + +</validators> \ No newline at end of file Copied: trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-warning-validation.xml (from rev 2027, trunk/nuiton-validator/src/test/resources/org/nuiton/validator/legacy/SimpleBean-warning-validation.xml) =================================================================== --- trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-warning-validation.xml (rev 0) +++ trunk/nuiton-validator/src/test/resources/org/nuiton/validator/bean/SimpleBean-warning-validation.xml 2011-01-23 22:00:53 UTC (rev 2034) @@ -0,0 +1,37 @@ +<!-- + #%L + Nuiton Utils :: Nuiton Validator + + $Id$ + $HeadURL$ + %% + Copyright (C) 2011 CodeLutin, Tony Chemit + %% + 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 + <http://www.gnu.org/licenses/lgpl-3.0.html>. + #L% + --> +<!DOCTYPE validators PUBLIC + "-//OpenSymphony Group//XWork Validator 1.0.2//EN" + "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd"> +<validators> + + <field name="stringValue"> + <field-validator type="fieldexpression"> + <param name="expression"><![CDATA[ stringValue != null && stringValue.length() > 5]]></param> + <message>stringValue.warning</message> + </field-validator> + </field> + +</validators> \ No newline at end of file