View Javadoc
1   /**
2    * Copyright 2005-2015 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.text.MessageFormat;
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collection;
22  import java.util.LinkedList;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Queue;
26  
27  import org.apache.commons.lang.StringUtils;
28  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
29  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
30  import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBeanBase;
31  import org.kuali.rice.krad.messages.MessageService;
32  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
33  import org.kuali.rice.krad.uif.UifConstants;
34  import org.kuali.rice.krad.uif.component.Component;
35  import org.kuali.rice.krad.uif.component.DataBinding;
36  import org.kuali.rice.krad.uif.container.Container;
37  import org.kuali.rice.krad.uif.container.ContainerBase;
38  import org.kuali.rice.krad.uif.field.FieldGroup;
39  import org.kuali.rice.krad.uif.field.InputField;
40  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleUtils;
41  import org.kuali.rice.krad.uif.util.LifecycleElement;
42  import org.kuali.rice.krad.uif.util.MessageStructureUtils;
43  import org.kuali.rice.krad.uif.util.RecycleUtils;
44  import org.kuali.rice.krad.uif.view.View;
45  import org.kuali.rice.krad.util.AuditCluster;
46  import org.kuali.rice.krad.util.AuditError;
47  import org.kuali.rice.krad.util.ErrorMessage;
48  import org.kuali.rice.krad.util.GlobalVariables;
49  import org.kuali.rice.krad.util.KRADConstants;
50  import org.kuali.rice.krad.util.KRADUtils;
51  import org.kuali.rice.krad.util.MessageMap;
52  
53  /**
54   * Field that displays error, warning, and info messages for the keys that are
55   * matched. By default, an ValidationMessages will match on id and bindingPath (if this
56   * ValidationMessages is for an InputField), but can be set to match on
57   * additionalKeys and nested components keys (of the its parentComponent).
58   *
59   * In addition, there are a variety of options which can be toggled to effect
60   * the display of these messages during both client and server side validation
61   * display. See documentation on each get method for more details on the effect
62   * of each option.
63   *
64   * @author Kuali Rice Team (rice.collab@kuali.org)
65   */
66  @BeanTag(name = "validationMessages", parent = "Uif-ValidationMessagesBase")
67  public class ValidationMessages extends UifDictionaryBeanBase {
68      private static final long serialVersionUID = 780940788435330077L;
69  
70      private List<String> additionalKeysToMatch;
71  
72      private boolean displayMessages;
73  
74      // Error messages
75      private List<String> errors;
76      private List<String> warnings;
77      private List<String> infos;
78  
79      /**
80       * Generates the messages based on the content in the messageMap
81       *
82       * @param view the current View
83       * @param model the current model
84       * @param parent the parent of this ValidationMessages
85       */
86      public void generateMessages(View view, Object model, Component parent) {
87          errors = new ArrayList<String>();
88          warnings = new ArrayList<String>();
89          infos = new ArrayList<String>();
90  
91          List<String> masterKeyList = getKeys(parent);
92          MessageMap messageMap = GlobalVariables.getMessageMap();
93  
94          String parentContainerId = "";
95  
96          Map<String, Object> parentContext = parent.getContext();
97          Object parentContainer = parentContext == null ? null : parentContext
98                  .get(UifConstants.ContextVariableNames.PARENT);
99  
100         if (parentContainer != null && (parentContainer instanceof Container
101                 || parentContainer instanceof FieldGroup)) {
102             parentContainerId = ((Component) parentContainer).getId();
103         }
104 
105         // special message component case
106         if (parentContainer != null && parentContainer instanceof Message && ((Message) parentContainer)
107                 .isRenderWrapperTag()) {
108             parentContainerId = ((Component) parentContainer).getId();
109         }
110 
111         // special case for nested contentElement with no parent
112         if (parentContainer != null && parentContainer instanceof Component && StringUtils.isBlank(parentContainerId)) {
113             parentContext = ((Component) parentContainer).getContext();
114             parentContainer = parentContext == null ? null : parentContext
115                     .get(UifConstants.ContextVariableNames.PARENT);
116             if (parentContainer != null && (parentContainer instanceof Container
117                     || parentContainer instanceof FieldGroup)) {
118                 parentContainerId = ((Component) parentContainer).getId();
119             }
120         }
121 
122         if ((parent.getDataAttributes() == null) || (parent.getDataAttributes().get(UifConstants.DataAttributes.PARENT)
123                 == null)) {
124             parent.addDataAttribute(UifConstants.DataAttributes.PARENT, parentContainerId);
125         }
126 
127         //Handle the special FieldGroup case - adds the FieldGroup itself to ids handled by this group (this must
128         //be a group if its parent is FieldGroup)
129         if (parentContainer != null && parentContainer instanceof FieldGroup) {
130             masterKeyList.add(parentContainerId);
131         }
132 
133         processAuditErrors(masterKeyList);
134 
135         for (String key : masterKeyList) {
136             errors.addAll(getMessages(view, key, messageMap.getErrorMessagesForProperty(key, true)));
137             warnings.addAll(getMessages(view, key, messageMap.getWarningMessagesForProperty(key, true)));
138             infos.addAll(getMessages(view, key, messageMap.getInfoMessagesForProperty(key, true)));
139         }
140     }
141 
142     /**
143      * Process any AuditErrors which exist in AuditClusters in the AuditErrorMap of GlobalVariables and add them
144      * to either errors or warnings for this component, matching on errorKey.
145      *
146      * @param masterKeyList the keys to look for
147      */
148     private void processAuditErrors(List<String> masterKeyList) {
149         Map<String, AuditCluster> clusterMap = GlobalVariables.getAuditErrorMap();
150 
151         for (AuditCluster auditCluster : clusterMap.values()) {
152             boolean isError = !(auditCluster.getCategory().equals(KRADConstants.Audit.AUDIT_WARNINGS));
153 
154             List<AuditError> auditErrors = auditCluster.getAuditErrorList();
155             if (auditErrors == null) {
156                 continue;
157             }
158 
159             for (AuditError auditError: auditErrors) {
160                 if (!masterKeyList.contains(auditError.getValidationKey())) {
161                     continue;
162                 }
163 
164                 MessageService messageService = KRADServiceLocatorWeb.getMessageService();
165 
166                 // find message by key
167                 String message = messageService.getMessageText(auditError.getMessageKey());
168                 if (message == null) {
169                     message = "Intended message with key: " + auditError.getErrorKey() + " not found.";
170                 }
171 
172                 if (auditError.getParams() != null && StringUtils.isNotBlank(message)) {
173                     message = message.replace("'", "''");
174                     message = MessageFormat.format(message, auditError.getParams());
175                 }
176 
177                 message = MessageStructureUtils.translateStringMessage(message);
178 
179                 if (isError) {
180                     errors.add(message);
181                 }
182                 else {
183                     warnings.add(message);
184                 }
185             }
186         }
187     }
188 
189     /**
190      * Gets all the messages from the list of lists passed in (which are
191      * lists of ErrorMessages associated to the key) and uses the configuration
192      * service to get the message String associated. This will also combine
193      * error messages per a field if that option is turned on. If
194      * displayFieldLabelWithMessages is turned on, it will also find the label
195      * by key passed in.
196      *
197      * @param view
198      * @param key
199      * @param lists
200      * @return list of messages
201      */
202     protected List<String> getMessages(View view, String key, List<List<ErrorMessage>> lists) {
203         List<String> result = new ArrayList<String>();
204         for (List<ErrorMessage> errorList : lists) {
205             if (errorList != null && StringUtils.isNotBlank(key)) {
206                 for (ErrorMessage e : errorList) {
207                     String message = KRADUtils.getMessageText(e, true);
208                     message = MessageStructureUtils.translateStringMessage(message);
209 
210                     result.add(message);
211                 }
212             }
213         }
214 
215         return result;
216     }
217 
218     /**
219      * Gets all the keys associated to this ValidationMessages. This includes the id of
220      * the parent component, additional keys to match, and the bindingPath if
221      * this is a ValidationMessages for a DataBinding component. These are the keys that are
222      * used to match errors with their component and display them as part of its
223      * ValidationMessages.
224      *
225      * @return list of keys
226      */
227     protected List<String> getKeys(Component parent) {
228         List<String> keyList = new ArrayList<String>();
229 
230         if (additionalKeysToMatch != null) {
231             keyList.addAll(additionalKeysToMatch);
232         }
233 
234         if (StringUtils.isNotBlank(parent.getId())) {
235             keyList.add(parent.getId());
236         }
237 
238         if (parent instanceof DataBinding) {
239             if (((DataBinding) parent).getBindingInfo() != null && StringUtils.isNotEmpty(
240                     ((DataBinding) parent).getBindingInfo().getBindingPath())) {
241                 keyList.add(((DataBinding) parent).getBindingInfo().getBindingPath());
242             }
243         }
244 
245         return keyList;
246     }
247 
248     /**
249      * Adds all group keys of this component (starting from this component itself) by calling getKeys on each of
250      * its nested group's ValidationMessages and adding them to the list.
251      *
252      * @param keyList
253      * @param component
254      */
255     protected void addNestedGroupKeys(Collection<String> keyList, Component component) {
256         @SuppressWarnings("unchecked")
257         Queue<LifecycleElement> elementQueue = RecycleUtils.getInstance(LinkedList.class);
258         try {
259             elementQueue.addAll(ViewLifecycleUtils.getElementsForLifecycle(component).values());
260             while (!elementQueue.isEmpty()) {
261                 LifecycleElement element = elementQueue.poll();
262 
263                 ValidationMessages ef = null;
264                 if (element instanceof ContainerBase) {
265                     ef = ((ContainerBase) element).getValidationMessages();
266                 } else if (element instanceof FieldGroup) {
267                     ef = ((FieldGroup) element).getGroup().getValidationMessages();
268                 }
269                 
270                 if (ef != null) {
271                     keyList.addAll(ef.getKeys((Component) element));
272                 }
273 
274                 elementQueue.addAll(ViewLifecycleUtils.getElementsForLifecycle(element).values());
275             }
276         } finally {
277             elementQueue.clear();
278             RecycleUtils.recycle(elementQueue);
279         }
280     }
281 
282     /**
283      * AdditionalKeysToMatch is an additional list of keys outside of the
284      * default keys that will be matched when messages are returned after a form
285      * is submitted. These keys are only used for displaying messages generated
286      * by the server and have no effect on client side validation error display.
287      *
288      * @return the additionalKeysToMatch
289      */
290     @BeanTagAttribute
291     public List<String> getAdditionalKeysToMatch() {
292         return this.additionalKeysToMatch;
293     }
294 
295     /**
296      * Convenience setter for additional keys to match that takes a string argument and
297      * splits on comma to build the list
298      *
299      * @param additionalKeysToMatch String to parse
300      */
301     public void setAdditionalKeysToMatch(String additionalKeysToMatch) {
302         if (StringUtils.isNotBlank(additionalKeysToMatch)) {
303             this.additionalKeysToMatch = Arrays.asList(StringUtils.split(additionalKeysToMatch, ","));
304         }
305     }
306 
307     /**
308      * @param additionalKeysToMatch the additionalKeysToMatch to set
309      */
310     public void setAdditionalKeysToMatch(List<String> additionalKeysToMatch) {
311         this.additionalKeysToMatch = additionalKeysToMatch;
312     }
313 
314     /**
315      * <p>If true, error, warning, and info messages will be displayed (provided
316      * they are also set to display). Otherwise, no messages for this
317      * ValidationMessages container will be displayed (including ones set to display).
318      * This is a global display on/off switch for all messages.</p>
319      *
320      * <p>Other areas of the screen react to
321      * a display flag being turned off at a certain level, if display is off for a field, the next
322      * level up will display that fields full message text, and if display is off at a section the
323      * next section up will display those messages nested in a sublist.</p>
324      *
325      * @return the displayMessages
326      */
327     @BeanTagAttribute
328     public boolean isDisplayMessages() {
329         return this.displayMessages;
330     }
331 
332     /**
333      * @param displayMessages the displayMessages to set
334      */
335     public void setDisplayMessages(boolean displayMessages) {
336         this.displayMessages = displayMessages;
337     }
338 
339     /**
340      * The list of error messages found for the keys that were matched on this
341      * ValidationMessages This is generated and cannot be set
342      *
343      * @return the errors
344      */
345     @BeanTagAttribute
346     public List<String> getErrors() {
347         return this.errors;
348     }
349 
350     /**
351      * @see ValidationMessages#getErrors()
352      */
353     protected void setErrors(List<String> errors) {
354         this.errors = errors;
355     }
356 
357     /**
358      * The list of warning messages found for the keys that were matched on this
359      * ValidationMessages This is generated and cannot be set
360      *
361      * @return the warnings
362      */
363     @BeanTagAttribute
364     public List<String> getWarnings() {
365         return this.warnings;
366     }
367 
368     /**
369      * @see ValidationMessages#getWarnings()
370      */
371     protected void setWarnings(List<String> warnings) {
372         this.warnings = warnings;
373     }
374 
375     /**
376      * The list of info messages found for the keys that were matched on this
377      * ValidationMessages This is generated and cannot be set
378      *
379      * @return the infos
380      */
381     @BeanTagAttribute
382     public List<String> getInfos() {
383         return this.infos;
384     }
385 
386     /**
387      * @see ValidationMessages#getInfos()
388      */
389     protected void setInfos(List<String> infos) {
390         this.infos = infos;
391     }
392 
393     /**
394      * Adds the value passed to the valueMap with the key specified, if the value does not match the
395      * value which already exists in defaults (to avoid having to write out extra data that can later
396      * be derived from the defaults in the js)
397      *
398      * @param valueMap the data map being constructed
399      * @param defaults defaults for validation messages
400      * @param key the variable name being added
401      * @param value the value set on this object
402      */
403     protected void addValidationDataSettingsValue(Map<String, Object> valueMap, Map<String, String> defaults,
404             String key, Object value) {
405         String defaultValue = defaults.get(key);
406         if ((defaultValue != null && !value.toString().equals(defaultValue)) || (defaultValue != null && defaultValue
407                 .equals("[]") && value instanceof List && !((List) value).isEmpty()) || defaultValue == null) {
408             valueMap.put(key, value);
409         }
410     }
411 
412 }