1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.rice.krad.util;
17
18 import org.apache.commons.lang.StringEscapeUtils;
19 import org.apache.commons.lang.StringUtils;
20 import org.apache.commons.lang.builder.EqualsBuilder;
21 import org.apache.commons.lang.builder.HashCodeBuilder;
22 import org.apache.commons.lang.builder.ToStringBuilder;
23 import org.springframework.util.AutoPopulatingList;
24
25 import java.io.Serializable;
26 import java.util.ArrayList;
27 import java.util.Iterator;
28 import java.util.LinkedHashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32
33
34
35
36
37
38
39
40
41
42
43
44 public class MessageMap implements Serializable {
45 private static final long serialVersionUID = -2328635367656516150L;
46
47 private List<String> errorPath = new ArrayList<String>();
48
49 private Map<String, AutoPopulatingList<ErrorMessage>> errorMessages =
50 new LinkedHashMap<String, AutoPopulatingList<ErrorMessage>>();
51 private Map<String, AutoPopulatingList<ErrorMessage>> warningMessages =
52 new LinkedHashMap<String, AutoPopulatingList<ErrorMessage>>();
53 private Map<String, AutoPopulatingList<ErrorMessage>> infoMessages =
54 new LinkedHashMap<String, AutoPopulatingList<ErrorMessage>>();
55 private AutoPopulatingList<GrowlMessage> growlMessages;
56
57 public MessageMap() {
58 growlMessages = new AutoPopulatingList<GrowlMessage>(GrowlMessage.class);
59 }
60
61 public MessageMap(MessageMap messageMap) {
62 this.errorPath = messageMap.errorPath;
63 this.errorMessages = messageMap.errorMessages;
64 this.warningMessages = messageMap.warningMessages;
65 this.infoMessages = messageMap.infoMessages;
66
67 growlMessages = new AutoPopulatingList<GrowlMessage>(GrowlMessage.class);
68 }
69
70 public void merge(MessageMap messageMap) {
71 if (messageMap != null) {
72 if (messageMap.hasErrors()) {
73 merge(messageMap.getErrorMessages(), errorMessages);
74 }
75 if (messageMap.hasInfo()) {
76 merge(messageMap.getInfoMessages(), infoMessages);
77 }
78 if (messageMap.hasWarnings()) {
79 merge(messageMap.getWarningMessages(), warningMessages);
80 }
81 if (messageMap.getGrowlMessages() != null) {
82 growlMessages.addAll(messageMap.getGrowlMessages());
83 }
84 }
85
86 }
87
88
89
90
91
92
93
94 public void merge(Map<String, AutoPopulatingList<ErrorMessage>> messagesFrom,
95 Map<String, AutoPopulatingList<ErrorMessage>> messagesTo) {
96 for (String key : messagesFrom.keySet()) {
97
98 if (messagesTo.containsKey(key)) {
99
100 AutoPopulatingList<ErrorMessage> tal = messagesFrom.get(key);
101 AutoPopulatingList<ErrorMessage> parentList = messagesTo.get(key);
102
103 for (Object o : tal) {
104
105 if (!parentList.contains(o)) {
106 parentList.add((ErrorMessage) o);
107 }
108 }
109
110 } else {
111 messagesTo.put(key, messagesFrom.get(key));
112 }
113
114 }
115 }
116
117 public AutoPopulatingList<ErrorMessage> putError(String propertyName, String errorKey, String... errorParameters) {
118 ErrorMessage message = new ErrorMessage(errorKey, errorParameters);
119 return putMessageInMap(errorMessages, propertyName, message, true, true);
120 }
121
122 public AutoPopulatingList<ErrorMessage> putWarning(String propertyName, String messageKey,
123 String... messageParameters) {
124 ErrorMessage message = new ErrorMessage(messageKey, messageParameters);
125 return putMessageInMap(warningMessages, propertyName, message, true, true);
126 }
127
128 public AutoPopulatingList<ErrorMessage> putInfo(String propertyName, String messageKey,
129 String... messageParameters) {
130 ErrorMessage message = new ErrorMessage(messageKey, messageParameters);
131 return putMessageInMap(infoMessages, propertyName, message, true, true);
132 }
133
134 public AutoPopulatingList<ErrorMessage> putError(String propertyName, ErrorMessage message) {
135 return putMessageInMap(errorMessages, propertyName, message, true, true);
136 }
137
138 public AutoPopulatingList<ErrorMessage> putWarning(String propertyName, ErrorMessage message) {
139 return putMessageInMap(warningMessages, propertyName, message, true, true);
140 }
141
142 public AutoPopulatingList<ErrorMessage> putInfo(String propertyName, ErrorMessage message) {
143 return putMessageInMap(infoMessages, propertyName, message, true, true);
144 }
145
146 public AutoPopulatingList<ErrorMessage> putErrorWithoutFullErrorPath(String propertyName, String errorKey,
147 String... errorParameters) {
148 ErrorMessage message = new ErrorMessage(errorKey, errorParameters);
149 return putMessageInMap(errorMessages, propertyName, message, false, true);
150 }
151
152 public AutoPopulatingList<ErrorMessage> putWarningWithoutFullErrorPath(String propertyName, String messageKey,
153 String... messageParameters) {
154 ErrorMessage message = new ErrorMessage(messageKey, messageParameters);
155 return putMessageInMap(warningMessages, propertyName, message, false, true);
156 }
157
158 public AutoPopulatingList<ErrorMessage> putInfoWithoutFullErrorPath(String propertyName, String messageKey,
159 String... messageParameters) {
160 ErrorMessage message = new ErrorMessage(messageKey, messageParameters);
161 return putMessageInMap(infoMessages, propertyName, message, false, true);
162 }
163
164 public AutoPopulatingList<ErrorMessage> putErrorWithoutFullErrorPath(String propertyName, ErrorMessage message) {
165 return putMessageInMap(errorMessages, propertyName, message, false, true);
166 }
167
168 public AutoPopulatingList<ErrorMessage> putWarningWithoutFullErrorPath(String propertyName, ErrorMessage message) {
169 return putMessageInMap(warningMessages, propertyName, message, false, true);
170 }
171
172 public AutoPopulatingList<ErrorMessage> putInfoWithoutFullErrorPath(String propertyName, ErrorMessage message) {
173 return putMessageInMap(infoMessages, propertyName, message, false, true);
174 }
175
176 public AutoPopulatingList<ErrorMessage> putErrorForSectionId(String sectionId, String errorKey,
177 String... errorParameters) {
178 return putErrorWithoutFullErrorPath(sectionId, errorKey, errorParameters);
179 }
180
181 public AutoPopulatingList<ErrorMessage> putWarningForSectionId(String sectionId, String messageKey,
182 String... messageParameters) {
183 return putWarningWithoutFullErrorPath(sectionId, messageKey, messageParameters);
184 }
185
186 public AutoPopulatingList<ErrorMessage> putInfoForSectionId(String sectionId, String messageKey,
187 String... messageParameters) {
188 return putInfoWithoutFullErrorPath(sectionId, messageKey, messageParameters);
189 }
190
191 public AutoPopulatingList<ErrorMessage> putErrorForSectionId(String sectionId, ErrorMessage message) {
192 return putErrorWithoutFullErrorPath(sectionId, message);
193 }
194
195 public AutoPopulatingList<ErrorMessage> putWarningForSectionId(String sectionId, ErrorMessage message) {
196 return putWarningWithoutFullErrorPath(sectionId, message);
197 }
198
199 public AutoPopulatingList<ErrorMessage> putInfoForSectionId(String sectionId, ErrorMessage message) {
200 return putInfoWithoutFullErrorPath(sectionId, message);
201 }
202
203
204
205
206
207
208
209
210 public void addGrowlMessage(String growlTitle, String messageKey, String... messageParameters) {
211 GrowlMessage growl = new GrowlMessage();
212
213 growl.setTitle(growlTitle);
214 growl.setMessageKey(messageKey);
215 growl.setMessageParameters(messageParameters);
216
217 growlMessages.add(growl);
218 }
219
220
221
222
223
224
225 public void addGrowlMessage(GrowlMessage growl) {
226 growlMessages.add(growl);
227 }
228
229
230
231
232
233
234
235
236
237
238
239
240 protected AutoPopulatingList<ErrorMessage> putMessageInMap(Map<String, AutoPopulatingList<ErrorMessage>> messagesMap,
241 String propertyName, ErrorMessage errorMessage, boolean prependFullErrorPath,
242 boolean escapeHtmlMessageParameters) {
243 if (StringUtils.isBlank(propertyName)) {
244 throw new IllegalArgumentException("invalid (blank) propertyName");
245 }
246 if (StringUtils.isBlank(errorMessage.getErrorKey())) {
247 throw new IllegalArgumentException("invalid (blank) errorKey");
248 }
249
250
251 AutoPopulatingList<ErrorMessage> errorList = null;
252 String propertyKey = getKeyPath(propertyName, prependFullErrorPath);
253 if (messagesMap.containsKey(propertyKey)) {
254 errorList = messagesMap.get(propertyKey);
255 } else {
256 errorList = new AutoPopulatingList<ErrorMessage>(ErrorMessage.class);
257 }
258
259 if (escapeHtmlMessageParameters) {
260 if (errorMessage.getMessageParameters() != null) {
261 String[] filteredMessageParameters = new String[errorMessage.getMessageParameters().length];
262 for (int i = 0; i < errorMessage.getMessageParameters().length; i++) {
263 filteredMessageParameters[i] = StringEscapeUtils.escapeHtml(errorMessage.getMessageParameters()[i]);
264 }
265 errorMessage.setMessageParameters(filteredMessageParameters);
266 }
267
268 if (errorMessage.getMessagePrefixParameters() != null) {
269 String[] filteredMessageParameters = new String[errorMessage.getMessagePrefixParameters().length];
270 for (int i = 0; i < errorMessage.getMessagePrefixParameters().length; i++) {
271 filteredMessageParameters[i] = StringEscapeUtils.escapeHtml(
272 errorMessage.getMessagePrefixParameters()[i]);
273 }
274 errorMessage.setMessagePrefixParameters(filteredMessageParameters);
275 }
276
277 if (errorMessage.getMessageSuffixParameters() != null) {
278 String[] filteredMessageParameters = new String[errorMessage.getMessageSuffixParameters().length];
279 for (int i = 0; i < errorMessage.getMessageSuffixParameters().length; i++) {
280 filteredMessageParameters[i] = StringEscapeUtils.escapeHtml(
281 errorMessage.getMessageSuffixParameters()[i]);
282 }
283 errorMessage.setMessageSuffixParameters(filteredMessageParameters);
284 }
285 }
286
287
288 boolean alreadyAdded = false;
289 for (ErrorMessage e : errorList) {
290 if (e.equals(errorMessage)) {
291 alreadyAdded = true;
292 break;
293 }
294 }
295 if (!alreadyAdded) {
296 errorList.add(errorMessage);
297 }
298
299 return messagesMap.put(propertyKey, errorList);
300 }
301
302
303
304
305
306
307
308
309
310
311
312 public boolean replaceError(String propertyName, String targetKey, String replaceKey, String... replaceParameters) {
313 return replaceError(propertyName, targetKey, true, replaceKey, replaceParameters);
314 }
315
316
317
318
319
320
321
322
323
324
325
326
327 public boolean replaceErrorWithoutFullErrorPath(String propertyName, String targetKey, String replaceKey,
328 String... replaceParameters) {
329 return replaceError(propertyName, targetKey, false, replaceKey, replaceParameters);
330 }
331
332
333
334
335
336
337
338
339
340
341
342 private boolean replaceError(String propertyName, String targetKey, boolean withFullErrorPath, String replaceKey,
343 String... replaceParameters) {
344 boolean replaced = false;
345
346 if (StringUtils.isBlank(propertyName)) {
347 throw new IllegalArgumentException("invalid (blank) propertyName");
348 }
349 if (StringUtils.isBlank(targetKey)) {
350 throw new IllegalArgumentException("invalid (blank) targetKey");
351 }
352 if (StringUtils.isBlank(replaceKey)) {
353 throw new IllegalArgumentException("invalid (blank) replaceKey");
354 }
355
356
357 AutoPopulatingList<ErrorMessage> errorList = null;
358 String propertyKey = getKeyPath(propertyName, withFullErrorPath);
359 if (errorMessages.containsKey(propertyKey)) {
360 errorList = errorMessages.get(propertyKey);
361
362
363 for (int i = 0; i < errorList.size(); ++i) {
364 ErrorMessage em = errorList.get(i);
365
366
367 if (em.getErrorKey().equals(targetKey)) {
368 ErrorMessage rm = new ErrorMessage(replaceKey, replaceParameters);
369 errorList.set(i, rm);
370 replaced = true;
371 }
372 }
373 }
374
375 return replaced;
376 }
377
378
379
380
381
382
383
384
385 public boolean fieldHasMessage(String fieldName, String errorKey) {
386 boolean found = false;
387
388 List<ErrorMessage> fieldMessages = errorMessages.get(fieldName);
389 if (fieldMessages != null) {
390 for (Iterator<ErrorMessage> i = fieldMessages.iterator(); !found && i.hasNext(); ) {
391 ErrorMessage errorMessage = i.next();
392 found = errorMessage.getErrorKey().equals(errorKey);
393 }
394 }
395
396 return found;
397 }
398
399
400
401
402
403
404
405 public int countFieldMessages(String fieldName) {
406 int count = 0;
407
408 List<ErrorMessage> fieldMessages = errorMessages.get(fieldName);
409 if (fieldMessages != null) {
410 count = fieldMessages.size();
411 }
412
413 return count;
414 }
415
416
417
418
419 public boolean containsMessageKey(String messageKey) {
420 ErrorMessage foundMessage = null;
421
422 if (!hasNoErrors()) {
423 for (Iterator<Map.Entry<String, AutoPopulatingList<ErrorMessage>>> i =
424 getAllPropertiesAndErrors().iterator(); (foundMessage == null) && i.hasNext(); ) {
425 Map.Entry<String, AutoPopulatingList<ErrorMessage>> e = i.next();
426 AutoPopulatingList<ErrorMessage> entryErrorList = e.getValue();
427 for (Iterator<ErrorMessage> j = entryErrorList.iterator(); j.hasNext(); ) {
428 ErrorMessage em = j.next();
429 if (messageKey.equals(em.getErrorKey())) {
430 foundMessage = em;
431 }
432 }
433 }
434 }
435
436 return (foundMessage != null);
437 }
438
439 private int getMessageCount(Map<String, AutoPopulatingList<ErrorMessage>> messageMap) {
440 int messageCount = 0;
441 for (Iterator<String> iter = messageMap.keySet().iterator(); iter.hasNext(); ) {
442 String errorKey = iter.next();
443 List<ErrorMessage> errors = messageMap.get(errorKey);
444 messageCount += errors.size();
445 }
446
447 return messageCount;
448 }
449
450
451
452
453
454
455 public int getErrorCount() {
456 return getMessageCount(errorMessages);
457 }
458
459
460
461
462
463
464 public int getWarningCount() {
465 return getMessageCount(warningMessages);
466 }
467
468
469
470
471
472
473 public int getInfoCount() {
474 return getMessageCount(infoMessages);
475 }
476
477
478
479
480
481 public AutoPopulatingList<ErrorMessage> getMessages(String path) {
482 return errorMessages.get(path);
483 }
484
485
486
487
488
489
490 public void addToErrorPath(String parentName) {
491 errorPath.add(parentName);
492 }
493
494
495
496
497
498
499 public List<String> getErrorPath() {
500 return errorPath;
501 }
502
503
504
505
506
507
508
509 public boolean removeFromErrorPath(String parentName) {
510 return errorPath.remove(parentName);
511 }
512
513
514
515
516 public void clearErrorPath() {
517 errorPath.clear();
518 }
519
520
521
522
523
524
525
526
527
528
529 public String getKeyPath(String propertyName, boolean prependFullErrorPath) {
530 String keyPath = "";
531
532 if (KRADConstants.GLOBAL_ERRORS.equals(propertyName)) {
533 return KRADConstants.GLOBAL_ERRORS;
534 }
535
536 if (!errorPath.isEmpty() && prependFullErrorPath) {
537 keyPath = StringUtils.join(errorPath.iterator(), ".");
538 keyPath += (keyPath != null && keyPath.endsWith(".")) ? propertyName : "." + propertyName;
539 } else {
540 keyPath = propertyName;
541 }
542
543 return keyPath;
544 }
545
546
547
548
549 public List<String> getPropertiesWithErrors() {
550 List<String> properties = new ArrayList<String>();
551
552 for (Iterator<String> iter = errorMessages.keySet().iterator(); iter.hasNext(); ) {
553 properties.add(iter.next());
554 }
555
556 return properties;
557 }
558
559
560
561
562 public List<String> getPropertiesWithWarnings() {
563 List<String> properties = new ArrayList<String>(warningMessages.keySet());
564 return properties;
565 }
566
567
568
569
570 public List<String> getPropertiesWithInfo() {
571 List<String> properties = new ArrayList<String>(infoMessages.keySet());
572 return properties;
573 }
574
575 public void clearErrorMessages() {
576 errorMessages.clear();
577 }
578
579 public boolean doesPropertyHaveError(String key) {
580 return errorMessages.containsKey(key);
581 }
582
583
584
585
586 public boolean containsKeyMatchingPattern(String pattern) {
587 List<String> simplePatterns = new ArrayList<String>();
588 List<String> wildcardPatterns = new ArrayList<String>();
589 String[] patterns = pattern.split(",");
590 for (int i = 0; i < patterns.length; i++) {
591 String s = patterns[i];
592 if (s.endsWith("*")) {
593 wildcardPatterns.add(s.substring(0, s.length() - 1));
594 } else {
595 simplePatterns.add(s);
596 }
597 }
598 for (Iterator<String> keys = errorMessages.keySet().iterator(); keys.hasNext(); ) {
599 String key = keys.next();
600 if (simplePatterns.contains(key)) {
601 return true;
602 }
603 for (Iterator<String> wildcardIterator = wildcardPatterns.iterator(); wildcardIterator.hasNext(); ) {
604 String wildcard = wildcardIterator.next();
605 if (key.startsWith(wildcard)) {
606 return true;
607 }
608 }
609 }
610 return false;
611 }
612
613 public Set<Map.Entry<String, AutoPopulatingList<ErrorMessage>>> getAllPropertiesAndErrors() {
614 return errorMessages.entrySet();
615 }
616
617 public AutoPopulatingList<ErrorMessage> getErrorMessagesForProperty(String propertyName) {
618 return errorMessages.get(propertyName);
619 }
620
621 public AutoPopulatingList<ErrorMessage> getWarningMessagesForProperty(String propertyName) {
622 return warningMessages.get(propertyName);
623 }
624
625 public AutoPopulatingList<ErrorMessage> getInfoMessagesForProperty(String propertyName) {
626 return infoMessages.get(propertyName);
627 }
628
629
630
631
632
633
634
635
636
637
638
639
640 public List<AutoPopulatingList<ErrorMessage>> getErrorMessagesForProperty(String propertyName,
641 boolean allowWildcard) {
642 List<AutoPopulatingList<ErrorMessage>> foundMessages = new ArrayList<AutoPopulatingList<ErrorMessage>>();
643 if (allowWildcard) {
644 boolean wildcard = false;
645 if (propertyName.endsWith("*")) {
646 wildcard = true;
647 propertyName = propertyName.substring(0, propertyName.length() - 1);
648 }
649 for (Iterator<String> keys = errorMessages.keySet().iterator(); keys.hasNext(); ) {
650 String key = keys.next();
651 if (!wildcard && propertyName.equals(key)) {
652 foundMessages.add(errorMessages.get(key));
653 break;
654 } else if (wildcard && key.startsWith(propertyName)) {
655 foundMessages.add(errorMessages.get(key));
656 }
657 }
658 } else {
659 foundMessages.add(getErrorMessagesForProperty(propertyName));
660 }
661
662 return foundMessages;
663 }
664
665
666
667
668
669
670
671
672
673
674
675
676 public List<AutoPopulatingList<ErrorMessage>> getWarningMessagesForProperty(String propertyName,
677 boolean allowWildcard) {
678 List<AutoPopulatingList<ErrorMessage>> foundMessages = new ArrayList<AutoPopulatingList<ErrorMessage>>();
679 if (allowWildcard) {
680 boolean wildcard = false;
681 if (propertyName.endsWith("*")) {
682 wildcard = true;
683 propertyName = propertyName.substring(0, propertyName.length() - 1);
684 }
685 for (Iterator<String> keys = warningMessages.keySet().iterator(); keys.hasNext(); ) {
686 String key = keys.next();
687 if (!wildcard && propertyName.equals(key)) {
688 foundMessages.add(warningMessages.get(key));
689 break;
690 } else if (wildcard && key.startsWith(propertyName)) {
691 foundMessages.add(warningMessages.get(key));
692 }
693 }
694 } else {
695 foundMessages.add(getWarningMessagesForProperty(propertyName));
696 }
697
698 return foundMessages;
699 }
700
701
702
703
704
705
706
707
708
709
710
711
712 public List<AutoPopulatingList<ErrorMessage>> getInfoMessagesForProperty(String propertyName,
713 boolean allowWildcard) {
714 List<AutoPopulatingList<ErrorMessage>> foundMessages = new ArrayList<AutoPopulatingList<ErrorMessage>>();
715 if (allowWildcard) {
716 boolean wildcard = false;
717 if (propertyName.endsWith("*")) {
718 wildcard = true;
719 propertyName = propertyName.substring(0, propertyName.length() - 1);
720 }
721 for (Iterator<String> keys = infoMessages.keySet().iterator(); keys.hasNext(); ) {
722 String key = keys.next();
723 if (!wildcard && propertyName.equals(key)) {
724 foundMessages.add(infoMessages.get(key));
725 break;
726 } else if (wildcard && key.startsWith(propertyName)) {
727 foundMessages.add(infoMessages.get(key));
728 }
729 }
730 } else {
731 foundMessages.add(getInfoMessagesForProperty(propertyName));
732 }
733
734 return foundMessages;
735 }
736
737 public boolean hasErrors() {
738 return !errorMessages.isEmpty();
739 }
740
741 public boolean hasNoErrors() {
742 return errorMessages.isEmpty();
743 }
744
745 public boolean hasWarnings() {
746 return !warningMessages.isEmpty();
747 }
748
749 public boolean hasNoWarnings() {
750 return warningMessages.isEmpty();
751 }
752
753 public boolean hasInfo() {
754 return !infoMessages.isEmpty();
755 }
756
757 public boolean hasNoInfo() {
758 return infoMessages.isEmpty();
759 }
760
761 public boolean hasMessages() {
762 if (!errorMessages.isEmpty() || !warningMessages.isEmpty() || !infoMessages.isEmpty()) {
763 return true;
764 }
765 return false;
766 }
767
768 public boolean hasNoMessages() {
769 if (errorMessages.isEmpty() && warningMessages.isEmpty() && infoMessages.isEmpty()) {
770 return true;
771 }
772 return false;
773 }
774
775 public Set<String> getAllPropertiesWithErrors() {
776 return errorMessages.keySet();
777 }
778
779 public Set<String> getAllPropertiesWithWarnings() {
780 return warningMessages.keySet();
781 }
782
783 public Set<String> getAllPropertiesWithInfo() {
784 return infoMessages.keySet();
785 }
786
787 public AutoPopulatingList<ErrorMessage> removeAllErrorMessagesForProperty(String property) {
788 return errorMessages.remove(property);
789 }
790
791 public AutoPopulatingList<ErrorMessage> removeAllWarningMessagesForProperty(String property) {
792 return warningMessages.remove(property);
793 }
794
795 public AutoPopulatingList<ErrorMessage> removeAllInfoMessagesForProperty(String property) {
796 return infoMessages.remove(property);
797 }
798
799 public int getNumberOfPropertiesWithErrors() {
800 return errorMessages.size();
801 }
802
803 public Map<String, AutoPopulatingList<ErrorMessage>> getErrorMessages() {
804 return this.errorMessages;
805 }
806
807 public Map<String, AutoPopulatingList<ErrorMessage>> getWarningMessages() {
808 return this.warningMessages;
809 }
810
811 public Map<String, AutoPopulatingList<ErrorMessage>> getInfoMessages() {
812 return this.infoMessages;
813 }
814
815
816
817
818
819
820
821 public List<GrowlMessage> getGrowlMessages() {
822 return this.growlMessages;
823 }
824
825 @Override
826 public boolean equals(Object o) {
827 return EqualsBuilder.reflectionEquals(this, o);
828 }
829
830 @Override
831 public int hashCode() {
832 return HashCodeBuilder.reflectionHashCode(this);
833 }
834
835 @Override
836 public String toString() {
837 return ToStringBuilder.reflectionToString(this);
838 }
839 }