View Javadoc

1   /**
2    * Copyright 2005-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.element;
17  
18  import com.google.common.collect.Lists;
19  import com.google.common.collect.Maps;
20  import org.apache.commons.lang.StringUtils;
21  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
22  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
23  import org.kuali.rice.krad.uif.UifConstants;
24  import org.kuali.rice.krad.uif.component.Component;
25  import org.kuali.rice.krad.uif.container.Container;
26  import org.kuali.rice.krad.uif.container.ContainerBase;
27  import org.kuali.rice.krad.uif.container.PageGroup;
28  import org.kuali.rice.krad.uif.field.FieldGroup;
29  import org.kuali.rice.krad.uif.field.InputField;
30  import org.kuali.rice.krad.uif.util.MessageStructureUtils;
31  import org.kuali.rice.krad.uif.view.View;
32  import org.kuali.rice.krad.util.ErrorMessage;
33  import org.kuali.rice.krad.util.GlobalVariables;
34  import org.kuali.rice.krad.util.KRADUtils;
35  import org.kuali.rice.krad.util.MessageMap;
36  import org.springframework.util.AutoPopulatingList;
37  
38  import java.beans.PropertyEditor;
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.Collection;
42  import java.util.HashSet;
43  import java.util.List;
44  import java.util.Map;
45  import java.util.Set;
46  
47  /**
48   * Field that displays error, warning, and info messages for the keys that are
49   * matched. By default, an ValidationMessages will match on id and bindingPath (if this
50   * ValidationMessages is for an InputField), but can be set to match on
51   * additionalKeys and nested components keys (of the its parentComponent).
52   *
53   * In addition, there are a variety of options which can be toggled to effect
54   * the display of these messages during both client and server side validation
55   * display. See documentation on each get method for more details on the effect
56   * of each option.
57   *
58   * @author Kuali Rice Team (rice.collab@kuali.org)
59   */
60  @BeanTag(name = "validationMessages-bean", parent = "Uif-ValidationMessagesBase")
61  public class ValidationMessages extends ContentElementBase {
62      private static final long serialVersionUID = 780940788435330077L;
63  
64      private List<String> additionalKeysToMatch;
65  
66      private boolean displayMessages;
67  
68      // Error messages
69      private List<String> errors;
70      private List<String> warnings;
71      private List<String> infos;
72  
73      private Map<String, String> validationDataDefaults;
74  
75      /**
76       * PerformFinalize will generate the messages and counts used by the
77       * errorsField based on the keys that were matched from the MessageMap for
78       * this ValidationMessages. It will also set up nestedComponents of its
79       * parentComponent correctly based on the flags that were chosen for this
80       * ValidationMessages.
81       *
82       * @see org.kuali.rice.krad.uif.field.FieldBase#performFinalize(org.kuali.rice.krad.uif.view.View,
83       *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
84       */
85      @Override
86      public void performFinalize(View view, Object model, Component parent) {
87          super.performFinalize(view, model, parent);
88  
89          generateMessages(true, view, model, parent);
90      }
91  
92      /**
93       * Generates the messages based on the content in the messageMap
94       *
95       * @param reset true to reset the errors, warnings, and info lists
96       * @param view the current View
97       * @param model the current model
98       * @param parent the parent of this ValidationMessages
99       */
100     public void generateMessages(boolean reset, View view, Object model, Component parent) {
101         if (reset) {
102             errors = new ArrayList<String>();
103             warnings = new ArrayList<String>();
104             infos = new ArrayList<String>();
105         }
106 
107         List<String> masterKeyList = getKeys(parent);
108         MessageMap messageMap = GlobalVariables.getMessageMap();
109 
110         String parentContainerId = "";
111         Object parentContainer = parent.getContext().get("parent");
112 
113         if (parentContainer != null && (parentContainer instanceof Container
114                 || parentContainer instanceof FieldGroup)) {
115             parentContainerId = ((Component) parentContainer).getId();
116         }
117 
118         // special message component case
119         if (parentContainer != null && parentContainer instanceof Message && ((Message) parentContainer)
120                 .isGenerateSpan()) {
121             parentContainerId = ((Component) parentContainer).getId();
122         }
123 
124         // special case for nested contentElement with no parent
125         if (parentContainer != null && parentContainer instanceof Component && StringUtils.isBlank(parentContainerId)) {
126             parentContainer = ((Component) parentContainer).getContext().get("parent");
127             if (parentContainer != null && (parentContainer instanceof Container
128                     || parentContainer instanceof FieldGroup)) {
129                 parentContainerId = ((Component) parentContainer).getId();
130             }
131         }
132 
133         //Add identifying data attributes
134         this.addDataAttribute(UifConstants.DataAttributes.MESSAGES_FOR, parent.getId());
135 
136         if (parent.getDataAttributes().get(UifConstants.DataAttributes.PARENT) == null) {
137             parent.addDataAttribute(UifConstants.DataAttributes.PARENT, parentContainerId);
138         }
139 
140         //Handle the special FieldGroup case - adds the FieldGroup itself to ids handled by this group (this must
141         //be a group if its parent is FieldGroup)
142         if (parentContainer != null && parentContainer instanceof FieldGroup) {
143             masterKeyList.add(parentContainerId);
144         }
145 
146         //Check for message keys that are not matched anywhere on the page - these unmatched messages must still be
147         //displayed at the page level
148         if (parent instanceof PageGroup) {
149             Map<String, PropertyEditor> propertyEditors = view.getViewIndex().getFieldPropertyEditors();
150             Map<String, PropertyEditor> securePropertyEditors = view.getViewIndex().getSecureFieldPropertyEditors();
151             List<String> allPossibleKeys = new ArrayList<String>(propertyEditors.keySet());
152             allPossibleKeys.addAll(securePropertyEditors.keySet());
153 
154             this.addNestedGroupKeys(allPossibleKeys, parent);
155             if (additionalKeysToMatch != null) {
156                 allPossibleKeys.addAll(additionalKeysToMatch);
157             }
158             if (StringUtils.isNotBlank(parent.getId())) {
159                 allPossibleKeys.add(parent.getId());
160             }
161 
162             Set<String> messageKeys = new HashSet<String>();
163             messageKeys.addAll(messageMap.getAllPropertiesWithErrors());
164             messageKeys.addAll(messageMap.getAllPropertiesWithWarnings());
165             messageKeys.addAll(messageMap.getAllPropertiesWithInfo());
166 
167             messageKeys.removeAll(allPossibleKeys);
168 
169             masterKeyList.addAll(messageKeys);
170         }
171 
172         for (String key : masterKeyList) {
173             errors.addAll(getMessages(view, key, messageMap.getErrorMessagesForProperty(key, true)));
174             warnings.addAll(getMessages(view, key, messageMap.getWarningMessagesForProperty(key, true)));
175             infos.addAll(getMessages(view, key, messageMap.getInfoMessagesForProperty(key, true)));
176         }
177     }
178 
179     /**
180      * Gets all the messages from the list of lists passed in (which are
181      * lists of ErrorMessages associated to the key) and uses the configuration
182      * service to get the message String associated. This will also combine
183      * error messages per a field if that option is turned on. If
184      * displayFieldLabelWithMessages is turned on, it will also find the label
185      * by key passed in.
186      *
187      * @param view
188      * @param key
189      * @param lists
190      * @return
191      */
192     private List<String> getMessages(View view, String key, List<AutoPopulatingList<ErrorMessage>> lists) {
193         List<String> result = new ArrayList<String>();
194         for (List<ErrorMessage> errorList : lists) {
195             if (errorList != null && StringUtils.isNotBlank(key)) {
196                 for (ErrorMessage e : errorList) {
197                     String message = KRADUtils.getMessageText(e, true);
198                     message = MessageStructureUtils.translateStringMessage(message);
199 
200                     result.add(message);
201                 }
202             }
203         }
204 
205         return result;
206     }
207 
208     /**
209      * Gets all the keys associated to this ValidationMessages. This includes the id of
210      * the parent component, additional keys to match, and the bindingPath if
211      * this is an ValidationMessages for an InputField. These are the keys that are
212      * used to match errors with their component and display them as part of its
213      * ValidationMessages.
214      *
215      * @return
216      */
217     protected List<String> getKeys(Component parent) {
218         List<String> keyList = new ArrayList<String>();
219         if (additionalKeysToMatch != null) {
220             keyList.addAll(additionalKeysToMatch);
221         }
222         if (StringUtils.isNotBlank(parent.getId())) {
223             keyList.add(parent.getId());
224         }
225         if (parent instanceof InputField) {
226             if (((InputField) parent).getBindingInfo() != null && StringUtils.isNotEmpty(
227                     ((InputField) parent).getBindingInfo().getBindingPath())) {
228                 keyList.add(((InputField) parent).getBindingInfo().getBindingPath());
229             }
230         }
231 
232         return keyList;
233     }
234 
235     /**
236      * Adds all group keys of this component (starting from this component itself) by calling getKeys on each of
237      * its nested group's ValidationMessages and adding them to the list.
238      *
239      * @param keyList
240      * @param component
241      */
242     private void addNestedGroupKeys(Collection<String> keyList, Component component) {
243         for (Component c : component.getComponentsForLifecycle()) {
244             ValidationMessages ef = null;
245             if (c instanceof ContainerBase) {
246                 ef = ((ContainerBase) c).getValidationMessages();
247             } else if (c instanceof FieldGroup) {
248                 ef = ((FieldGroup) c).getGroup().getValidationMessages();
249             }
250             if (ef != null) {
251                 keyList.addAll(ef.getKeys(c));
252                 addNestedGroupKeys(keyList, c);
253             }
254         }
255     }
256 
257     /**
258      * AdditionalKeysToMatch is an additional list of keys outside of the
259      * default keys that will be matched when messages are returned after a form
260      * is submitted. These keys are only used for displaying messages generated
261      * by the server and have no effect on client side validation error display.
262      *
263      * @return the additionalKeysToMatch
264      */
265     @BeanTagAttribute(name = "additionalKeysToMatch", type = BeanTagAttribute.AttributeType.LISTVALUE)
266     public List<String> getAdditionalKeysToMatch() {
267         return this.additionalKeysToMatch;
268     }
269 
270     /**
271      * Convenience setter for additional keys to match that takes a string argument and
272      * splits on comma to build the list
273      *
274      * @param additionalKeysToMatch String to parse
275      */
276     public void setAdditionalKeysToMatch(String additionalKeysToMatch) {
277         if (StringUtils.isNotBlank(additionalKeysToMatch)) {
278             this.additionalKeysToMatch = Arrays.asList(StringUtils.split(additionalKeysToMatch, ","));
279         }
280     }
281 
282     /**
283      * @param additionalKeysToMatch the additionalKeysToMatch to set
284      */
285     public void setAdditionalKeysToMatch(List<String> additionalKeysToMatch) {
286         this.additionalKeysToMatch = additionalKeysToMatch;
287     }
288 
289     /**
290      * <p>If true, error, warning, and info messages will be displayed (provided
291      * they are also set to display). Otherwise, no messages for this
292      * ValidationMessages container will be displayed (including ones set to display).
293      * This is a global display on/off switch for all messages.</p>
294      *
295      * <p>Other areas of the screen react to
296      * a display flag being turned off at a certain level, if display is off for a field, the next
297      * level up will display that fields full message text, and if display is off at a section the
298      * next section up will display those messages nested in a sublist.</p>
299      *
300      * @return the displayMessages
301      */
302     @BeanTagAttribute(name = "displayMessages")
303     public boolean isDisplayMessages() {
304         return this.displayMessages;
305     }
306 
307     /**
308      * @param displayMessages the displayMessages to set
309      */
310     public void setDisplayMessages(boolean displayMessages) {
311         this.displayMessages = displayMessages;
312     }
313 
314     /**
315      * The list of error messages found for the keys that were matched on this
316      * ValidationMessages This is generated and cannot be set
317      *
318      * @return the errors
319      */
320     @BeanTagAttribute(name = "errors", type = BeanTagAttribute.AttributeType.LISTVALUE)
321     public List<String> getErrors() {
322         return this.errors;
323     }
324 
325     /**
326      * The list of warning messages found for the keys that were matched on this
327      * ValidationMessages This is generated and cannot be set
328      *
329      * @return the warnings
330      */
331     @BeanTagAttribute(name = "warnings", type = BeanTagAttribute.AttributeType.LISTVALUE)
332     public List<String> getWarnings() {
333         return this.warnings;
334     }
335 
336     /**
337      * The list of info messages found for the keys that were matched on this
338      * ValidationMessages This is generated and cannot be set
339      *
340      * @return the infos
341      */
342     @BeanTagAttribute(name = "infos", type = BeanTagAttribute.AttributeType.LISTVALUE)
343     public List<String> getInfos() {
344         return this.infos;
345     }
346 
347     public Map<String, String> getValidationDataDefaults() {
348         return validationDataDefaults;
349     }
350 
351     public void setValidationDataDefaults(Map<String, String> validationDataDefaults) {
352         this.validationDataDefaults = validationDataDefaults;
353     }
354 
355     protected void addValidationDataSettingsValue(Map<String, Object> valueMap, Map<String, String> defaults,
356             String key, Object value) {
357         String defaultValue = defaults.get(key);
358         if ((defaultValue != null && !value.toString().equals(defaultValue)) || (defaultValue != null && defaultValue
359                 .equals("[]") && value instanceof List && !((List) value).isEmpty()) || defaultValue == null) {
360             valueMap.put(key, value);
361         }
362     }
363 
364     public void setErrors(List<String> errors) {
365         this.errors = errors;
366     }
367 
368     public void setWarnings(List<String> warnings) {
369         this.warnings = warnings;
370     }
371 
372     public void setInfos(List<String> infos) {
373         this.infos = infos;
374     }
375 
376     /**
377      * @see org.kuali.rice.krad.uif.component.ComponentBase#copy()
378      */
379     @Override
380     protected <T> void copyProperties(T component) {
381         super.copyProperties(component);
382         ValidationMessages validationMessagesCopy = (ValidationMessages) component;
383 
384         if (additionalKeysToMatch != null) {
385             List<String> additionalKeysToMatchCopy = Lists.newArrayListWithExpectedSize(additionalKeysToMatch.size());
386             for (String additionalKeyToMatch : additionalKeysToMatch) {
387                 additionalKeysToMatchCopy.add(additionalKeyToMatch);
388             }
389 
390             validationMessagesCopy.setAdditionalKeysToMatch(additionalKeysToMatchCopy);
391         }
392 
393         validationMessagesCopy.setDisplayMessages(this.isDisplayMessages());
394 
395         if (warnings != null) {
396             // Error messages
397             List<String> warningsCopy = Lists.newArrayListWithExpectedSize(warnings.size());
398             for (String warning : warnings) {
399                 warningsCopy.add(warning);
400             }
401 
402             validationMessagesCopy.setWarnings(warningsCopy);
403         }
404 
405         if (errors != null) {
406             List<String> errorsCopy = Lists.newArrayListWithExpectedSize(errors.size());
407             for (String error : errors) {
408                 errorsCopy.add(error);
409             }
410 
411             validationMessagesCopy.setErrors(errorsCopy);
412         }
413 
414         if (infos != null) {
415             List<String> infosCopy = Lists.newArrayListWithExpectedSize(infos.size());
416             for (String info : infos) {
417                 infosCopy.add(info);
418             }
419 
420             validationMessagesCopy.setInfos(infosCopy);
421         }
422 
423         if (this.getValidationDataDefaults() != null) {
424             Map<String, String> validationDataDefaultsCopy = Maps.newHashMapWithExpectedSize(
425                     this.getValidationDataDefaults().size());
426             for (Map.Entry validationDataDefault : getValidationDataDefaults().entrySet()) {
427                 validationDataDefaultsCopy.put(validationDataDefault.getKey().toString(),
428                         validationDataDefault.getValue().toString());
429             }
430 
431             validationMessagesCopy.setValidationDataDefaults(validationDataDefaultsCopy);
432         }
433     }
434 
435 }