001 /** 002 * Copyright 2005-2013 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 */ 016 package org.kuali.rice.krad.uif.element; 017 018 import com.google.common.collect.Lists; 019 import com.google.common.collect.Maps; 020 import org.apache.commons.lang.StringUtils; 021 import org.kuali.rice.krad.datadictionary.parse.BeanTag; 022 import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute; 023 import org.kuali.rice.krad.uif.UifConstants; 024 import org.kuali.rice.krad.uif.component.Component; 025 import org.kuali.rice.krad.uif.container.Container; 026 import org.kuali.rice.krad.uif.container.ContainerBase; 027 import org.kuali.rice.krad.uif.container.PageGroup; 028 import org.kuali.rice.krad.uif.field.FieldGroup; 029 import org.kuali.rice.krad.uif.field.InputField; 030 import org.kuali.rice.krad.uif.util.MessageStructureUtils; 031 import org.kuali.rice.krad.uif.view.View; 032 import org.kuali.rice.krad.util.ErrorMessage; 033 import org.kuali.rice.krad.util.GlobalVariables; 034 import org.kuali.rice.krad.util.KRADUtils; 035 import org.kuali.rice.krad.util.MessageMap; 036 import org.springframework.util.AutoPopulatingList; 037 038 import java.beans.PropertyEditor; 039 import java.util.ArrayList; 040 import java.util.Arrays; 041 import java.util.Collection; 042 import java.util.HashSet; 043 import java.util.List; 044 import java.util.Map; 045 import java.util.Set; 046 047 /** 048 * Field that displays error, warning, and info messages for the keys that are 049 * matched. By default, an ValidationMessages will match on id and bindingPath (if this 050 * ValidationMessages is for an InputField), but can be set to match on 051 * additionalKeys and nested components keys (of the its parentComponent). 052 * 053 * In addition, there are a variety of options which can be toggled to effect 054 * the display of these messages during both client and server side validation 055 * display. See documentation on each get method for more details on the effect 056 * of each option. 057 * 058 * @author Kuali Rice Team (rice.collab@kuali.org) 059 */ 060 @BeanTag(name = "validationMessages-bean", parent = "Uif-ValidationMessagesBase") 061 public class ValidationMessages extends ContentElementBase { 062 private static final long serialVersionUID = 780940788435330077L; 063 064 private List<String> additionalKeysToMatch; 065 066 private boolean displayMessages; 067 068 // Error messages 069 private List<String> errors; 070 private List<String> warnings; 071 private List<String> infos; 072 073 private Map<String, String> validationDataDefaults; 074 075 /** 076 * PerformFinalize will generate the messages and counts used by the 077 * errorsField based on the keys that were matched from the MessageMap for 078 * this ValidationMessages. It will also set up nestedComponents of its 079 * parentComponent correctly based on the flags that were chosen for this 080 * ValidationMessages. 081 * 082 * @see org.kuali.rice.krad.uif.field.FieldBase#performFinalize(org.kuali.rice.krad.uif.view.View, 083 * java.lang.Object, org.kuali.rice.krad.uif.component.Component) 084 */ 085 @Override 086 public void performFinalize(View view, Object model, Component parent) { 087 super.performFinalize(view, model, parent); 088 089 generateMessages(true, view, model, parent); 090 } 091 092 /** 093 * Generates the messages based on the content in the messageMap 094 * 095 * @param reset true to reset the errors, warnings, and info lists 096 * @param view the current View 097 * @param model the current model 098 * @param parent the parent of this ValidationMessages 099 */ 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 //Add identifying data attributes 125 this.addDataAttribute(UifConstants.DataAttributes.MESSAGES_FOR, parent.getId()); 126 127 if (parent.getDataAttributes().get(UifConstants.DataAttributes.PARENT) == null) { 128 parent.addDataAttribute(UifConstants.DataAttributes.PARENT, parentContainerId); 129 } 130 131 //Handle the special FieldGroup case - adds the FieldGroup itself to ids handled by this group (this must 132 //be a group if its parent is FieldGroup) 133 if (parentContainer != null && parentContainer instanceof FieldGroup) { 134 masterKeyList.add(parentContainerId); 135 } 136 137 //Check for message keys that are not matched anywhere on the page - these unmatched messages must still be 138 //displayed at the page level 139 if (parent instanceof PageGroup) { 140 Map<String, PropertyEditor> propertyEditors = view.getViewIndex().getFieldPropertyEditors(); 141 Map<String, PropertyEditor> securePropertyEditors = view.getViewIndex().getSecureFieldPropertyEditors(); 142 List<String> allPossibleKeys = new ArrayList<String>(propertyEditors.keySet()); 143 allPossibleKeys.addAll(securePropertyEditors.keySet()); 144 145 this.addNestedGroupKeys(allPossibleKeys, parent); 146 if (additionalKeysToMatch != null) { 147 allPossibleKeys.addAll(additionalKeysToMatch); 148 } 149 if (StringUtils.isNotBlank(parent.getId())) { 150 allPossibleKeys.add(parent.getId()); 151 } 152 153 Set<String> messageKeys = new HashSet<String>(); 154 messageKeys.addAll(messageMap.getAllPropertiesWithErrors()); 155 messageKeys.addAll(messageMap.getAllPropertiesWithWarnings()); 156 messageKeys.addAll(messageMap.getAllPropertiesWithInfo()); 157 158 messageKeys.removeAll(allPossibleKeys); 159 160 masterKeyList.addAll(messageKeys); 161 } 162 163 for (String key : masterKeyList) { 164 errors.addAll(getMessages(view, key, messageMap.getErrorMessagesForProperty(key, true))); 165 warnings.addAll(getMessages(view, key, messageMap.getWarningMessagesForProperty(key, true))); 166 infos.addAll(getMessages(view, key, messageMap.getInfoMessagesForProperty(key, true))); 167 } 168 } 169 170 /** 171 * Gets all the messages from the list of lists passed in (which are 172 * lists of ErrorMessages associated to the key) and uses the configuration 173 * service to get the message String associated. This will also combine 174 * error messages per a field if that option is turned on. If 175 * displayFieldLabelWithMessages is turned on, it will also find the label 176 * by key passed in. 177 * 178 * @param view 179 * @param key 180 * @param lists 181 * @return 182 */ 183 private List<String> getMessages(View view, String key, List<AutoPopulatingList<ErrorMessage>> lists) { 184 List<String> result = new ArrayList<String>(); 185 for (List<ErrorMessage> errorList : lists) { 186 if (errorList != null && StringUtils.isNotBlank(key)) { 187 for (ErrorMessage e : errorList) { 188 String message = KRADUtils.getMessageText(e, true); 189 message = MessageStructureUtils.translateStringMessage(message); 190 191 result.add(message); 192 } 193 } 194 } 195 196 return result; 197 } 198 199 /** 200 * Gets all the keys associated to this ValidationMessages. This includes the id of 201 * the parent component, additional keys to match, and the bindingPath if 202 * this is an ValidationMessages for an InputField. These are the keys that are 203 * used to match errors with their component and display them as part of its 204 * ValidationMessages. 205 * 206 * @return 207 */ 208 protected List<String> getKeys(Component parent) { 209 List<String> keyList = new ArrayList<String>(); 210 if (additionalKeysToMatch != null) { 211 keyList.addAll(additionalKeysToMatch); 212 } 213 if (StringUtils.isNotBlank(parent.getId())) { 214 keyList.add(parent.getId()); 215 } 216 if (parent instanceof InputField) { 217 if (((InputField) parent).getBindingInfo() != null && StringUtils.isNotEmpty( 218 ((InputField) parent).getBindingInfo().getBindingPath())) { 219 keyList.add(((InputField) parent).getBindingInfo().getBindingPath()); 220 } 221 } 222 223 return keyList; 224 } 225 226 /** 227 * Adds all group keys of this component (starting from this component itself) by calling getKeys on each of 228 * its nested group's ValidationMessages and adding them to the list. 229 * 230 * @param keyList 231 * @param component 232 */ 233 private void addNestedGroupKeys(Collection<String> keyList, Component component) { 234 for (Component c : component.getComponentsForLifecycle()) { 235 ValidationMessages ef = null; 236 if (c instanceof ContainerBase) { 237 ef = ((ContainerBase) c).getValidationMessages(); 238 } else if (c instanceof FieldGroup) { 239 ef = ((FieldGroup) c).getGroup().getValidationMessages(); 240 } 241 if (ef != null) { 242 keyList.addAll(ef.getKeys(c)); 243 addNestedGroupKeys(keyList, c); 244 } 245 } 246 } 247 248 /** 249 * AdditionalKeysToMatch is an additional list of keys outside of the 250 * default keys that will be matched when messages are returned after a form 251 * is submitted. These keys are only used for displaying messages generated 252 * by the server and have no effect on client side validation error display. 253 * 254 * @return the additionalKeysToMatch 255 */ 256 @BeanTagAttribute(name = "additionalKeysToMatch", type = BeanTagAttribute.AttributeType.LISTVALUE) 257 public List<String> getAdditionalKeysToMatch() { 258 return this.additionalKeysToMatch; 259 } 260 261 /** 262 * Convenience setter for additional keys to match that takes a string argument and 263 * splits on comma to build the list 264 * 265 * @param additionalKeysToMatch String to parse 266 */ 267 public void setAdditionalKeysToMatch(String additionalKeysToMatch) { 268 if (StringUtils.isNotBlank(additionalKeysToMatch)) { 269 this.additionalKeysToMatch = Arrays.asList(StringUtils.split(additionalKeysToMatch, ",")); 270 } 271 } 272 273 /** 274 * @param additionalKeysToMatch the additionalKeysToMatch to set 275 */ 276 public void setAdditionalKeysToMatch(List<String> additionalKeysToMatch) { 277 this.additionalKeysToMatch = additionalKeysToMatch; 278 } 279 280 /** 281 * <p>If true, error, warning, and info messages will be displayed (provided 282 * they are also set to display). Otherwise, no messages for this 283 * ValidationMessages container will be displayed (including ones set to display). 284 * This is a global display on/off switch for all messages.</p> 285 * 286 * <p>Other areas of the screen react to 287 * a display flag being turned off at a certain level, if display is off for a field, the next 288 * level up will display that fields full message text, and if display is off at a section the 289 * next section up will display those messages nested in a sublist.</p> 290 * 291 * @return the displayMessages 292 */ 293 @BeanTagAttribute(name = "displayMessages") 294 public boolean isDisplayMessages() { 295 return this.displayMessages; 296 } 297 298 /** 299 * @param displayMessages the displayMessages to set 300 */ 301 public void setDisplayMessages(boolean displayMessages) { 302 this.displayMessages = displayMessages; 303 } 304 305 /** 306 * The list of error messages found for the keys that were matched on this 307 * ValidationMessages This is generated and cannot be set 308 * 309 * @return the errors 310 */ 311 @BeanTagAttribute(name = "errors", type = BeanTagAttribute.AttributeType.LISTVALUE) 312 public List<String> getErrors() { 313 return this.errors; 314 } 315 316 /** 317 * The list of warning messages found for the keys that were matched on this 318 * ValidationMessages This is generated and cannot be set 319 * 320 * @return the warnings 321 */ 322 @BeanTagAttribute(name = "warnings", type = BeanTagAttribute.AttributeType.LISTVALUE) 323 public List<String> getWarnings() { 324 return this.warnings; 325 } 326 327 /** 328 * The list of info messages found for the keys that were matched on this 329 * ValidationMessages This is generated and cannot be set 330 * 331 * @return the infos 332 */ 333 @BeanTagAttribute(name = "infos", type = BeanTagAttribute.AttributeType.LISTVALUE) 334 public List<String> getInfos() { 335 return this.infos; 336 } 337 338 public Map<String, String> getValidationDataDefaults() { 339 return validationDataDefaults; 340 } 341 342 public void setValidationDataDefaults(Map<String, String> validationDataDefaults) { 343 this.validationDataDefaults = validationDataDefaults; 344 } 345 346 protected void addValidationDataSettingsValue(Map<String, Object> valueMap, Map<String, String> defaults, 347 String key, Object value) { 348 String defaultValue = defaults.get(key); 349 if ((defaultValue != null && !value.toString().equals(defaultValue)) || (defaultValue != null && defaultValue 350 .equals("[]") && value instanceof List && !((List) value).isEmpty()) || defaultValue == null) { 351 valueMap.put(key, value); 352 } 353 } 354 355 public void setErrors(List<String> errors) { 356 this.errors = errors; 357 } 358 359 public void setWarnings(List<String> warnings) { 360 this.warnings = warnings; 361 } 362 363 public void setInfos(List<String> infos) { 364 this.infos = infos; 365 } 366 367 /** 368 * @see org.kuali.rice.krad.uif.component.ComponentBase#copy() 369 */ 370 @Override 371 protected <T> void copyProperties(T component) { 372 super.copyProperties(component); 373 ValidationMessages validationMessagesCopy = (ValidationMessages) component; 374 375 if (additionalKeysToMatch != null) { 376 List<String> additionalKeysToMatchCopy = Lists.newArrayListWithExpectedSize(additionalKeysToMatch.size()); 377 for(String additionalKeyToMatch : additionalKeysToMatch) { 378 additionalKeysToMatchCopy.add(additionalKeyToMatch); 379 } 380 381 validationMessagesCopy.setAdditionalKeysToMatch(additionalKeysToMatchCopy); 382 } 383 384 validationMessagesCopy.setDisplayMessages(this.isDisplayMessages()); 385 386 if (warnings != null) { 387 // Error messages 388 List<String> warningsCopy = Lists.newArrayListWithExpectedSize(warnings.size()); 389 for(String warning : warnings) { 390 warningsCopy.add(warning); 391 } 392 393 validationMessagesCopy.setWarnings(warningsCopy); 394 } 395 396 if (errors != null) { 397 List<String> errorsCopy = Lists.newArrayListWithExpectedSize(errors.size()); 398 for(String error : errors) { 399 errorsCopy.add(error); 400 } 401 402 validationMessagesCopy.setErrors(errorsCopy); 403 } 404 405 if (infos != null) { 406 List<String> infosCopy = Lists.newArrayListWithExpectedSize(infos.size()); 407 for(String info : infos) { 408 infosCopy.add(info); 409 } 410 411 validationMessagesCopy.setInfos(infosCopy); 412 } 413 414 if (this.getValidationDataDefaults() != null) { 415 Map<String, String> validationDataDefaultsCopy = Maps.newHashMapWithExpectedSize( 416 this.getValidationDataDefaults().size()); 417 for(Map.Entry validationDataDefault : getValidationDataDefaults().entrySet()) { 418 validationDataDefaultsCopy.put(validationDataDefault.getKey().toString(),validationDataDefault.getValue().toString()); 419 } 420 421 validationMessagesCopy.setValidationDataDefaults(validationDataDefaultsCopy); 422 } 423 } 424 425 }