View Javadoc
1   package org.nuiton.validator.bean;
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.collect.Lists;
25  import com.google.common.collect.Maps;
26  import org.apache.commons.beanutils.ConversionException;
27  import org.apache.commons.beanutils.Converter;
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.nuiton.converter.ConverterUtil;
31  import org.nuiton.validator.NuitonValidator;
32  import org.nuiton.validator.NuitonValidatorResult;
33  import org.nuiton.validator.NuitonValidatorScope;
34  import org.nuiton.validator.bean.list.BeanListValidator;
35  import org.nuiton.validator.bean.simple.SimpleBeanValidator;
36  
37  import java.beans.Introspector;
38  import java.util.Collections;
39  import java.util.HashSet;
40  import java.util.Iterator;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.Set;
44  
45  /**
46   * Defines a context of validation used for a single bean.
47   *
48   * {@link SimpleBeanValidator} will then used one of this object and
49   * {@link BeanListValidator} as many as it contains beans.
50   *
51   * This object box a {@link NuitonValidator} to get validation state each time
52   * a higher validator requires it.
53   *
54   * It also offers the way to create events (merge logic
55   *
56   * @param <O> type of bean to validate
57   * @param <V> type of bean validator used
58   * @param <E> type of event to create
59   * @author Tony Chemit - chemit@codelutin.com
60   * @since 2.5.2
61   */
62  public abstract class AbstractNuitonValidatorContext<O, V, E> {
63  
64      /** Logger. */
65      private static final Log log =
66              LogFactory.getLog(AbstractNuitonValidatorContext.class);
67  
68      /** Bean to validate. */
69      protected O bean;
70  
71      /**
72       * State of validation (keep all messages of validation for the filled
73       * bean).
74       */
75      protected NuitonValidatorResult messages;
76  
77      /** Validator. */
78      protected NuitonValidator<O> validator;
79  
80      /** map of conversion errors detected by this validator */
81      protected final Map<String, String> conversionErrors;
82  
83      /**
84       * State to know if the validator can be used (we keep this state for
85       * performance reasons : do not want to compute this value each time a
86       * validation is asked...).
87       */
88      protected boolean canValidate;
89  
90      protected abstract E createEvent(V source,
91                                       O bean,
92                                       String field,
93                                       NuitonValidatorScope scope,
94                                       String[] toAdd,
95                                       String[] toDelete);
96  
97      public AbstractNuitonValidatorContext() {
98          conversionErrors = Maps.newTreeMap();
99      }
100 
101     public O getBean() {
102         return bean;
103     }
104 
105     public void setBean(O bean) {
106         if (log.isDebugEnabled()) {
107             log.debug(this + " : " + bean);
108         }
109 
110         // clean conversions of previous bean
111         conversionErrors.clear();
112         this.bean = bean;
113 
114         setCanValidate(!validator.getEffectiveFields().isEmpty() && bean != null);
115     }
116 
117     public NuitonValidator<O> getValidator() {
118         return validator;
119     }
120 
121     public NuitonValidatorResult getMessages() {
122         return messages;
123     }
124 
125     public boolean isCanValidate() {
126         return canValidate;
127     }
128 
129     public void setCanValidate(boolean canValidate) {
130         this.canValidate = canValidate;
131     }
132 
133     public boolean isValid() {
134         return messages == null || messages.isValid();
135     }
136 
137     public boolean hasFatalErrors() {
138         boolean result = messages != null && messages.hasFatalMessages();
139         return result;
140     }
141 
142     public boolean hasErrors() {
143         boolean result = messages != null && messages.hasErrorMessagess();
144         return result;
145     }
146 
147     public boolean hasWarnings() {
148         boolean result = messages != null && messages.hasWarningMessages();
149         return result;
150     }
151 
152     public boolean hasInfos() {
153         boolean result = messages != null && messages.hasInfoMessages();
154         return result;
155     }
156 
157     public boolean isValid(String fieldName) {
158 
159         // field is valid if no fatal messages nor error messages
160         boolean result = !(
161                 messages.hasMessagesForScope(fieldName, NuitonValidatorScope.FATAL) ||
162                 messages.hasMessagesForScope(fieldName, NuitonValidatorScope.ERROR));
163 
164         return result;
165     }
166 
167     public NuitonValidatorScope getHighestScope(String field) {
168 
169         NuitonValidatorScope scope = messages.getFieldHighestScope(field);
170         return scope;
171     }
172 
173     public void setValidator(NuitonValidator<O> validator) {
174         this.validator = validator;
175     }
176 
177     public NuitonValidatorResult validate() {
178         NuitonValidatorResult result = validator.validate(bean);
179 
180         // treate conversion errors
181         // reinject them
182         for (Map.Entry<String, String> entry : conversionErrors.entrySet()) {
183 
184 
185             // remove from validation, errors occurs on this field
186             String field = entry.getKey();
187 
188 
189             List<String> errors = result.getErrorMessages(field);
190 
191             String conversionError = entry.getValue();
192             if (errors != null) {
193                 errors.clear();
194                 errors.add(conversionError);
195             } else {
196                 errors = Collections.singletonList(conversionError);
197             }
198 
199             result.setMessagesForScope(NuitonValidatorScope.ERROR, field, errors);
200         }
201         return result;
202     }
203 
204     /**
205      * Convert a value.
206      *
207      * If an error occurs, then add an error in validator.
208      *
209      * @param <T>        the type of conversion
210      * @param fieldName  the name of the bean property
211      * @param value      the value to convert
212      * @param valueClass the type of converted value
213      * @return the converted value, or null if conversion was not ok
214      */
215     @SuppressWarnings({"unchecked"})
216     public <T> T convert(String fieldName, String value, Class<T> valueClass) {
217         if (fieldName == null) {
218             throw new IllegalArgumentException("fieldName can not be null");
219         }
220         if (valueClass == null) {
221             throw new IllegalArgumentException("valueClass can not be null");
222         }
223 
224         // on ne convertit pas si il y a un bean et que le resultat de la
225         // validation pourra etre affiche quelque part
226         if (!isCanValidate() || value == null) {
227             return null;
228         }
229 
230         // remove the previous conversion error for the field
231         conversionErrors.remove(fieldName);
232 
233         T result;
234         try {
235             Converter converter = ConverterUtil.getConverter(valueClass);
236             if (converter == null) {
237                 throw new RuntimeException(
238                         "could not find converter for the type " + valueClass);
239             }
240             result = converter.convert(valueClass, value);
241             /* Why this test ? if (result != null && !value.equals(result.toString())) {
242             conversionErrors.put(fieldName, "error.convertor." + Introspector.decapitalize(valueClass.getSimpleName()));
243             result = null;
244             validate();
245             }*/
246         } catch (ConversionException e) {
247             // get
248             String s = Introspector.decapitalize(valueClass.getSimpleName());
249             conversionErrors.put(fieldName, "error.convertor." + s);
250             throw e;
251         }
252         return result;
253     }
254 
255     public List<E> mergeMessages(V beanValidator,
256                                  NuitonValidatorResult newMessages) {
257 
258         if (newMessages == null && messages == null) {
259 
260             // no messages ever registred and ask to delete them, so nothing
261             // to do
262             return null;
263         }
264 
265         Set<NuitonValidatorScope> scopes = getValidator().getEffectiveScopes();
266 
267         // list of events to send after the merge of messages
268         List<E> events = Lists.newArrayList();
269 
270         for (NuitonValidatorScope scope : scopes) {
271 
272             // do the merge at scope level
273             mergeMessages(beanValidator, scope, newMessages, events);
274 
275         }
276 
277         if (newMessages != null) {
278 
279             //TODO tchemit 2011-01-23 Perharps it will necessary to clear the messages for memory performance ?
280 
281             // finally keep the new messages as the current messages
282             this.messages = newMessages;
283         }
284 
285         return events;
286     }
287 
288     protected void mergeMessages(V beanValidator,
289                                  NuitonValidatorScope scope,
290                                  NuitonValidatorResult newMessages,
291                                  List<E> events) {
292 
293 
294         if (newMessages == null) {
295 
296             // special case to empty all messages
297 
298             List<String> fieldsForScope = messages.getFieldsForScope(scope);
299 
300             for (String field : fieldsForScope) {
301                 List<String> messagesForScope = messages.getMessagesForScope(field, scope);
302                 events.add(createEvent(beanValidator, bean, field, scope, null, messagesForScope.toArray(new String[messagesForScope.size()])));
303             }
304 
305             // suppress all messages for this scope
306             messages.clearMessagesForScope(scope);
307 
308 
309         } else {
310 
311             List<String> newFields = newMessages.getFieldsForScope(scope);
312 
313             if (messages == null) {
314 
315                 // first time of a merge, just add new messages
316 
317                 for (String field : newFields) {
318                     List<String> messagesForScope = newMessages.getMessagesForScope(field, scope);
319                     events.add(createEvent(beanValidator, bean, field, scope, messagesForScope.toArray(new String[messagesForScope.size()]), null));
320                 }
321 
322                 // nothing else to do
323                 return;
324             }
325 
326             List<String> oldFields = messages.getFieldsForScope(scope);
327 
328             Iterator<String> itr;
329 
330             // detects field with only new messages
331             itr = newFields.iterator();
332             while (itr.hasNext()) {
333                 String newField = itr.next();
334 
335                 if (!oldFields.contains(newField)) {
336 
337                     // this fields has now messages but not before : new messages
338                     List<String> messagesForScope = newMessages.getMessagesForScope(newField, scope);
339                     events.add(createEvent(beanValidator, bean, newField, scope, messagesForScope.toArray(new String[messagesForScope.size()]), null));
340 
341                     // treated field
342                     itr.remove();
343                 }
344             }
345 
346             // detects fields with only obsolete messages
347             itr = oldFields.iterator();
348             while (itr.hasNext()) {
349                 String oldField = itr.next();
350 
351                 if (!newFields.contains(oldField)) {
352 
353                     // this fields has no more messages
354                     List<String> messagesForScope = messages.getMessagesForScope(oldField, scope);
355                     events.add(createEvent(beanValidator, bean, oldField, scope, null, messagesForScope.toArray(new String[messagesForScope.size()])));
356 
357                     // treated field
358                     itr.remove();
359                 }
360             }
361 
362             // now deal with mixte field (toAdd and toDelete)
363             for (String field : newFields) {
364 
365                 List<String> newMessagesForScope = newMessages.getMessagesForScope(field, scope);
366                 List<String> oldMessagesForScope = messages.getMessagesForScope(field, scope);
367 
368                 // get old obsoletes messages to delete
369                 Set<String> toDelete = new HashSet<String>(oldMessagesForScope);
370                 toDelete.removeAll(newMessagesForScope);
371 
372                 // get new messages to add
373                 Set<String> toAdd = new HashSet<String>(newMessagesForScope);
374                 toAdd.removeAll(oldMessagesForScope);
375 
376                 events.add(createEvent(
377                         beanValidator,
378                         bean,
379                         field,
380                         scope,
381                         toAdd.isEmpty() ? null : toAdd.toArray(new String[toAdd.size()]),
382                         toDelete.isEmpty() ? null : toDelete.toArray(new String[toDelete.size()])
383                 ));
384 
385             }
386         }
387     }
388 }