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}