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 }