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.xwork2.field;
23  
24  import com.google.common.base.Objects;
25  import com.opensymphony.xwork2.util.ValueStack;
26  import com.opensymphony.xwork2.validator.ValidationException;
27  import com.opensymphony.xwork2.validator.validators.FieldExpressionValidator;
28  import org.apache.commons.lang3.builder.HashCodeBuilder;
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  
32  import java.util.Collection;
33  import java.util.Collections;
34  import java.util.Comparator;
35  import java.util.Set;
36  import java.util.TreeSet;
37  
38  /**
39   * Un validateur basé sur {@link FieldExpressionValidator} qui valide sur une
40   * collection de propriéte.
41   *
42   * @author Tony Chemit - chemit@codelutin.com
43   */
44  public class CollectionFieldExpressionValidator extends NuitonFieldExpressionValidator {
45  
46      private static final Log log = LogFactory.getLog(CollectionFieldExpressionValidator.class);
47  
48      public enum Mode {
49  
50          /** au moins une entrée de la collection doit etre valide */
51          AT_LEAST_ONE,
52          /** exactement une entrée dela collection doit être valide */
53          EXACTLY_ONE,
54          /** toutes les valeurs de la collection doivent etre valides */
55          ALL,
56          /** aucune valeur de la collection doivent etre valides */
57          NONE,
58          /** detection de clef unique */
59          UNIQUE_KEY
60      }
61  
62      /** le mode de validation sur la liste */
63      protected Mode mode;
64  
65      /**
66       * pour indiquer la propriété qui contient la liste à valider.
67       *
68       * Si cette prorpiété n'est pas renseignée alors on utilise la
69       * {@link #getFieldName()} pour obtenir la collection.
70       *
71       * Cela permet d'effectuer une validation si une collection mais portant
72       * en fait sur un autre champs
73       *
74       * @since 1.5
75       */
76      protected String collectionFieldName;
77  
78      /**
79       * drapeau pour utiliser le contexte de parcours pour valider
80       * l'expression, on dispose donc alors des variables previous, current,
81       * index, size et empty dans l'expression.
82       *
83       * Sinon l'expression s'applique directement sur l'entrée courant dans le
84       * parcours sans préfixe.
85       */
86      protected boolean useSensitiveContext;
87  
88      /**
89       * expression a valider sur la premiètre entrée de la collection.
90       *
91       * Note : Pour le moment, on autorise uniquement cela en mode ALL.
92       */
93      protected String expressionForFirst;
94  
95      /**
96       * expression a valider sur la dernière entrée de la collection.
97       *
98       * Note : Pour le moment, on autorise uniquement cela en mode ALL.
99       */
100     protected String expressionForLast;
101 
102     /**
103      * la liste des propriétés d'une entrée de la collection qui définit la
104      * clef unique (en mode UNIQUE_KEY).
105      */
106     protected String[] keys;
107 
108     /** le context de parcours */
109     protected WalkerContext c;
110 
111     private boolean useFirst, useLast;
112 
113     public Mode getMode() {
114         return mode;
115     }
116 
117     public void setMode(Mode mode) {
118         this.mode = mode;
119     }
120 
121     public String getCollectionFieldName() {
122         return collectionFieldName;
123     }
124 
125     public void setCollectionFieldName(String collectionFieldName) {
126         this.collectionFieldName = collectionFieldName;
127     }
128 
129     public boolean isUseSensitiveContext() {
130         return useSensitiveContext;
131     }
132 
133     public void setUseSensitiveContext(boolean useSensitiveContext) {
134         this.useSensitiveContext = useSensitiveContext;
135     }
136 
137     public String getExpressionForFirst() {
138         return expressionForFirst;
139     }
140 
141     public void setExpressionForFirst(String expressionForFirst) {
142         this.expressionForFirst = expressionForFirst;
143     }
144 
145     public String getExpressionForLast() {
146         return expressionForLast;
147     }
148 
149     public void setExpressionForLast(String expressionForLast) {
150         this.expressionForLast = expressionForLast;
151     }
152 
153     public String[] getKeys() {
154         return keys;
155     }
156 
157     public void setKeys(String[] keys) {
158         if (keys != null && keys.length == 1 && keys[0].contains(",")) {
159             this.keys = keys[0].split(",");
160         } else {
161             this.keys = keys;
162         }
163     }
164 
165     @Override
166     public void validateWhenNotSkip(Object object) throws ValidationException {
167         if (mode == null) {
168             throw new ValidationException("no mode defined!");
169         }
170         useFirst = expressionForFirst != null && !expressionForFirst.trim().isEmpty();
171         useLast = expressionForLast != null && !expressionForLast.trim().isEmpty();
172 
173         if (useFirst && mode != Mode.ALL) {
174             throw new ValidationException("can  only use expressionForFirst in " +
175                                                   "mode ALL but was " + mode);
176         }
177         if (useLast && mode != Mode.ALL) {
178             throw new ValidationException("can  only use expressionForLast in " +
179                                                   "mode ALL but was " + mode);
180         }
181 
182         String fieldName = getFieldName();
183 
184         Collection<?> col = getCollection(object);
185 
186         if (useSensitiveContext) {
187             c = new WalkerContext(col.size());
188         }
189 
190         boolean answer;
191 
192         boolean pop = false;
193 
194         if (!stack.getRoot().contains(object)) {
195             stack.push(object);
196             pop = true;
197         }
198 
199         switch (mode) {
200             case ALL:
201                 answer = validateAllEntries(col);
202                 break;
203             case AT_LEAST_ONE:
204                 answer = validateAtLeastOneEntry(col);
205                 break;
206             case EXACTLY_ONE:
207                 answer = validateExtacltyOneEntry(col);
208                 break;
209             case NONE:
210                 answer = validateNoneEntry(col);
211                 break;
212             case UNIQUE_KEY:
213                 if (keys == null || keys.length == 0) {
214                     throw new ValidationException("no unique keys defined");
215                 }
216                 answer = validateUniqueKey(col);
217                 break;
218 
219             default:
220                 // should never come here...
221                 answer = false;
222         }
223 
224         if (!answer) {
225             addFieldError(fieldName, object);
226         }
227         if (pop) {
228             stack.pop();
229         }
230     }
231 
232     protected ValueStack stack;
233 
234     @Override
235     public void setValueStack(ValueStack stack) {
236         super.setValueStack(stack);
237         this.stack = stack;
238     }
239 
240     @Override
241     public String getMessage(Object object) {
242         boolean pop = false;
243 
244         if (useSensitiveContext && !stack.getRoot().contains(c)) {
245             stack.push(c);
246             pop = true;
247         }
248         String message = super.getMessage(object);
249 
250         if (pop) {
251             stack.pop();
252         }
253         return message;
254     }
255 
256     protected Boolean validateAllEntries(Collection<?> col) throws ValidationException {
257         boolean answer = true;
258         for (Object entry : col) {
259             answer = validateOneEntry(entry);
260             if (!answer) {
261                 // validation on one entry has failed
262                 // no need to continue
263                 break;
264             }
265         }
266         return answer;
267     }
268 
269     protected Boolean validateNoneEntry(Collection<?> col) throws ValidationException {
270         boolean answer = true;
271         for (Object entry : col) {
272             boolean b = validateOneEntry(entry);
273             if (b) {
274                 // one entry has sucessed, validation has failed
275                 // no need to continue
276                 answer = false;
277                 break;
278             }
279         }
280         return answer;
281     }
282 
283     protected Boolean validateAtLeastOneEntry(Collection<?> col) throws ValidationException {
284         boolean answer = false;
285         for (Object entry : col) {
286             answer = validateOneEntry(entry);
287             if (answer) {
288                 // one entry was succes, validation is ok,
289                 // no need to continue
290                 break;
291             }
292         }
293         return answer;
294     }
295 
296     protected Boolean validateExtacltyOneEntry(Collection<?> col) throws ValidationException {
297         int count = 0;
298         for (Object entry : col) {
299             boolean answer = validateOneEntry(entry);
300             if (answer) {
301                 // one entry has succed
302                 count++;
303                 if (count > 1) {
304                     // more than one entriy was successfull
305                     // so validation has failed
306                     break;
307                 }
308 
309             }
310         }
311         return count == 1;
312     }
313 
314 //    protected Boolean validateUniqueKey(Collection<?> col) throws ValidationException {
315 //        boolean answer = true;
316 //
317 //        Set<Integer> hashCodes = new HashSet<Integer>();
318 //        int index = -1;
319 //        for (Object entry : col) {
320 //            index++;
321 //            // construction du hash de la clef d'unicite
322 //            Integer hash = getUniqueKeyHashCode(entry);
323 //            if (!hashCodes.contains(hash)) {
324 //                hashCodes.add(hash);
325 //                continue;
326 //            }
327 //            // une entree avec ce hash a deja ete trouvee
328 //            // on est donc en violation sur la clef unique
329 //            answer = false;
330 //            if (log.isDebugEnabled()) {
331 //                log.debug("duplicated uniquekey " + hash + " for entry " + index);
332 //            }
333 //        }
334 //        hashCodes.clear();
335 //        return answer;
336 //    }
337 
338     protected Boolean validateUniqueKey(Collection<?> col) throws ValidationException {
339         boolean answer = true;
340 
341         Comparator<? super Object> comparator1 = getComparator();
342         Set<? super Object> hashCodes = new TreeSet<Object>(comparator1);
343         int index = -1;
344         for (Object entry : col) {
345             index++;
346             boolean wasAdded = hashCodes.add(entry);
347             if (!wasAdded) {
348                 answer = false;
349                 if (log.isDebugEnabled()) {
350                     log.debug("duplicated unique entry at position: " + index);
351                 }
352                 break;
353             }
354         }
355         return answer;
356     }
357 
358     protected boolean validateOneEntry(Object object) throws ValidationException {
359 
360         Boolean answer = Boolean.FALSE;
361 
362         boolean extraExpression = false;
363 
364         if (useSensitiveContext) {
365             c.addCurrent(object);
366             object = c;
367 
368             if (c.isFirst() && useFirst) {
369                 // on valide l'expression sur la premiètre entrée
370                 answer = evaluateExpression(expressionForFirst, object);
371                 extraExpression = true;
372             }
373             if (c.isLast() && useLast) {
374                 // on valide l'expression sur la dernière entrée
375                 answer = (!extraExpression || answer) && evaluateExpression(expressionForLast, object);
376                 extraExpression = true;
377             }
378         }
379 
380         answer = (!extraExpression || answer) && evaluateExpression(getExpression(), object);
381 
382         return answer;
383     }
384 
385     protected boolean evaluateExpression(String expression, Object object) throws ValidationException {
386         Object obj = null;
387         try {
388             obj = getFieldValue(expression, object);
389         } catch (ValidationException e) {
390             throw e;
391         } catch (Exception e) {
392             log.error(e.getMessage(), e);
393             // let this pass, but it will be logged right below
394         }
395 
396         Boolean answer = Boolean.FALSE;
397 
398         if (obj != null && obj instanceof Boolean) {
399             answer = (Boolean) obj;
400         } else {
401             log.warn("Got result of " + obj + " when trying to get Boolean for expression " + expression);
402         }
403         return answer;
404     }
405 
406     /**
407      * @param object the incoming object containing the collection to test
408      * @return the collection of the incoming object given by the fieldName property
409      * @throws ValidationException if any pb to retreave the collection
410      */
411     protected Collection<?> getCollection(Object object) throws ValidationException {
412         String fieldName = getCollectionFieldName();
413         if (fieldName == null || fieldName.trim().isEmpty()) {
414             // on travaille directement sur le fieldName
415             fieldName = getFieldName();
416         }
417 
418         Object obj = null;
419 
420         // obtain the collection to test
421         try {
422             obj = getFieldValue(fieldName, object);
423         } catch (ValidationException e) {
424             throw e;
425         } catch (Exception e) {
426             // let this pass, but it will be logged right below
427         }
428 
429         if (obj == null) {
430             // la collection est nulle, donc on renvoie une collection vide
431             return Collections.emptyList();
432         }
433 
434         if (!Collection.class.isInstance(obj)) {
435             throw new ValidationException("field " + fieldName + " is not a collection type! (" + obj.getClass() + ")");
436         }
437         return (Collection<?>) obj;
438     }
439 
440     /**
441      * Calcule pour une entrée donné, le hash de la clef unique
442      *
443      * @param o l'entree de la collection dont on va calculer le hash de la clef unique
444      * @return le hashCode calclé de la clef unique sur l'entrée donné
445      * @throws ValidationException if any pb to retreave properties values
446      */
447     protected Integer getUniqueKeyHashCode(Object o) throws ValidationException {
448         // calcul du hash à la volée
449         HashCodeBuilder builder = new HashCodeBuilder();
450         for (String key : keys) {
451             Object property = getFieldValue(key, o);
452             if (log.isDebugEnabled()) {
453                 log.debug("key " + key + " : " + property);
454             }
455             builder.append(property);
456         }
457         return builder.toHashCode();
458     }
459 
460     @Override
461     public String getValidatorType() {
462         return "collectionFieldExpression";
463     }
464 
465     public class WalkerContext {
466 
467         protected final int size;
468 
469         public WalkerContext(int size) {
470             this.size = size;
471         }
472 
473         protected int index = -1;
474 
475         protected Object current;
476 
477         protected Object previous;
478 
479         public void addCurrent(Object current) {
480             index++;
481             previous = this.current;
482             this.current = current;
483         }
484 
485         public Object getCurrent() {
486             return current;
487         }
488 
489         public int getIndex() {
490             return index;
491         }
492 
493         public Object getPrevious() {
494             return previous;
495         }
496 
497         public int getSize() {
498             return size;
499         }
500 
501         public boolean isEmpty() {
502             return size == 0;
503         }
504 
505         public boolean isFirst() {
506             return index == 0;
507         }
508 
509         public boolean isLast() {
510             return index == size - 1;
511         }
512     }
513 
514     Comparator<? super Object> comparator;
515 
516     private Comparator<? super Object> getComparator() {
517         if (comparator == null) {
518             comparator = new MyComparator<Object>(keys);
519         }
520         return comparator;
521     }
522 
523     private class MyComparator<O> implements Comparator<O> {
524 
525         private final String[] keys;
526 
527         public MyComparator(String... keys) {
528             this.keys = keys;
529         }
530 
531         @Override
532         public int compare(O o1, O o2) {
533 
534             boolean equals = true;
535 
536             for (String key : keys) {
537 
538                 Object property1 = getPropertyValue(key, o1);
539                 Object property2 = getPropertyValue(key, o2);
540 
541                 equals = Objects.equal(property1, property2);
542 
543                 if (!equals) {
544                     break;
545                 }
546             }
547 
548             return equals ? 0 : -1;
549         }
550     }
551 
552     protected Object getPropertyValue(String key, Object o) {
553 
554         try {
555             return getFieldValue(key, o);
556 
557         } catch (ValidationException e) {
558             if (log.isErrorEnabled()) {
559                 log.error("Can't get property '" + key + "'value on oject: " + o);
560             }
561             return null;
562         }
563     }
564 }