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 }