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