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 org.apache.commons.lang.StringUtils;
019import org.kuali.rice.krad.datadictionary.parse.BeanTag;
020import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
021import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
022import org.kuali.rice.krad.uif.UifConstants;
023import org.kuali.rice.krad.uif.component.Component;
024import org.kuali.rice.krad.uif.container.CollectionGroup;
025import org.kuali.rice.krad.uif.container.Container;
026import org.kuali.rice.krad.uif.container.LightTable;
027import org.kuali.rice.krad.uif.container.PageGroup;
028import org.kuali.rice.krad.uif.field.FieldGroup;
029import org.kuali.rice.krad.uif.field.InputField;
030import org.kuali.rice.krad.uif.layout.StackedLayoutManager;
031import org.kuali.rice.krad.uif.layout.TableLayoutManager;
032import org.kuali.rice.krad.uif.util.ScriptUtils;
033import org.kuali.rice.krad.uif.view.View;
034import org.kuali.rice.krad.util.GlobalVariables;
035
036import java.util.ArrayList;
037import java.util.HashMap;
038import java.util.List;
039import java.util.Map;
040
041/**
042 * ValidationMessages for logic and options specific to groups.
043 *
044 * @author Kuali Rice Team (rice.collab@kuali.org)
045 */
046@BeanTag(name = "groupValidationMessages", parent = "Uif-GroupValidationMessages")
047public class GroupValidationMessages extends ValidationMessages {
048
049    private static final long serialVersionUID = -5389990220206079052L;
050
051    private boolean closeable;
052
053    private boolean displayFieldLabelWithMessages;
054    private boolean collapseAdditionalFieldLinkMessages;
055    private boolean displayHeaderMessageSummary;
056
057    private static final String SECTION_TOKEN = "s$";
058    private static final String FIELDGROUP_TOKEN = "f$";
059    private static final String TABLE_COLLECTION_TOKEN = "c$";
060
061    /**
062     * {@inheritDoc}
063     */
064    @Override
065    public void generateMessages(View view, Object model, Component parent) {
066        super.generateMessages(view, model, parent);
067
068        addValidationMessageDataAttributes(parent);
069    }
070
071    /**
072     * Adds dataAttributes that are appropriate for group level validationMessages data.
073     *
074     * <p>This data is used by the validation framework clientside. Some special handling at this level includes
075     * retrieving the groups and fields generated by different collection layouts and handling page and fieldGroup
076     * scenarios slightly differently due to the nature of how they are built out in the js.</p>
077     *
078     * @param parent component that is the parent of the validation messages
079     */
080    protected void addValidationMessageDataAttributes(Component parent) {
081        HashMap<String, Object> validationMessagesDataAttributes = new HashMap<String, Object>();
082
083        Map<String, Object> parentContext = parent.getContext();
084        Object parentContainer = parentContext == null ? null : parentContext.get(
085                UifConstants.ContextVariableNames.PARENT);
086
087        List<? extends Component> items = ((Container) parent).getItems();
088        boolean skipSections = false;
089        boolean isTableCollection = false;
090
091        // Handle the special CollectionGroup case by getting the StackedGroups and DataFields generated by them
092        if (parent instanceof CollectionGroup && ((CollectionGroup) parent)
093                .getLayoutManager() instanceof StackedLayoutManager) {
094            items = ((StackedLayoutManager) ((CollectionGroup) parent).getLayoutManager()).getStackedGroups();
095        } else if ((parent instanceof CollectionGroup && ((CollectionGroup) parent)
096                .getLayoutManager() instanceof TableLayoutManager) || parent instanceof LightTable) {
097            // order is not needed  so null items
098            items = null;
099            skipSections = true;
100            isTableCollection = true;
101        }
102
103        List<String> sectionIds = new ArrayList<String>();
104        List<String> fieldOrder = new ArrayList<String>();
105        collectIdsFromItems(items, sectionIds, fieldOrder, skipSections);
106
107        boolean pageLevel = false;
108        boolean forceShow = false;
109        boolean showPageSummaryHeader = true;
110        if (parent instanceof PageGroup) {
111            pageLevel = true;
112            forceShow = true;
113            parent.addDataAttribute(UifConstants.DataAttributes.SERVER_MESSAGES, Boolean.toString(
114                    GlobalVariables.getMessageMap().hasMessages() || !GlobalVariables.getAuditErrorMap().isEmpty()));
115            if (this instanceof PageValidationMessages) {
116                showPageSummaryHeader = ((PageValidationMessages) this).isShowPageSummaryHeader();
117            }
118        } else if (parentContainer instanceof FieldGroup) {
119            Map<String, String> parentFieldGroupDataAttributes = ((FieldGroup) parentContainer).getDataAttributes();
120            String role = parentFieldGroupDataAttributes == null ? null : parentFieldGroupDataAttributes.get(
121                    UifConstants.DataAttributes.ROLE);
122            if (StringUtils.isNotBlank(role) && role.equals("detailsFieldGroup")) {
123                forceShow = false;
124            } else {
125                //note this means container of the parent is a FieldGroup
126                forceShow = true;
127            }
128        }
129
130        boolean hasMessages = false;
131        if (!this.getErrors().isEmpty() || !this.getWarnings().isEmpty() || !this.getInfos().isEmpty()) {
132            hasMessages = true;
133        }
134
135        Map<String, String> dataDefaults =
136                (Map<String, String>) (KRADServiceLocatorWeb.getDataDictionaryService().getDictionaryBean(
137                        "Uif-GroupValidationMessages-DataDefaults"));
138
139        //add necessary data attributes to map
140        //display related
141        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
142                UifConstants.DataAttributes.SUMMARIZE, true);
143        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
144                UifConstants.DataAttributes.DISPLAY_MESSAGES, this.isDisplayMessages());
145        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
146                UifConstants.DataAttributes.CLOSEABLE, this.isCloseable());
147        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
148                UifConstants.DataAttributes.COLLAPSE_FIELD_MESSAGES, collapseAdditionalFieldLinkMessages);
149        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
150                UifConstants.DataAttributes.SHOW_PAGE_SUMMARY_HEADER, showPageSummaryHeader);
151        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
152                UifConstants.DataAttributes.DISPLAY_LABEL, displayFieldLabelWithMessages);
153        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
154                UifConstants.DataAttributes.DISPLAY_HEADER_SUMMARY, displayHeaderMessageSummary);
155        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
156                UifConstants.DataAttributes.IS_TABLE_COLLECTION, isTableCollection);
157
158        //options
159        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
160                UifConstants.DataAttributes.HAS_OWN_MESSAGES, hasMessages);
161        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
162                UifConstants.DataAttributes.PAGE_LEVEL, pageLevel);
163        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
164                UifConstants.DataAttributes.FORCE_SHOW, forceShow);
165
166        //order related
167        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
168                UifConstants.DataAttributes.SECTIONS, sectionIds);
169        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
170                UifConstants.DataAttributes.ORDER, fieldOrder);
171
172        //server messages
173        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
174                UifConstants.DataAttributes.SERVER_ERRORS, ScriptUtils.escapeHtml(this.getErrors()));
175        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
176                UifConstants.DataAttributes.SERVER_WARNINGS, ScriptUtils.escapeHtml(this.getWarnings()));
177        this.addValidationDataSettingsValue(validationMessagesDataAttributes, dataDefaults,
178                UifConstants.DataAttributes.SERVER_INFO, ScriptUtils.escapeHtml(this.getInfos()));
179
180        if (!validationMessagesDataAttributes.isEmpty()) {
181            parent.addScriptDataAttribute(UifConstants.DataAttributes.VALIDATION_MESSAGES, ScriptUtils.translateValue(
182                    validationMessagesDataAttributes));
183        }
184    }
185
186    /**
187     * Collects all the ids from the items passed into this method.
188     *
189     * <p>Puts the ids of items determined to be sections
190     * into the sectionIds list, and orders all items by the order they appear on the page in the order list with
191     * special identifiers
192     * to determine the type of item they are (used by the client js).  When skipSections is true do not
193     * include sectionIds found in the lists.</p>
194     *
195     * @param items items of the group
196     * @param sectionIds list to put section ids into
197     * @param order list to put order of ids into (both fields and sections)
198     * @param skipSections skip adding sections
199     */
200    protected void collectIdsFromItems(List<? extends Component> items, List<String> sectionIds, List<String> order,
201            boolean skipSections) {
202
203        if (items == null) {
204            return;
205        }
206
207        for (Component component : items) {
208            String id = StringUtils.replace(component.getId(), "@id@", "");
209
210            if (component instanceof FieldGroup) {
211                boolean treatFieldGroupAsSection = ((FieldGroup) component).getFieldLabel().isRender() &&
212                        !((FieldGroup) component).getFieldLabel().isHidden() &&
213                        (StringUtils.isNotEmpty(((FieldGroup) component).getLabel()) || StringUtils.isNotEmpty(
214                                ((FieldGroup) component).getFieldLabel().getLabelText()));
215
216                if (!skipSections && treatFieldGroupAsSection) {
217                    sectionIds.add(id);
218
219                    // Add with field group identifier
220                    order.add(FIELDGROUP_TOKEN + id);
221                    continue;
222                } else {
223                    component = ((FieldGroup) component).getGroup();
224                    if (component == null) {
225                        continue;
226                    }
227                }
228            }
229
230            // Not an 'else if' because component being evaluated can change to container above^
231            if (component instanceof Container) {
232                id = StringUtils.replace(component.getId(), "@id@", "");
233
234                //If any kind of header text is showing consider this group a section
235                boolean isSection =
236                        ((Container) component).getHeader() != null && ((Container) component).getHeader().isRender()
237                                && (StringUtils.isNotBlank(((Container) component).getHeader().getHeaderText())
238                                || StringUtils.isNotBlank(component.getTitle()));
239
240                if (!skipSections && isSection) {
241                    sectionIds.add(id);
242
243                    // Add with section identifier
244                    order.add(SECTION_TOKEN + id);
245                } else if ((component instanceof CollectionGroup && ((CollectionGroup) component)
246                        .getLayoutManager() instanceof TableLayoutManager) || component instanceof LightTable) {
247                    // Add with collection identifier
248                    order.add(TABLE_COLLECTION_TOKEN + id);
249                } else {
250                    // If a non-section container, collect ids from sub items
251                    collectIdsFromItems(((Container) component).getItems(), sectionIds, order, skipSections);
252                }
253            } else if (component instanceof InputField) {
254                order.add(id);
255            }
256        }
257    }
258
259    /**
260     * If true, validation message divs are closeable(dismissable) by the user and
261     * they do not return until another page level validation/component refresh/page refresh is invoked
262     *
263     * @return true
264     */
265    public boolean isCloseable() {
266        return closeable;
267    }
268
269    /**
270     * @see GroupValidationMessages#isCloseable()
271     */
272    public void setCloseable(boolean closeable) {
273        this.closeable = closeable;
274    }
275
276    /**
277     * If true, the error messages will display the an InputField's title
278     * alongside the error, warning, and info messages related to it. This
279     * setting has no effect on messages which do not relate directly to a
280     * single InputField.
281     *
282     * @return the displayFieldLabelWithMessages
283     */
284    @BeanTagAttribute
285    public boolean isDisplayFieldLabelWithMessages() {
286        return this.displayFieldLabelWithMessages;
287    }
288
289    /**
290     * If true, the error messages will display the an InputField's title
291     * alongside the error, warning, and info messages related to it. This
292     * setting has no effect on messages which do not relate directly to a
293     * single InputField.
294     *
295     * @param displayFieldLabelWithMessages the displayFieldLabelWithMessages to set
296     */
297    public void setDisplayFieldLabelWithMessages(boolean displayFieldLabelWithMessages) {
298        this.displayFieldLabelWithMessages = displayFieldLabelWithMessages;
299    }
300
301    /**
302     * When collapseAdditionalFieldLinkMessages is set to true, the messages generated on field links will be
303     * summarized to limit the space they take up with an appendage similar to [+n message type] appended for
304     * additional
305     * messages that are omitted.  When this flag is false, all messages will be part of the link separated by
306     * a comma.
307     *
308     * @return if field link messages are being collapsed
309     */
310    @BeanTagAttribute
311    public boolean isCollapseAdditionalFieldLinkMessages() {
312        return collapseAdditionalFieldLinkMessages;
313    }
314
315    /**
316     * Set collapseAdditionalFieldLinkMessages
317     *
318     * @param collapseAdditionalFieldLinkMessages - true if field link messages are being collapsed
319     */
320    public void setCollapseAdditionalFieldLinkMessages(boolean collapseAdditionalFieldLinkMessages) {
321        this.collapseAdditionalFieldLinkMessages = collapseAdditionalFieldLinkMessages;
322    }
323
324    /**
325     * If true, the header message summary will display (this is the message count message appended to section
326     * headers).
327     *
328     * @return true if the summary will display, false otherwise
329     */
330    @BeanTagAttribute
331    public boolean isDisplayHeaderMessageSummary() {
332        return displayHeaderMessageSummary;
333    }
334
335    /**
336     * Sets whether the header message summary will display or not for this section/page.
337     */
338    public void setDisplayHeaderMessageSummary(boolean displayHeaderMessageSummary) {
339        this.displayHeaderMessageSummary = displayHeaderMessageSummary;
340    }
341}