1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.student.common.ui.client.validator;
17
18 import com.google.gwt.i18n.client.DateTimeFormat;
19
20 import org.kuali.student.common.assembly.data.ConstraintMetadata;
21 import org.kuali.student.common.assembly.data.Data;
22 import org.kuali.student.common.assembly.data.Metadata;
23 import org.kuali.student.common.assembly.data.QueryPath;
24 import org.kuali.student.common.assembly.data.Data.DataType;
25 import org.kuali.student.common.assembly.data.Data.StringKey;
26 import org.kuali.student.common.ui.client.application.Application;
27 import org.kuali.student.common.ui.client.configurable.mvc.FieldDescriptor;
28 import org.kuali.student.common.ui.client.mvc.DataModel;
29 import org.kuali.student.common.ui.client.mvc.DataModelDefinition;
30 import org.kuali.student.common.ui.client.util.UtilConstants;
31 import org.kuali.student.common.util.MessageUtils;
32 import org.kuali.student.common.validation.dto.ValidationResultInfo;
33 import org.kuali.student.common.validator.DateParser;
34
35 import java.util.*;
36
37 import static org.kuali.student.common.assembly.data.MetadataInterrogator.*;
38 import static org.kuali.student.common.ui.client.validator.ValidationMessageKeys.*;
39
40 public class DataModelValidator {
41
42 private static final String RUNTIME_DELETED_KEY = "_runtimeData/deleted";
43
44 private DateParser dateParser = null;
45 private boolean validateNextState = false;
46
47
48
49
50 public DateParser getDateParser() {
51 return dateParser;
52 }
53
54
55
56
57 public void setDateParser(DateParser dateParser) {
58 this.dateParser = dateParser;
59 }
60
61
62
63
64
65
66
67
68 public List<ValidationResultInfo> validate(final DataModel model) {
69 validateNextState = false;
70 List<ValidationResultInfo> results = new ArrayList<ValidationResultInfo>();
71 DataModelDefinition def = (DataModelDefinition) model.getDefinition();
72 doValidate(model, def.getMetadata(), new QueryPath(), results);
73 return results;
74 }
75
76
77
78
79
80
81
82
83 public List<ValidationResultInfo> validateNextState(final DataModel model) {
84 validateNextState = true;
85 List<ValidationResultInfo> results = new ArrayList<ValidationResultInfo>();
86 DataModelDefinition def = (DataModelDefinition) model.getDefinition();
87 doValidate(model, def.getMetadata(), new QueryPath(), results);
88
89 return results;
90 }
91
92
93
94
95
96
97
98
99
100 public List<ValidationResultInfo> validateForMetadata(Metadata metadata, final DataModel model) {
101 validateNextState = true;
102 List<ValidationResultInfo> results = new ArrayList<ValidationResultInfo>();
103 doValidate(model, metadata, new QueryPath(), results);
104 return results;
105 }
106
107
108
109
110
111
112
113
114 public List<ValidationResultInfo> validate(FieldDescriptor fd,
115 DataModel model) {
116 validateNextState = false;
117 List<ValidationResultInfo> results = new ArrayList<ValidationResultInfo>();
118 if (fd != null && fd.getMetadata() != null && fd.getFieldKey() != null) {
119 doValidate(model, fd.getMetadata(), QueryPath.parse(fd.getFieldKey()), results);
120 }
121
122 return results;
123 }
124
125 private void doValidate(final DataModel model, final Metadata meta, final QueryPath path, List<ValidationResultInfo> results) {
126 switch (meta.getDataType()) {
127 case DATA:
128
129 case LIST:
130 doValidateComplex(model, meta, path, results);
131 break;
132 }
133
134 if (meta.getConstraints() != null) {
135 switch (meta.getDataType()) {
136
137 case BOOLEAN:
138 doValidateBoolean(model, meta, path, results);
139 break;
140
141 case DATE:
142
143 case TRUNCATED_DATE:
144 doValidateDate(model, meta, path, results);
145 break;
146
147 case DOUBLE:
148 doValidateDouble(model, meta, path, results);
149 break;
150
151 case FLOAT:
152 doValidateFloat(model, meta, path, results);
153 break;
154
155 case INTEGER:
156 doValidateInteger(model, meta, path, results);
157 break;
158
159 case LONG:
160 doValidateLong(model, meta, path, results);
161 break;
162
163 case STRING:
164 doValidateString(model, meta, path, results);
165 break;
166
167 default:
168
169 }
170 }
171 }
172
173 private void addError(List<ValidationResultInfo> list, QueryPath element, ValidationMessageKeys msgKey, Map<String, Object> constraintInfo) {
174 ValidationResultInfo v = new ValidationResultInfo();
175 String rawMsg = getValidationMessage(msgKey.getKey());
176 v.setElement(element.toString());
177 v.setError(MessageUtils.interpolate(rawMsg, constraintInfo));
178 list.add(v);
179 }
180
181 private void addError(List<ValidationResultInfo> list, QueryPath element, ValidationMessageKeys msgKey, Object value) {
182 ValidationResultInfo v = new ValidationResultInfo();
183 String rawMsg = getValidationMessage(msgKey.getKey());
184 v.setElement(element.toString());
185 v.setError(MessageUtils.interpolate(rawMsg, msgKey.getProperty(), value));
186 list.add(v);
187 }
188
189 protected String getValidationMessage(String msgKey) {
190 return Application.getApplicationContext().getMessage(msgKey);
191 }
192
193 private void addError(List<ValidationResultInfo> list, QueryPath element, ValidationMessageKeys msgKey) {
194 addError(list, element, msgKey.getKey());
195 }
196
197 private void addError(List<ValidationResultInfo> list, QueryPath element, String msgKey) {
198 ValidationResultInfo v = new ValidationResultInfo();
199 v.setElement(element.toString());
200 v.setError(getValidationMessage(msgKey));
201 list.add(v);
202 }
203
204 private void addRangeError(List<ValidationResultInfo> list, QueryPath element, ValidationMessageKeys msgKey, Object minValue, Object maxValue) {
205 Map<String, Object> constraintInfo = new HashMap<String, Object>();
206
207 put(constraintInfo, MIN_VALUE.getProperty(), minValue);
208 put(constraintInfo, MAX_VALUE.getProperty(), maxValue);
209
210 addError(list, element, msgKey, constraintInfo);
211 }
212
213 private boolean isRequiredCheck(Metadata meta) {
214 if (validateNextState) {
215 return (isRequired(meta) || isRequiredForNextState(meta));
216 } else {
217 return isRequired(meta);
218 }
219 }
220
221 private void doValidateString(DataModel model, Metadata meta,
222 QueryPath path, List<ValidationResultInfo> results) {
223
224 Map<QueryPath, Object> values = model.query(path);
225
226 if (values.isEmpty() && isRequiredCheck(meta)) {
227 addError(results, path, REQUIRED);
228 } else {
229 Object[] keys = values.keySet().toArray();
230 for (int keyIndex = 0; keyIndex < keys.length; keyIndex++) {
231 QueryPath element = (QueryPath) keys[keyIndex];
232
233 String s = (values.get(element) == null) ? "" : values.get(element).toString();
234 doValidateString(s, element, meta, results);
235
236 }
237 }
238 }
239
240
241 public void doValidateString(String s, QueryPath element, Metadata meta,
242 List<ValidationResultInfo> results) {
243 if (s.isEmpty() && isRequiredCheck(meta)) {
244 addError(results, element, REQUIRED);
245 } else if (!s.isEmpty()) {
246 if (s.equals(UtilConstants.IMPOSSIBLE_CHARACTERS)) {
247 QueryPath path = new QueryPath();
248 path.add(new StringKey(element.get(0).toString()));
249 addError(results, path, INVALID_VALUE);
250 } else {
251 int len = s.length();
252 Integer minLength = getLargestMinLength(meta);
253 Integer maxLength = getSmallestMaxLength(meta);
254
255 if (minLength != null && maxLength != null) {
256 if (len < minLength || len > maxLength) {
257 addRangeError(results, element, LENGTH_OUT_OF_RANGE, minLength, maxLength);
258 }
259 } else if (minLength != null && len < minLength) {
260 addError(results, element, MIN_LENGTH, minLength);
261 } else if (maxLength != null && len > maxLength) {
262 addError(results, element, MAX_LENGTH, maxLength);
263 }
264
265
266 if (meta.getConstraints() != null) {
267 boolean failed = false;
268 List<ConstraintMetadata> constraints = meta.getConstraints();
269
270 for (int consIdx = 0; consIdx < constraints.size(); consIdx++) {
271 ConstraintMetadata cons = constraints.get(consIdx);
272 if (failed) {
273 break;
274 }
275 String validChars = cons.getValidChars();
276 validChars = (validChars == null) ? "" : validChars.trim();
277 if (!validChars.isEmpty()) {
278 if (validChars.startsWith("regex:")) {
279 validChars = validChars.substring(6);
280 if (!s.matches(validChars)) {
281 if (cons.getValidCharsMessageId() != null) {
282 addError(results, element, cons.getValidCharsMessageId());
283 } else {
284 addError(results, element, VALID_CHARS);
285 }
286 failed = true;
287 break;
288 }
289 } else {
290 for (char c : s.toCharArray()) {
291 if (!validChars.contains(String.valueOf(c))) {
292 if (cons.getValidCharsMessageId() != null) {
293 addError(results, element, cons.getValidCharsMessageId());
294 } else {
295 addError(results, element, VALID_CHARS);
296 }
297 failed = true;
298 break;
299 }
300 }
301 }
302 }
303 }
304 }
305 }
306 }
307
308 }
309
310 private void doValidateInteger(DataModel model, Metadata meta,
311 QueryPath path, List<ValidationResultInfo> results) {
312
313 Map<QueryPath, Object> values = model.query(path);
314
315 if (values.isEmpty() && isRequiredCheck(meta)) {
316 addError(results, path, REQUIRED);
317 } else {
318 Object[] keys = values.keySet().toArray();
319 for (int keyIndex = 0; keyIndex < keys.length; keyIndex++) {
320 QueryPath element = (QueryPath) keys[keyIndex];
321
322 Object o = values.get(element);
323
324 if (o == null) {
325 if (isRequiredCheck(meta)) {
326 addError(results, element, REQUIRED);
327 }
328 } else {
329 Integer i = null;
330 try {
331 i = (o instanceof Integer) ? (Integer) o : Integer.valueOf(o.toString());
332 } catch (Exception ex) {
333 addError(results, element, INTEGER);
334 }
335
336 if (i != null) {
337 Long min = getLargestMinValue(meta);
338 Long max = getSmallestMaxValue(meta);
339
340 if (min != null && max != null) {
341 if (i < min || i > max) {
342 addRangeError(results, element, OUT_OF_RANGE, min, max);
343 }
344 } else if (min != null && i < min) {
345 addError(results, element, MIN_VALUE, min);
346 } else if (max != null && i > max) {
347 addError(results, element, MAX_VALUE, max);
348 }
349 }
350 }
351 }
352 }
353 }
354
355 private void doValidateLong(DataModel model, Metadata meta,
356 QueryPath path, List<ValidationResultInfo> results) {
357
358 Map<QueryPath, Object> values = model.query(path);
359
360 if (values.isEmpty() && isRequiredCheck(meta)) {
361 addError(results, path, REQUIRED);
362 } else {
363 Object[] keys = values.keySet().toArray();
364 for (int keyIndex = 0; keyIndex < keys.length; keyIndex++) {
365 QueryPath element = (QueryPath) keys[keyIndex];
366
367 Object o = values.get(element);
368
369 if (o == null) {
370 if (isRequiredCheck(meta)) {
371 addError(results, element, REQUIRED);
372 }
373 } else {
374 Long i = null;
375 try {
376 i = (o instanceof Long) ? (Long) o : Long.valueOf(o.toString());
377 } catch (Exception ex) {
378 addError(results, element, LONG);
379 }
380
381
382 if (i != null) {
383 Long min = getLargestMinValue(meta);
384 Long max = getSmallestMaxValue(meta);
385
386 if (min != null && max != null) {
387 if (i < min || i > max) {
388 addRangeError(results, element, OUT_OF_RANGE, min, max);
389 }
390 } else if (min != null && i < min) {
391 addError(results, element, MIN_VALUE, min);
392 } else if (max != null && i > max) {
393 addError(results, element, MAX_VALUE, max);
394 }
395 }
396 }
397 }
398 }
399 }
400
401 private void doValidateDouble(DataModel model, Metadata meta,
402 QueryPath path, List<ValidationResultInfo> results) {
403
404 Map<QueryPath, Object> values = model.query(path);
405
406 if (values.isEmpty() && isRequiredCheck(meta)) {
407 addError(results, path, REQUIRED);
408 } else {
409 Object[] keys = values.keySet().toArray();
410 for (int keyIndex = 0; keyIndex < keys.length; keyIndex++) {
411 QueryPath element = (QueryPath) keys[keyIndex];
412
413 Object o = values.get(element);
414
415 if (o == null) {
416 if (isRequiredCheck(meta)) {
417 addError(results, element, REQUIRED);
418 }
419 } else {
420 Double d = null;
421 try {
422 d = (o instanceof Double) ? (Double) o : Double.valueOf(o.toString());
423 } catch (Exception ex) {
424 addError(results, element, DOUBLE);
425 }
426
427
428 if (d != null) {
429 Double min = getLargestMinValueDouble(meta);
430 Double max = getSmallestMaxValueDouble(meta);
431
432 if (min != null && max != null) {
433 if (d < min || d > max) {
434 addRangeError(results, element, OUT_OF_RANGE, min, max);
435 }
436 } else if (min != null && d < min) {
437 addError(results, element, MIN_VALUE, min);
438 } else if (max != null && d > max) {
439 addError(results, element, MAX_VALUE, max);
440 }
441 }
442 }
443 }
444 }
445 }
446
447 private void doValidateFloat(DataModel model, Metadata meta,
448 QueryPath path, List<ValidationResultInfo> results) {
449
450 Map<QueryPath, Object> values = model.query(path);
451
452 if (values.isEmpty() && isRequiredCheck(meta)) {
453 addError(results, path, REQUIRED);
454 } else {
455 Object[] keys = values.keySet().toArray();
456 for (int keyIndex = 0; keyIndex < keys.length; keyIndex++) {
457 QueryPath element = (QueryPath) keys[keyIndex];
458
459 Object o = values.get(element);
460
461 if (o == null) {
462 if (isRequiredCheck(meta)) {
463 addError(results, element, REQUIRED);
464 }
465 } else {
466 Float d = null;
467 try {
468 d = (o instanceof Float) ? (Float) o : Float.valueOf(o.toString());
469 } catch (Exception ex) {
470 addError(results, element, FLOAT);
471 }
472
473
474 if (d != null) {
475 Double min = getLargestMinValueDouble(meta);
476 Double max = getSmallestMaxValueDouble(meta);
477
478 if (min != null && max != null) {
479 if (d < min || d > max) {
480 addRangeError(results, element, OUT_OF_RANGE, min, max);
481 }
482 } else if (min != null && d < min) {
483 addError(results, element, MIN_VALUE, min);
484 } else if (max != null && d > max) {
485 addError(results, element, MAX_VALUE, max);
486 }
487 }
488 }
489 }
490 }
491 }
492
493 private void doValidateDate(DataModel model, Metadata meta,
494 QueryPath path, List<ValidationResultInfo> results) {
495
496 Map<QueryPath, Object> values = model.query(path);
497
498 if (values.isEmpty() && isRequiredCheck(meta)) {
499 addError(results, path, REQUIRED);
500 } else {
501 Object[] keys = values.keySet().toArray();
502 for (int keyIndex = 0; keyIndex < keys.length; keyIndex++) {
503 QueryPath element = (QueryPath) keys[keyIndex];
504 Object o = values.get(element);
505
506 if (o == null) {
507 if (isRequiredCheck(meta)) {
508 addError(results, element, REQUIRED);
509 }
510 } else {
511 Date d = null;
512 try {
513 d = (o instanceof Date) ? (Date) o : dateParser.parseDate(o.toString());
514 } catch (Exception ex) {
515 addError(results, element, DATE);
516 }
517
518
519 if (d != null) {
520
521 Date min = getLargestMinValueDate(meta, dateParser, getCrossFieldMinValue(model, element, meta));
522 Date max = getSmallestMaxValueDate(meta, dateParser, getCrossFieldMaxValue(model, element, meta));
523
524 if (min != null && max != null) {
525 if (d.getTime() < min.getTime() || d.getTime() > max.getTime()) {
526 addRangeError(results, element, OUT_OF_RANGE, asDateString(min), asDateString(max));
527 }
528 } else if (min != null && d.getTime() < min.getTime()) {
529 addError(results, element, MIN_VALUE, asDateString(min));
530 } else if (max != null && d.getTime() > max.getTime()) {
531 addError(results, element, MAX_VALUE, asDateString(max));
532 }
533 }
534 }
535 }
536 }
537 }
538
539
540 private void doValidateBoolean(DataModel model, Metadata meta,
541 QueryPath path, List<ValidationResultInfo> results) {
542
543 Map<QueryPath, Object> values = model.query(path);
544
545 if (values.isEmpty() && isRequiredCheck(meta)) {
546 addError(results, path, REQUIRED);
547 } else {
548
549 Object[] keys = values.keySet().toArray();
550 for (int keyIndex = 0; keyIndex < keys.length; keyIndex++) {
551 QueryPath element = (QueryPath) keys[keyIndex];
552
553 Object o = values.get(element);
554
555 if (o == null) {
556 if (isRequiredCheck(meta)) {
557 addError(results, element, REQUIRED);
558 }
559 } else {
560 if (o instanceof Boolean == false) {
561 addError(results, element, BOOLEAN);
562 }
563 }
564 }
565 }
566 }
567
568 private void doValidateComplex(final DataModel model, final Metadata meta, final QueryPath path, List<ValidationResultInfo> results) {
569 Map<QueryPath, Object> values = model.query(path);
570 boolean hasChildElements = true;
571
572
573 if (values.isEmpty() && isRequiredCheck(meta)) {
574 addError(results, path, REQUIRED);
575 hasChildElements = false;
576 } else if (meta.getDataType().equals(DataType.LIST)) {
577 hasChildElements = false;
578 for (Map.Entry<QueryPath, Object> e : values.entrySet()) {
579 QueryPath listPath = QueryPath.parse(e.getKey().toString());
580 listPath.add(Data.WILDCARD_KEY);
581 values = model.query(listPath);
582
583 if (!values.isEmpty()) {
584 hasChildElements = true;
585 }
586
587 if (values.isEmpty() && isRequiredCheck(meta)) {
588 addError(results, e.getKey(), REQUIRED);
589 } else {
590
591 validateOccurs(e.getKey(), values, meta, results);
592 }
593 }
594 }
595
596
597 if (hasChildElements || path.toString().isEmpty()) {
598 String basePath = path.toString();
599 if (meta.getProperties() != null) {
600 Object[] keys = meta.getProperties().keySet().toArray();
601 parentElementLoop:
602 for (int keyIndex = 0; keyIndex < keys.length; keyIndex++) {
603 String element = (String) keys[keyIndex];
604 if (!element.contains("runtimeData")) {
605 QueryPath childPath = QueryPath.concat(basePath, element);
606 Map<QueryPath, Object> childValues = model.query(childPath);
607 if (!childValues.isEmpty()) {
608 Object[] childKeys = childValues.keySet().toArray();
609 for (int childKeyIndex = 0; childKeyIndex < childKeys.length; childKeyIndex++) {
610 QueryPath childElement = (QueryPath) childKeys[childKeyIndex];
611 QueryPath childElementDeletePath = QueryPath.parse(childElement.toString() + QueryPath.getPathSeparator() + RUNTIME_DELETED_KEY);
612 try {
613 Boolean childDeletedObject = model.get(childElementDeletePath);
614 if (childDeletedObject != null && childDeletedObject) {
615 continue parentElementLoop;
616 }
617 } catch (Exception e) {
618
619 }
620 }
621 }
622 doValidate(model, meta.getProperties().get(element), childPath, results);
623 }
624 }
625 }
626 }
627 }
628
629 private boolean validateOccurs(QueryPath path, Map<QueryPath, Object> values, Metadata meta, List<ValidationResultInfo> results) {
630
631 int size = getListSize(values, meta);
632
633 Integer min = getLargestMinOccurs(meta);
634 boolean minValid = min == null || min <= size;
635
636 Integer max = getSmallestMaxOccurs(meta);
637 boolean maxValid = (max == null || max == -1 || max >= size);
638
639
640 if (!minValid || !maxValid) {
641 if (!minValid && !maxValid) {
642 addRangeError(results, path, OCCURS, min, max);
643 } else if (!minValid) {
644 addError(results, path, MIN_OCCURS, min);
645 } else {
646 addError(results, path, MAX_OCCURS, max);
647 }
648 }
649
650 return minValid && maxValid;
651 }
652
653 private int getListSize(Map<QueryPath, Object> values, Metadata meta) {
654 int size = 0;
655
656
657 Map<String, Metadata> properties = meta.getProperties();
658 if (properties.containsKey(Data.WILDCARD_KEY.toString())) {
659 Metadata listMeta = properties.get(Data.WILDCARD_KEY.toString());
660 if (listMeta != null && listMeta.getDataType().equals(DataType.DATA)) {
661 Object[] valueList = values.values().toArray();
662 for (int i = 0; i < valueList.length; i++) {
663 Object value = valueList[i];
664 Data d = (Data) value;
665 Boolean deleted = d.query(RUNTIME_DELETED_KEY);
666 if (deleted == null || !deleted) {
667 size++;
668 }
669 }
670 } else {
671 size = values.size();
672 }
673 } else {
674 size = values.size();
675 }
676
677 return size;
678 }
679
680
681
682 private Object getCrossFieldMinValue(DataModel model, QueryPath path, Metadata meta) {
683 Object v = null;
684 List<ConstraintMetadata> constraints = meta.getConstraints();
685 for (int i = 0; i < constraints.size(); i++) {
686 ConstraintMetadata cons = constraints.get(i);
687 if (cons.getMinValue() != null && cons.getMinValue().contains("../")) {
688 QueryPath crossFieldPath = QueryPath.parse(path.toString());
689 String crossFieldKey = cons.getMinValue().substring(3);
690 crossFieldPath.remove(crossFieldPath.size() - 1);
691 crossFieldPath.add(new StringKey(crossFieldKey));
692 v = model.get(crossFieldPath);
693 }
694 }
695
696 return v;
697 }
698
699
700
701 private Object getCrossFieldMaxValue(DataModel model, QueryPath path, Metadata meta) {
702 Object v = null;
703 List<ConstraintMetadata> constraints = meta.getConstraints();
704 for (int i = 0; i < constraints.size(); i++) {
705 ConstraintMetadata cons = constraints.get(i);
706 if (cons.getMaxValue() != null && cons.getMaxValue().contains("../")) {
707 QueryPath crossFieldPath = QueryPath.parse(path.toString());
708 String crossFieldKey = cons.getMinValue().substring(3);
709 crossFieldPath.remove(crossFieldPath.size() - 1);
710 crossFieldPath.add(new StringKey(crossFieldKey));
711 v = model.get(crossFieldPath);
712 }
713 }
714
715 return v;
716 }
717
718 private void put(Map<String, Object> m, String key, Object value) {
719 if (value != null) {
720 m.put(key, value);
721 }
722 }
723
724 private String asDateString(Date date) {
725 DateTimeFormat dateTimeFormat = DateTimeFormat.getFormat("MM/dd/yyyy");
726 return dateTimeFormat.format(date);
727 }
728
729
730 }