View Javadoc
1   package org.nuiton.validator.bean.list;
2   /*
3    * #%L
4    * Nuiton Validator
5    * %%
6    * Copyright (C) 2013 - 2014 Code Lutin, Tony Chemit
7    * %%
8    * This program is free software: you can redistribute it and/or modify
9    * it under the terms of the GNU Lesser General Public License as 
10   * published by the Free Software Foundation, either version 3 of the 
11   * License, or (at your option) any later version.
12   * 
13   * This program is distributed in the hope that it will be useful,
14   * but WITHOUT ANY WARRANTY; without even the implied warranty of
15   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16   * GNU General Lesser Public License for more details.
17   * 
18   * You should have received a copy of the GNU General Lesser Public 
19   * License along with this program.  If not, see
20   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
21   * #L%
22   */
23  
24  import com.google.common.base.Preconditions;
25  import com.google.common.collect.ImmutableSet;
26  import com.google.common.collect.Lists;
27  import com.google.common.collect.Maps;
28  import com.google.common.collect.Sets;
29  import org.apache.commons.beanutils.ConversionException;
30  import org.apache.commons.collections4.CollectionUtils;
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.nuiton.util.beans.BeanUtil;
34  import org.nuiton.validator.NuitonValidator;
35  import org.nuiton.validator.NuitonValidatorFactory;
36  import org.nuiton.validator.NuitonValidatorModel;
37  import org.nuiton.validator.NuitonValidatorProvider;
38  import org.nuiton.validator.NuitonValidatorResult;
39  import org.nuiton.validator.NuitonValidatorScope;
40  import org.nuiton.validator.bean.AbstractNuitonValidatorContext;
41  import org.nuiton.validator.bean.AbstractValidator;
42  
43  import java.beans.PropertyChangeListener;
44  import java.util.Collection;
45  import java.util.Collections;
46  import java.util.List;
47  import java.util.Map;
48  import java.util.Set;
49  
50  /**
51   * To validate a list of beans.
52   *
53   * Each bean of the list will be associated with a {@link NuitonValidatorContext}.
54   *
55   * @author Tony Chemit - chemit@codelutin.com
56   * @since 2.5.2
57   */
58  public class BeanListValidator<O> extends AbstractValidator<O> {
59  
60      /** Logger. */
61      private static final Log log = LogFactory.getLog(BeanListValidator.class);
62  
63      /**
64       * Obtain a new {@link BeanListValidator} for the given parameters.
65       *
66       * <b>Note:</b> It will use the default provider of {@link NuitonValidator}
67       *
68       * @param type    type of bean to validate
69       * @param context context of validation
70       * @param scopes  authorized scopes (if {@code null}, will use all scopes)
71       * @param <O>     type of bean to validate
72       * @return the new instanciated {@link BeanListValidator}.
73       * @throws NullPointerException if type is {@code null}
74       * @see NuitonValidatorFactory#getDefaultProviderName()
75       */
76      public static <O> BeanListValidator<O> newValidator(Class<O> type,
77                                                          String context,
78                                                          NuitonValidatorScope... scopes) throws NullPointerException {
79  
80  
81          // get the provider default name
82          String providerName = NuitonValidatorFactory.getDefaultProviderName();
83  
84          // get the bean validator with this provider
85          BeanListValidator<O> beanValidator = newValidator(providerName,
86                                                            type,
87                                                            context,
88                                                            scopes
89          );
90          return beanValidator;
91      }
92  
93      /**
94       * Obtain a new {@link BeanListValidator} for the given parameters.
95       *
96       * <b>Note:</b> It will use the provider of {@link NuitonValidator}
97       * defined by the {@code providerName}.
98       *
99       * @param providerName name of {@link NuitonValidator} to use
100      * @param type         type of bean to validate
101      * @param context      context of validation
102      * @param scopes       authorized scopes (if {@code null}, will use all scopes)
103      * @param <O>          type of bean to validate
104      * @return the new instanciated {@link BeanListValidator}.
105      * @throws NullPointerException if type is {@code null}
106      * @see NuitonValidatorFactory#getProvider(String)
107      */
108     public static <O> BeanListValidator<O> newValidator(String providerName,
109                                                         Class<O> type,
110                                                         String context,
111                                                         NuitonValidatorScope... scopes) throws NullPointerException {
112 
113         Preconditions.checkNotNull(type, "type parameter can not be null.");
114 
115         // get delegate validator provider
116         NuitonValidatorProvider provider =
117                 NuitonValidatorFactory.getProvider(providerName);
118 
119         Preconditions.checkState(
120                 provider != null,
121                 "Could not find provider with name " + providerName);
122 
123         // create the new instance of bean validator
124         BeanListValidator<O> validator = new BeanListValidator<O>(
125                 provider, type, context, scopes
126         );
127 
128         return validator;
129     }
130 
131     /**
132      * Context for each bean registred.
133      *
134      * @since 2.5.2
135      */
136     protected final Map<O, NuitonValidatorContext<O>> contexts;
137 
138     /**
139      * The delegate validator used to validate the bean.
140      *
141      * @since 2.5.2
142      */
143     protected NuitonValidator<O> delegate;
144 
145     public BeanListValidator(NuitonValidatorProvider validatorProvider,
146                              Class<O> beanClass,
147                              String context) {
148 
149         this(validatorProvider, beanClass,
150              context,
151              NuitonValidatorScope.values()
152         );
153     }
154 
155     public BeanListValidator(NuitonValidatorProvider validatorProvider,
156                              Class<O> beanClass,
157                              String context,
158                              NuitonValidatorScope... scopes) {
159 
160         super(validatorProvider, beanClass);
161 
162         contexts = Maps.newHashMap();
163 
164         // build delegate validator
165         rebuildDelegateValidator(beanClass, context, scopes);
166 
167         // context has changed
168         firePropertyChange(CONTEXT_PROPERTY,
169                            null,
170                            context
171         );
172 
173         // scopes has changed
174         firePropertyChange(SCOPES_PROPERTY,
175                            null,
176                            scopes
177         );
178     }
179 
180     /**
181      * Add a bean to validate.
182      *
183      * The bean can not be null, nor registered twice in the validator.
184      *
185      * @param bean the bean to attach (can not be {@code null}).
186      */
187     public void addBean(O bean) {
188 
189         // bean can not be null
190         Preconditions.checkNotNull(bean);
191 
192         // can not register twice the same bean
193         Preconditions.checkState(!contexts.containsKey(bean),
194                                  "The bean " + bean +
195                                  " is already registred in this validator.");
196 
197         if (log.isDebugEnabled()) {
198             log.debug(this + " : " + bean);
199         }
200 
201         // create validator for this bean
202         NuitonValidator<O> validator = validatorProvider.newValidator(getModel());
203 
204         // register it
205         NuitonValidatorContext<O> newcontext =
206                 new NuitonValidatorContext<O>(bean, validator);
207         newcontext.setValidator(validator);
208         newcontext.setBean(bean);
209 
210         contexts.put(bean, newcontext);
211 
212         setCanValidate(isCanValidate() && newcontext.isCanValidate());
213 
214         try {
215 
216             BeanUtil.addPropertyChangeListener(l, bean);
217         } catch (Exception eee) {
218             if (log.isInfoEnabled()) {
219                 log.info("Can't register as listener for bean " + bean.getClass() +
220                          " for reason " + eee.getMessage(), eee);
221             }
222         }
223         validate(bean);
224 
225         setChanged(false);
226         setValid(isValid0());
227     }
228 
229     public void addAllBeans(Collection<O> beansToAdd) {
230         for (O bean : beansToAdd) {
231             addBean(bean);
232         }
233     }
234 
235     /**
236      * Remove the given bean from the validaotr.
237      *
238      * The bean can not be {@code null}, nor not has been previously
239      * registred in this validator.
240      *
241      * @param bean the bean to unregister
242      */
243     public void removeBean(O bean) {
244 
245         // bean can not be null
246         Preconditions.checkNotNull(bean);
247 
248         if (log.isDebugEnabled()) {
249             log.debug(this + " : " + bean);
250         }
251 
252         // remove all messages for all fields of the validator
253         NuitonValidatorContext<O> context = getContext(bean);
254 
255         mergeMessages(context, null);
256 
257         contexts.remove(bean);
258 
259         try {
260             BeanUtil.removePropertyChangeListener(l, bean);
261         } catch (Exception eee) {
262             if (log.isInfoEnabled()) {
263                 log.info("Can't unregister as listener for bean " + bean.getClass() +
264                          " for reason " + eee.getMessage(), eee);
265             }
266         }
267     }
268 
269     /**
270      * Remove all the given beans fro this validator.
271      *
272      * Like in method {@link #removeBean(Object)}, each bean must be not
273      * {@code null} and has been previously registred in this validator.
274      *
275      * @param beansToRemove beans to remove from this validator
276      */
277     public void removeAllBeans(Collection<O> beansToRemove) {
278         for (O bean : beansToRemove) {
279             removeBean(bean);
280         }
281     }
282 
283     /**
284      * Shortcut method to unregister all previously registred beans from
285      * this validator.
286      */
287     public void removeAllBeans() {
288         Set<O> beansToRemove = getBeans();
289         removeAllBeans(beansToRemove);
290     }
291 
292     @Override
293     public boolean hasFatalErrors() {
294         boolean result = false;
295         for (NuitonValidatorContext<O> context : contexts.values()) {
296             result = context.hasFatalErrors();
297             if (result) {
298                 break;
299             }
300         }
301         return result;
302     }
303 
304     @Override
305     public boolean hasErrors() {
306         boolean result = false;
307         for (NuitonValidatorContext<O> context : contexts.values()) {
308             result = context.hasErrors();
309             if (result) {
310                 break;
311             }
312         }
313         return result;
314     }
315 
316     @Override
317     public boolean hasWarnings() {
318         boolean result = false;
319         for (NuitonValidatorContext<O> context : contexts.values()) {
320             result = context.hasWarnings();
321             if (result) {
322                 break;
323             }
324         }
325         return result;
326     }
327 
328     @Override
329     public boolean hasInfos() {
330         boolean result = false;
331         for (NuitonValidatorContext<O> context : contexts.values()) {
332             result = context.hasInfos();
333             if (result) {
334                 break;
335             }
336         }
337         return result;
338     }
339 
340     @Override
341     public boolean isValid(String fieldName) {
342         boolean result = true;
343 
344         for (NuitonValidatorContext<O> context : contexts.values()) {
345             result = context.isValid(fieldName);
346             if (!result) {
347                 break;
348             }
349         }
350         return result;
351     }
352 
353     @Override
354     public NuitonValidatorScope getHighestScope(String field) {
355         Set<NuitonValidatorScope> scopes = Sets.newHashSet();
356         for (NuitonValidatorContext<O> context : contexts.values()) {
357             scopes.add(context.getHighestScope(field));
358         }
359         NuitonValidatorScope scope = null;
360         if (scopes.isEmpty()) {
361             List<NuitonValidatorScope> scopeList = Lists.newArrayList(scopes);
362             Collections.sort(scopeList);
363             scope = scopeList.get(0);
364         }
365         return scope;
366     }
367 
368     @Override
369     public void doValidate() {
370         validate();
371         setValid(isValid0());
372         setChanged(true);
373     }
374 
375     /**
376      * Convert a value.
377      *
378      * If an error occurs, then add an error in validator.
379      *
380      * @param <T>        the type of conversion
381      * @param fieldName  the name of the bean property
382      * @param value      the value to convert
383      * @param valueClass the type of converted value
384      * @return the converted value, or null if conversion was not ok
385      */
386     @Override
387     public <T> T convert(O bean,
388                          String fieldName,
389                          String value,
390                          Class<T> valueClass) {
391         NuitonValidatorContext<O> context = getContext(bean);
392         T convert = null;
393         try {
394             convert = context.convert(fieldName, value, valueClass);
395         } catch (ConversionException e) {
396             // must revalidate
397             validate();
398         }
399         return convert;
400     }
401 
402     public void addBeanListValidatorListener(BeanListValidatorListener listener) {
403         listenerList.add(BeanListValidatorListener.class, listener);
404     }
405 
406     public void removeBeanListValidatorListener(BeanListValidatorListener listener) {
407         listenerList.remove(BeanListValidatorListener.class, listener);
408     }
409 
410     public BeanListValidatorListener[] getBeanListValidatorListeners() {
411         return listenerList.getListeners(BeanListValidatorListener.class);
412     }
413 
414     public Set<O> getBeans() {
415         return ImmutableSet.copyOf(contexts.keySet());
416     }
417 
418     @Override
419     protected void doValidate(O bean) {
420         validate(bean);
421         setValid(isValid0());
422         setChanged(true);
423     }
424 
425     @Override
426     protected NuitonValidator<O> getDelegate() {
427         return delegate;
428     }
429 
430     @Override
431     protected void rebuildDelegateValidator(Class<O> beanType,
432                                             String context,
433                                             NuitonValidatorScope... scopes) {
434 
435         // changing context could change fields definition
436         // so dettach bean, must rebuild the fields
437 
438         // Dettach the bean before any thing, because with the new delegate
439         // validator some old fields could not be used any longer, and then
440         // listeners will never have the full reset of their model...
441 
442         // remove all validators.
443 
444         if (scopes == null || scopes.length == 0) {
445             scopes = NuitonValidatorScope.values();
446         }
447 
448         // compute the new validator model
449         NuitonValidatorModel<O> model = validatorProvider.getModel(beanType,
450                                                                    context,
451                                                                    scopes
452         );
453 
454         // remove old delegate validator
455         delegate = validatorProvider.newValidator(model);
456     }
457 
458     /**
459      * il faut eviter le code re-intrant (durant une validation, une autre est
460      * demandee). Pour cela on fait la validation dans un thread, et tant que la
461      * premiere validation n'est pas fini, on ne repond pas aux solicitations.
462      * Cette method est public pour permettre de force une validation par
463      * programmation, ce qui est utile par exemple si le bean ne supporte pas
464      * les {@link PropertyChangeListener}
465      *
466      * <b>Note:</b> la methode est protected et on utilise la methode
467      * {@link #doValidate()} car la méthode ne modifie pas les etats
468      * internes et cela en rend son utilisation delicate (le validateur entre
469      * dans un etat incoherent par rapport aux messages envoyés).
470      */
471     protected void validate() {
472 
473         // on ne valide que si il y a un bean et que le resultat de la validation
474         // pourra etre affiche quelque part
475         if (isCanValidate()) {
476 
477             for (O bean : contexts.keySet()) {
478 
479                 validate(bean);
480             }
481         }
482     }
483 
484     protected void validate(O bean) {
485         NuitonValidatorContext<O> validator = getContext(bean);
486         NuitonValidatorResult result = validator.validate();
487         mergeMessages(validator, result);
488     }
489 
490     protected boolean isValid0() {
491         boolean result = true;
492         for (NuitonValidatorContext<O> context : contexts.values()) {
493             result = context.isValid();
494             if (!result) {
495                 break;
496             }
497         }
498         return result;
499     }
500 
501     protected void mergeMessages(NuitonValidatorContext<O> context,
502                                  NuitonValidatorResult newMessages) {
503 
504         List<BeanListValidatorEvent> events = context.mergeMessages(
505                 this, newMessages);
506 
507         if (CollectionUtils.isNotEmpty(events)) {
508 
509             // send all messages
510             for (BeanListValidatorEvent event : events) {
511                 fireFieldChanged(event);
512             }
513         }
514     }
515 
516     protected void fireFieldChanged(BeanListValidatorEvent evt) {
517 
518         for (BeanListValidatorListener listener :
519                 listenerList.getListeners(BeanListValidatorListener.class)) {
520             listener.onFieldChanged(evt);
521         }
522     }
523 
524     public NuitonValidatorContext<O> getContext(O bean) {
525         NuitonValidatorContext<O> context = contexts.get(bean);
526         Preconditions.checkState(
527                 context != null,
528                 "Bean " + bean + " was not register in this list validator");
529         return context;
530     }
531 
532 
533     public static class NuitonValidatorContext<O> extends AbstractNuitonValidatorContext<O, BeanListValidator<O>, BeanListValidatorEvent> {
534 
535         public NuitonValidatorContext(O bean, NuitonValidator<O> validator) {
536             setValidator(validator);
537             setBean(bean);
538         }
539 
540         @Override
541         protected BeanListValidatorEvent createEvent(BeanListValidator<O> source,
542                                                      O bean,
543                                                      String field,
544                                                      NuitonValidatorScope scope,
545                                                      String[] toAdd,
546                                                      String[] toDelete) {
547             return new BeanListValidatorEvent(
548                     source,
549                     bean,
550                     field,
551                     scope,
552                     toAdd,
553                     toDelete
554             );
555         }
556     }
557 }