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    }