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 java.beans.PropertyEditor;
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collection;
22  import java.util.HashMap;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  
28  import org.apache.commons.lang.StringUtils;
29  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
30  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
31  import org.kuali.rice.krad.uif.UifConstants;
32  import org.kuali.rice.krad.uif.component.Component;
33  import org.kuali.rice.krad.uif.container.Container;
34  import org.kuali.rice.krad.uif.container.ContainerBase;
35  import org.kuali.rice.krad.uif.container.PageGroup;
36  import org.kuali.rice.krad.uif.field.FieldGroup;
37  import org.kuali.rice.krad.uif.field.InputField;
38  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
39  import org.kuali.rice.krad.uif.util.MessageStructureUtils;
40  import org.kuali.rice.krad.uif.view.View;
41  import org.kuali.rice.krad.util.ErrorMessage;
42  import org.kuali.rice.krad.util.GlobalVariables;
43  import org.kuali.rice.krad.util.KRADUtils;
44  import org.kuali.rice.krad.util.MessageMap;
45  import org.springframework.util.AutoPopulatingList;
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(Object model, Component parent) {
87          super.performFinalize(model, parent);
88  
89          generateMessages(true, ViewLifecycle.getActiveLifecycle().getView(), 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 
112         Map<String, Object> parentContext = parent.getContext();
113         Object parentContainer = parentContext == null ? null : parentContext
114                 .get(UifConstants.ContextVariableNames.PARENT);
115 
116         if (parentContainer != null && (parentContainer instanceof Container
117                 || parentContainer instanceof FieldGroup)) {
118             parentContainerId = ((Component) parentContainer).getId();
119         }
120 
121         // special message component case
122         if (parentContainer != null && parentContainer instanceof Message && ((Message) parentContainer)
123                 .isGenerateSpan()) {
124             parentContainerId = ((Component) parentContainer).getId();
125         }
126 
127         // special case for nested contentElement with no parent
128         if (parentContainer != null && parentContainer instanceof Component && StringUtils.isBlank(parentContainerId)) {
129             parentContext = ((Component) parentContainer).getContext();
130             parentContainer = parentContext == null ? null : parentContext
131                     .get(UifConstants.ContextVariableNames.PARENT);
132             if (parentContainer != null && (parentContainer instanceof Container
133                     || parentContainer instanceof FieldGroup)) {
134                 parentContainerId = ((Component) parentContainer).getId();
135             }
136         }
137 
138         //Add identifying data attributes
139         this.addDataAttribute(UifConstants.DataAttributes.MESSAGES_FOR, parent.getId());
140 
141         if ((parent.getDataAttributes() == null) || (parent.getDataAttributes().get(UifConstants.DataAttributes.PARENT)
142                 == null)) {
143             parent.addDataAttribute(UifConstants.DataAttributes.PARENT, parentContainerId);
144         }
145 
146         //Handle the special FieldGroup case - adds the FieldGroup itself to ids handled by this group (this must
147         //be a group if its parent is FieldGroup)
148         if (parentContainer != null && parentContainer instanceof FieldGroup) {
149             masterKeyList.add(parentContainerId);
150         }
151 
152         //Check for message keys that are not matched anywhere on the page - these unmatched messages must still be
153         //displayed at the page level
154         if (parent instanceof PageGroup) {
155             Map<String, PropertyEditor> propertyEditors = view.getViewIndex().getFieldPropertyEditors();
156             Map<String, PropertyEditor> securePropertyEditors = view.getViewIndex().getSecureFieldPropertyEditors();
157             List<String> allPossibleKeys = new ArrayList<String>(propertyEditors.keySet());
158             allPossibleKeys.addAll(securePropertyEditors.keySet());
159 
160             this.addNestedGroupKeys(allPossibleKeys, parent);
161             if (additionalKeysToMatch != null) {
162                 allPossibleKeys.addAll(additionalKeysToMatch);
163             }
164             if (StringUtils.isNotBlank(parent.getId())) {
165                 allPossibleKeys.add(parent.getId());
166             }
167 
168             Set<String> messageKeys = new HashSet<String>();
169             messageKeys.addAll(messageMap.getAllPropertiesWithErrors());
170             messageKeys.addAll(messageMap.getAllPropertiesWithWarnings());
171             messageKeys.addAll(messageMap.getAllPropertiesWithInfo());
172 
173             messageKeys.removeAll(allPossibleKeys);
174 
175             masterKeyList.addAll(messageKeys);
176         }
177 
178         for (String key : masterKeyList) {
179             errors.addAll(getMessages(view, key, messageMap.getErrorMessagesForProperty(key, true)));
180             warnings.addAll(getMessages(view, key, messageMap.getWarningMessagesForProperty(key, true)));
181             infos.addAll(getMessages(view, key, messageMap.getInfoMessagesForProperty(key, true)));
182         }
183     }
184 
185     /**
186      * Gets all the messages from the list of lists passed in (which are
187      * lists of ErrorMessages associated to the key) and uses the configuration
188      * service to get the message String associated. This will also combine
189      * error messages per a field if that option is turned on. If
190      * displayFieldLabelWithMessages is turned on, it will also find the label
191      * by key passed in.
192      *
193      * @param view
194      * @param key
195      * @param lists
196      * @return
197      */
198     private List<String> getMessages(View view, String key, List<AutoPopulatingList<ErrorMessage>> lists) {
199         List<String> result = new ArrayList<String>();
200         for (List<ErrorMessage> errorList : lists) {
201             if (errorList != null && StringUtils.isNotBlank(key)) {
202                 for (ErrorMessage e : errorList) {
203                     String message = KRADUtils.getMessageText(e, true);
204                     message = MessageStructureUtils.translateStringMessage(message);
205 
206                     result.add(message);
207                 }
208             }
209         }
210 
211         return result;
212     }
213 
214     /**
215      * Gets all the keys associated to this ValidationMessages. This includes the id of
216      * the parent component, additional keys to match, and the bindingPath if
217      * this is an ValidationMessages for an InputField. These are the keys that are
218      * used to match errors with their component and display them as part of its
219      * ValidationMessages.
220      *
221      * @return
222      */
223     protected List<String> getKeys(Component parent) {
224         List<String> keyList = new ArrayList<String>();
225         if (additionalKeysToMatch != null) {
226             keyList.addAll(additionalKeysToMatch);
227         }
228         if (StringUtils.isNotBlank(parent.getId())) {
229             keyList.add(parent.getId());
230         }
231         if (parent instanceof InputField) {
232             if (((InputField) parent).getBindingInfo() != null && StringUtils.isNotEmpty(
233                     ((InputField) parent).getBindingInfo().getBindingPath())) {
234                 keyList.add(((InputField) parent).getBindingInfo().getBindingPath());
235             }
236         }
237 
238         return keyList;
239     }
240 
241     /**
242      * Adds all group keys of this component (starting from this component itself) by calling getKeys on each of
243      * its nested group's ValidationMessages and adding them to the list.
244      *
245      * @param keyList
246      * @param component
247      */
248     private void addNestedGroupKeys(Collection<String> keyList, Component component) {
249         for (Component c : component.getComponentsForLifecycle()) {
250             ValidationMessages ef = null;
251             if (c instanceof ContainerBase) {
252                 ef = ((ContainerBase) c).getValidationMessages();
253             } else if (c instanceof FieldGroup) {
254                 ef = ((FieldGroup) c).getGroup().getValidationMessages();
255             }
256             if (ef != null) {
257                 keyList.addAll(ef.getKeys(c));
258                 addNestedGroupKeys(keyList, c);
259             }
260         }
261     }
262 
263     /**
264      * AdditionalKeysToMatch is an additional list of keys outside of the
265      * default keys that will be matched when messages are returned after a form
266      * is submitted. These keys are only used for displaying messages generated
267      * by the server and have no effect on client side validation error display.
268      *
269      * @return the additionalKeysToMatch
270      */
271     @BeanTagAttribute(name = "additionalKeysToMatch", type = BeanTagAttribute.AttributeType.LISTVALUE)
272     public List<String> getAdditionalKeysToMatch() {
273         return this.additionalKeysToMatch;
274     }
275 
276     /**
277      * Convenience setter for additional keys to match that takes a string argument and
278      * splits on comma to build the list
279      *
280      * @param additionalKeysToMatch String to parse
281      */
282     public void setAdditionalKeysToMatch(String additionalKeysToMatch) {
283         if (StringUtils.isNotBlank(additionalKeysToMatch)) {
284             this.additionalKeysToMatch = Arrays.asList(StringUtils.split(additionalKeysToMatch, ","));
285         }
286     }
287 
288     /**
289      * @param additionalKeysToMatch the additionalKeysToMatch to set
290      */
291     public void setAdditionalKeysToMatch(List<String> additionalKeysToMatch) {
292         this.additionalKeysToMatch = additionalKeysToMatch;
293     }
294 
295     /**
296      * <p>If true, error, warning, and info messages will be displayed (provided
297      * they are also set to display). Otherwise, no messages for this
298      * ValidationMessages container will be displayed (including ones set to display).
299      * This is a global display on/off switch for all messages.</p>
300      *
301      * <p>Other areas of the screen react to
302      * a display flag being turned off at a certain level, if display is off for a field, the next
303      * level up will display that fields full message text, and if display is off at a section the
304      * next section up will display those messages nested in a sublist.</p>
305      *
306      * @return the displayMessages
307      */
308     @BeanTagAttribute(name = "displayMessages")
309     public boolean isDisplayMessages() {
310         return this.displayMessages;
311     }
312 
313     /**
314      * @param displayMessages the displayMessages to set
315      */
316     public void setDisplayMessages(boolean displayMessages) {
317         this.displayMessages = displayMessages;
318     }
319 
320     /**
321      * The list of error messages found for the keys that were matched on this
322      * ValidationMessages This is generated and cannot be set
323      *
324      * @return the errors
325      */
326     @BeanTagAttribute(name = "errors", type = BeanTagAttribute.AttributeType.LISTVALUE)
327     public List<String> getErrors() {
328         return this.errors;
329     }
330 
331     /**
332      * The list of warning messages found for the keys that were matched on this
333      * ValidationMessages This is generated and cannot be set
334      *
335      * @return the warnings
336      */
337     @BeanTagAttribute(name = "warnings", type = BeanTagAttribute.AttributeType.LISTVALUE)
338     public List<String> getWarnings() {
339         return this.warnings;
340     }
341 
342     /**
343      * The list of info messages found for the keys that were matched on this
344      * ValidationMessages This is generated and cannot be set
345      *
346      * @return the infos
347      */
348     @BeanTagAttribute(name = "infos", type = BeanTagAttribute.AttributeType.LISTVALUE)
349     public List<String> getInfos() {
350         return this.infos;
351     }
352 
353     public Map<String, String> getValidationDataDefaults() {
354         return validationDataDefaults;
355     }
356 
357     public void setValidationDataDefaults(Map<String, String> validationDataDefaults) {
358         this.validationDataDefaults = validationDataDefaults;
359     }
360 
361     protected void addValidationDataSettingsValue(Map<String, Object> valueMap, Map<String, String> defaults,
362             String key, Object value) {
363         String defaultValue = defaults.get(key);
364         if ((defaultValue != null && !value.toString().equals(defaultValue)) || (defaultValue != null && defaultValue
365                 .equals("[]") && value instanceof List && !((List) value).isEmpty()) || defaultValue == null) {
366             valueMap.put(key, value);
367         }
368     }
369 
370     /**
371      * Sets errors
372      *
373      * @param errors
374      */
375     protected void setErrors(List<String> errors) {
376         this.errors = errors;
377     }
378 
379     /**
380      * Sets warnings
381      *
382      * @param warnings
383      */
384     protected void setWarnings(List<String> warnings) {
385         this.warnings = warnings;
386     }
387 
388     /**
389      * Sets infos
390      *
391      * @param infos
392      */
393     protected void setInfos(List<String> infos) {
394         this.infos = infos;
395     }
396 
397     /**
398      * @see org.kuali.rice.krad.datadictionary.DictionaryBeanBase#copyProperties(Object)
399      */
400     @Override
401     protected <T> void copyProperties(T component) {
402         super.copyProperties(component);
403 
404         ValidationMessages validationMessagesCopy = (ValidationMessages) component;
405 
406         if (additionalKeysToMatch != null) {
407             validationMessagesCopy.setAdditionalKeysToMatch(new ArrayList<String>(this.additionalKeysToMatch));
408         }
409 
410         validationMessagesCopy.setDisplayMessages(this.displayMessages);
411 
412         if (warnings != null) {
413             validationMessagesCopy.setWarnings(new ArrayList<String>(this.warnings));
414         }
415 
416         if (errors != null) {
417             validationMessagesCopy.setErrors(new ArrayList<String>(this.errors));
418         }
419 
420         if (infos != null) {
421             validationMessagesCopy.setInfos(new ArrayList<String>(this.infos));
422         }
423 
424         if (this.getValidationDataDefaults() != null) {
425             validationMessagesCopy.setValidationDataDefaults(new HashMap<String, String>(this.validationDataDefaults));
426         }
427     }
428 
429 }