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.opensymphony.xwork2.validator.ValidationException;
25  import com.opensymphony.xwork2.validator.validators.FieldExpressionValidator;
26  import org.apache.commons.lang3.builder.HashCodeBuilder;
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.Collections;
33  import java.util.List;
34  
35  /**
36   * Un validateur basé sur {@link FieldExpressionValidator} qui valide une clef
37   * unique sur une collection.
38   *
39   * Le {@link #fieldName} sert à récupérer la propriété de type de collection du
40   * bean.
41   *
42   * @author Tony Chemit - chemit@codelutin.com
43   */
44  public class CollectionUniqueKeyValidator extends NuitonFieldValidatorSupport {
45  
46      private static final Log log = LogFactory.getLog(CollectionUniqueKeyValidator.class);
47  
48      /**
49       * pour indiquer la propriété qui contient la liste à valider.
50       *
51       * Si cette prorpiété n'est pas renseignée alors on utilise la
52       * {@link #getFieldName()} pour obtenir la collection.
53       *
54       * Cela permet d'effectuer une validation si une collection mais portant
55       * en fait sur un autre champs
56       *
57       * @since 1.5
58       */
59      protected String collectionFieldName;
60  
61      /**
62       * la liste des propriétés d'une entrée de la collection qui définit la
63       * clef unique.
64       */
65      protected String[] keys;
66  
67      /**
68       * Une propriété optionnelle pour valider que l'objet reflétée par cette
69       * propriété ne viole pas l'intégrité de la clef unique.
70       * Cela permet de valider l'unicité sans que l'objet soit dans la collection
71       */
72      protected String againstProperty;
73  
74      /**
75       * Une propriété optionnelle pour utiliser l'objet en cours de validation pour
76       * valider que l'objet reflétée par cette propriété ne viole pas l'intégrité de la clef unique.
77       * Cela permet de valider l'unicité sans que l'objet soit dans la collection
78       */
79      protected boolean againstMe;
80  
81      /**
82       * Lors de l'utilisation de la againstProperty et qu'un ne peut pas utiliser
83       * le equals sur l'objet, on peut spécifier une expression pour exclure des
84       * tests lors de la recherche de la violation de clef unique.
85       */
86      protected String againstIndexExpression;
87  
88      /**
89       * Pour ne pas traiter les valeurs nulles (si positionné à {@code true} les
90       * valeurs nulles ne sont pas considérée comme unique).
91       *
92       * @since 2.2.6
93       */
94      protected boolean nullValueSkipped;
95  
96      public String getCollectionFieldName() {
97          return collectionFieldName;
98      }
99  
100     public void setCollectionFieldName(String collectionFieldName) {
101         this.collectionFieldName = collectionFieldName;
102     }
103 
104     public String[] getKeys() {
105         return keys;
106     }
107 
108     public boolean getAgainstMe() {
109         return againstMe;
110     }
111 
112     public void setKeys(String[] keys) {
113         if (keys != null && keys.length == 1 && keys[0].indexOf(',') != -1) {
114             this.keys = keys[0].split(",");
115         } else {
116             this.keys = keys;
117         }
118     }
119 
120     public String getAgainstProperty() {
121         return againstProperty;
122     }
123 
124     public void setAgainstProperty(String againstProperty) {
125         this.againstProperty = againstProperty;
126     }
127 
128     public String getAgainstIndexExpression() {
129         return againstIndexExpression;
130     }
131 
132     public void setAgainstIndexExpression(String againstIndexExpression) {
133         this.againstIndexExpression = againstIndexExpression;
134     }
135 
136     public void setAgainstMe(boolean againstMe) {
137         this.againstMe = againstMe;
138     }
139 
140     public boolean isNullValueSkipped() {
141         return nullValueSkipped;
142     }
143 
144     public void setNullValueSkipped(boolean nullValueSkipped) {
145         this.nullValueSkipped = nullValueSkipped;
146     }
147 
148     @Override
149     public void validateWhenNotSkip(Object object) throws ValidationException {
150 
151         if (keys == null || keys.length == 0) {
152             throw new ValidationException("no unique keys defined");
153         }
154 
155         String fieldName = getFieldName();
156 
157         Collection<?> col = getCollection(object);
158 
159         if (log.isDebugEnabled()) {
160             log.debug("collection found : " + col);
161         }
162         Object againstBean = againstProperty == null ? null :
163                              getFieldValue(againstProperty, object);
164 
165         if (log.isDebugEnabled()) {
166             log.debug("againtBean = " + againstBean);
167         }
168         Integer againstIndex = (Integer) (againstIndexExpression == null ?
169                                           -1 :
170                                           getFieldValue(againstIndexExpression, object));
171         if (againstIndex == null) {
172             againstIndex = -1;
173         }
174         if (!againstMe && againstBean == null && col.size() < 2) {
175             // la liste ne contient pas deux entrées donc c'est valide
176             return;
177         }
178 
179         if (againstMe) {
180             // try on this object
181             againstBean = object;
182             if (log.isDebugEnabled()) {
183                 log.debug("againtBean from me = " + againstBean);
184             }
185         }
186 
187 
188         boolean answer = true;
189 
190         Integer againstHashCode = againstBean == null ?
191                                   null : getUniqueKeyHashCode(againstBean);
192         if (log.isDebugEnabled()) {
193             log.debug("hash for new key " + againstHashCode);
194         }
195 
196         if (againstHashCode == null && nullValueSkipped) {
197 
198             // clef nulle, donc pas d'erreur vu que le flag a ete positionne
199             return;
200         }
201         List<Integer> hashCodes = new ArrayList<Integer>();
202 
203         int index = 0;
204         for (Object o : col) {
205             Integer hash = getUniqueKeyHashCode(o);
206 
207             if (log.isDebugEnabled()) {
208                 log.debug("hash for object  " + o + " = " + hash);
209             }
210 
211             if (hash == null && nullValueSkipped) {
212 
213                 // clef nulle, donc pas d'erreur vu que le flag a ete positionne
214                 continue;
215             }
216 
217             if (againstBean == null) {
218                 if (hashCodes.contains(hash)) {
219 
220                     // on a deja rencontre cette clef unique,
221                     // donc la validation a echouee
222                     answer = false;
223                     if (log.isDebugEnabled()) {
224                         log.debug("Found same hashcode, not unique!");
225                     }
226                     break;
227                 }
228             } else {
229                 // utilisation de againstBean
230                 if (againstIndex != -1) {
231                     if (index != againstIndex &&
232                         hash.equals(againstHashCode)) {
233                         // on a deja rencontre cette clef unique,
234                         // donc la validation a echouee
235                         answer = false;
236                         break;
237                     }
238                 } else {
239                     if (!againstBean.equals(o) &&
240                         hash.equals(againstHashCode)) {
241                         // on a deja rencontre cette clef unique,
242                         // donc la validation a echouee
243                         answer = false;
244                         break;
245                     }
246                 }
247             }
248             // nouveau hashcode enregistre
249             hashCodes.add(hash);
250             // index suivant
251             index++;
252         }
253 
254         if (!answer) {
255             addFieldError(fieldName, object);
256         }
257     }
258 
259     /**
260      * Calcule pour une entrée donné, le hash de la clef unique
261      *
262      * @param o l'entree de la collection dont on va calculer le hash de
263      *          la clef unique
264      * @return le hashCode calclé de la clef unique sur l'entrée donné
265      * @throws ValidationException if any pb to retreave properties values
266      */
267     protected Integer getUniqueKeyHashCode(Object o)
268             throws ValidationException {
269         // calcul du hash à la volée
270         HashCodeBuilder builder = new HashCodeBuilder();
271         for (String key : keys) {
272             Object property = getFieldValue(key, o);
273             if (property == null && nullValueSkipped) {
274                 // une valeur nulle a ete trouvee et le flag de skip est positionne
275                 // on retourne alors null comme cas limite
276                 return null;
277             }
278 
279             builder.append(property);
280         }
281         return builder.toHashCode();
282     }
283 
284     /**
285      * @param object the incoming object containing the collection to test
286      * @return the collection of the incoming object given by the fieldName
287      * property
288      * @throws ValidationException if any pb to retreave the collection
289      */
290     protected Collection<?> getCollection(Object object)
291             throws ValidationException {
292         String fieldName = getCollectionFieldName();
293         if (fieldName == null || fieldName.trim().isEmpty()) {
294             // on travaille directement sur le fieldName
295             fieldName = getFieldName();
296         }
297 
298         Object obj = null;
299 
300         // obtain the collection to test
301         try {
302             obj = getFieldValue(fieldName, object);
303         } catch (ValidationException e) {
304             throw e;
305         } catch (Exception e) {
306             // let this pass, but it will be logged right below
307         }
308 
309         if (obj == null) {
310             // la collection est nulle, donc on renvoie une collection vide
311             return Collections.emptyList();
312         }
313 
314         if (!Collection.class.isInstance(obj)) {
315             throw new ValidationException("field " + fieldName +
316                                           " is not a collection type! (" + obj.getClass() + ')');
317         }
318         return (Collection<?>) obj;
319     }
320 
321     @Override
322     public String getValidatorType() {
323         return "collectionUniqueKey";
324     }
325 }