001/** 002 * Copyright 2005-2018 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.uif.element; 017 018import java.text.MessageFormat; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.LinkedList; 023import java.util.List; 024import java.util.Map; 025import java.util.Queue; 026 027import org.apache.commons.lang.StringUtils; 028import org.kuali.rice.krad.datadictionary.parse.BeanTag; 029import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute; 030import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBeanBase; 031import org.kuali.rice.krad.messages.MessageService; 032import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 033import org.kuali.rice.krad.uif.UifConstants; 034import org.kuali.rice.krad.uif.component.Component; 035import org.kuali.rice.krad.uif.component.DataBinding; 036import org.kuali.rice.krad.uif.container.Container; 037import org.kuali.rice.krad.uif.container.ContainerBase; 038import org.kuali.rice.krad.uif.field.FieldGroup; 039import org.kuali.rice.krad.uif.field.InputField; 040import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleUtils; 041import org.kuali.rice.krad.uif.util.LifecycleElement; 042import org.kuali.rice.krad.uif.util.MessageStructureUtils; 043import org.kuali.rice.krad.uif.util.RecycleUtils; 044import org.kuali.rice.krad.uif.view.View; 045import org.kuali.rice.krad.util.AuditCluster; 046import org.kuali.rice.krad.util.AuditError; 047import org.kuali.rice.krad.util.ErrorMessage; 048import org.kuali.rice.krad.util.GlobalVariables; 049import org.kuali.rice.krad.util.KRADConstants; 050import org.kuali.rice.krad.util.KRADUtils; 051import org.kuali.rice.krad.util.MessageMap; 052 053/** 054 * Field that displays error, warning, and info messages for the keys that are 055 * matched. By default, an ValidationMessages will match on id and bindingPath (if this 056 * ValidationMessages is for an InputField), but can be set to match on 057 * additionalKeys and nested components keys (of the its parentComponent). 058 * 059 * In addition, there are a variety of options which can be toggled to effect 060 * the display of these messages during both client and server side validation 061 * display. See documentation on each get method for more details on the effect 062 * of each option. 063 * 064 * @author Kuali Rice Team (rice.collab@kuali.org) 065 */ 066@BeanTag(name = "validationMessages", parent = "Uif-ValidationMessagesBase") 067public class ValidationMessages extends UifDictionaryBeanBase { 068 private static final long serialVersionUID = 780940788435330077L; 069 070 private List<String> additionalKeysToMatch; 071 072 private boolean displayMessages; 073 074 // Error messages 075 private List<String> errors; 076 private List<String> warnings; 077 private List<String> infos; 078 079 /** 080 * Generates the messages based on the content in the messageMap 081 * 082 * @param view the current View 083 * @param model the current model 084 * @param parent the parent of this ValidationMessages 085 */ 086 public void generateMessages(View view, Object model, Component parent) { 087 errors = new ArrayList<String>(); 088 warnings = new ArrayList<String>(); 089 infos = new ArrayList<String>(); 090 091 List<String> masterKeyList = getKeys(parent); 092 MessageMap messageMap = GlobalVariables.getMessageMap(); 093 094 String parentContainerId = ""; 095 096 Map<String, Object> parentContext = parent.getContext(); 097 Object parentContainer = parentContext == null ? null : parentContext 098 .get(UifConstants.ContextVariableNames.PARENT); 099 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}