001/**
002 * Copyright 2005-2016 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.kew.actionlist.web;
017
018import org.apache.commons.collections.ComparatorUtils;
019import org.apache.commons.lang.StringUtils;
020import org.apache.commons.lang.builder.EqualsBuilder;
021import org.apache.commons.lang.builder.HashCodeBuilder;
022import org.apache.struts.action.*;
023import org.displaytag.pagination.PaginatedList;
024import org.displaytag.properties.SortOrderEnum;
025import org.displaytag.util.LookupUtil;
026import org.kuali.rice.core.api.config.property.ConfigContext;
027import org.kuali.rice.core.api.delegation.DelegationType;
028import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
029import org.kuali.rice.core.api.exception.RiceRuntimeException;
030import org.kuali.rice.core.web.format.DateFormatter;
031import org.kuali.rice.kew.actionitem.ActionItem;
032import org.kuali.rice.kew.actionitem.ActionItemActionListExtension;
033import org.kuali.rice.kew.actionitem.OutboxItemActionListExtension;
034import org.kuali.rice.kew.actionlist.ActionListFilter;
035import org.kuali.rice.kew.actionlist.ActionToTake;
036import org.kuali.rice.kew.actionlist.PaginatedActionList;
037import org.kuali.rice.kew.actionlist.service.ActionListService;
038import org.kuali.rice.kew.actionrequest.Recipient;
039import org.kuali.rice.kew.api.KewApiConstants;
040import org.kuali.rice.kew.api.action.ActionInvocation;
041import org.kuali.rice.kew.api.action.ActionItemCustomization;
042import org.kuali.rice.kew.api.action.ActionSet;
043import org.kuali.rice.kew.api.action.ActionType;
044import org.kuali.rice.kew.api.document.DocumentStatus;
045import org.kuali.rice.kew.api.exception.WorkflowException;
046import org.kuali.rice.kew.api.extension.ExtensionDefinition;
047import org.kuali.rice.kew.api.preferences.Preferences;
048import org.kuali.rice.kew.framework.KewFrameworkServiceLocator;
049import org.kuali.rice.kew.framework.actionlist.ActionListCustomizationMediator;
050import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValueActionListExtension;
051import org.kuali.rice.kew.service.KEWServiceLocator;
052import org.kuali.rice.kew.util.PerformanceLogger;
053import org.kuali.rice.kim.api.identity.Person;
054import org.kuali.rice.kim.api.identity.principal.Principal;
055import org.kuali.rice.kim.api.identity.principal.PrincipalContract;
056import org.kuali.rice.kns.web.struts.action.KualiAction;
057import org.kuali.rice.kns.web.ui.ExtraButton;
058import org.kuali.rice.krad.UserSession;
059import org.kuali.rice.krad.exception.AuthorizationException;
060import org.kuali.rice.krad.util.GlobalVariables;
061
062import javax.servlet.http.HttpServletRequest;
063import javax.servlet.http.HttpServletResponse;
064import java.text.DateFormat;
065import java.text.ParseException;
066import java.text.SimpleDateFormat;
067import java.util.*;
068
069/**
070 * Action doing Action list stuff
071 *
072 * @author Kuali Rice Team (rice.collab@kuali.org)a
073 *
074 */
075public class ActionListAction extends KualiAction {
076
077    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ActionListAction.class);
078    protected static final String MAX_ACTION_ITEM_DATE_FORMAT = "yyyy-MM-dd hh:mm:ss.S";
079
080    private static final String ACTION_LIST_KEY = "actionList";
081    private static final String ACTION_LIST_PAGE_KEY = "actionListPage";
082    private static final String ACTION_LIST_USER_KEY = "actionList.user";
083    /*private static final String REQUERY_ACTION_LIST_KEY = "requeryActionList";*/
084    private static final String ACTION_ITEM_COUNT_FOR_USER_KEY = "actionList.count";
085    private static final String MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY = "actionList.maxActionItemDateAssigned";
086
087    private static final String ACTIONREQUESTCD_PROP = "actionRequestCd";
088    private static final String CUSTOMACTIONLIST_PROP = "customActionList";
089    private static final String ACTIONITEM_PROP = "actionitem";
090    private static final String HELPDESK_ACTIONLIST_USERNAME = "helpDeskActionListUserName";
091
092    private static final String ACTIONITEM_ACTIONREQUESTCD_INVALID_ERRKEY = "actionitem.actionrequestcd.invalid";
093    private static final String ACTIONLIST_BAD_CUSTOM_ACTION_LIST_ITEMS_ERRKEY = "actionlist.badCustomActionListItems";
094    private static final String ACTIONLIST_BAD_ACTION_ITEMS_ERRKEY = "actionlist.badActionItems";
095    private static final String HELPDESK_LOGIN_EMPTY_ERRKEY = "helpdesk.login.empty";
096    private static final String HELPDESK_LOGIN_INVALID_ERRKEY = "helpdesk.login.invalid";
097
098    private static final ActionType [] actionListActionTypes =
099            { ActionType.APPROVE, ActionType.DISAPPROVE, ActionType.CANCEL, ActionType.ACKNOWLEDGE, ActionType.FYI };
100
101    @Override
102    public ActionForward execute(ActionMapping mapping, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception {
103        ActionListForm frm = (ActionListForm)actionForm;
104        request.setAttribute("Constants", getServlet().getServletContext().getAttribute("KewApiConstants"));
105        request.setAttribute("preferences", getUserSession().retrieveObject(KewApiConstants.PREFERENCES));
106        frm.setHeaderButtons(getHeaderButtons());
107        return super.execute(mapping, actionForm, request, response);
108    }
109
110    private List<ExtraButton> getHeaderButtons(){
111        List<ExtraButton> headerButtons = new ArrayList<ExtraButton>();
112        ExtraButton eb = new ExtraButton();
113        String krBaseUrl = ConfigContext.getCurrentContextConfig().getKRBaseURL();
114        eb.setExtraButtonSource( krBaseUrl + "/images/tinybutton-preferences.gif");
115        eb.setExtraButtonOnclick("Preferences.do?returnMapping=viewActionList");
116
117        headerButtons.add(eb);
118        eb = new ExtraButton();
119        eb.setExtraButtonSource(krBaseUrl + "/images/tinybutton-refresh.gif");
120        eb.setExtraButtonProperty("methodToCall.refresh");
121
122        headerButtons.add(eb);
123        eb = new ExtraButton();
124        eb.setExtraButtonSource(krBaseUrl + "/images/tinybutton-filter.gif");
125        eb.setExtraButtonOnclick("javascript: window.open('ActionListFilter.do?methodToCall=start');");
126        headerButtons.add(eb);
127
128
129        return headerButtons;
130    }
131
132    @Override
133    public ActionForward refresh(ActionMapping mapping,
134                                 ActionForm form,
135                                         HttpServletRequest request,
136                                         HttpServletResponse response) throws Exception {
137        request.getSession().setAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY, "true");
138        return start(mapping, form, request, response);
139    }
140
141    @Override
142    protected ActionForward defaultDispatch(ActionMapping mapping,
143                                            ActionForm form, HttpServletRequest request,
144                                            HttpServletResponse response) throws Exception {
145        return start(mapping, form, request, response);
146    }
147
148    public ActionForward start(ActionMapping mapping, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception {
149        PerformanceLogger plog = new PerformanceLogger();
150        plog.log("Starting ActionList fetch");
151        ActionListForm form = (ActionListForm) actionForm;
152        ActionListService actionListSrv = KEWServiceLocator.getActionListService();
153
154
155        // process display tag parameters
156        Integer page = form.getPage();
157        String sortCriterion = form.getSort();
158        SortOrderEnum sortOrder = SortOrderEnum.ASCENDING;
159        final UserSession uSession = getUserSession();
160
161        if (form.getDir() != null) {
162            sortOrder = parseSortOrder(form.getDir());
163        }
164        else if ( !StringUtils.isEmpty((String) uSession.retrieveObject(KewApiConstants.SORT_ORDER_ATTR_NAME)))     {
165            sortOrder = parseSortOrder((String) uSession.retrieveObject(KewApiConstants.SORT_ORDER_ATTR_NAME));
166        }
167        // if both the page and the sort criteria are null, that means its the first entry into the page, use defaults
168        if (page == null && sortCriterion == null) {
169            page = 1;
170            sortCriterion = ActionItemComparator.ACTION_LIST_DEFAULT_SORT;
171        }
172        else if ( !StringUtils.isEmpty((String) uSession.retrieveObject(KewApiConstants.SORT_CRITERIA_ATTR_NAME)))     {
173            sortCriterion = (String) uSession.retrieveObject(KewApiConstants.SORT_CRITERIA_ATTR_NAME);
174        }
175        // if the page is still null, that means the user just performed a sort action, pull the currentPage off of the form
176        if (page == null) {
177            page = form.getCurrentPage();
178        }
179
180        // update the values of the "current" display tag parameters
181        form.setCurrentPage(page);
182        if (!StringUtils.isEmpty(sortCriterion)) {
183            form.setCurrentSort(sortCriterion);
184            form.setCurrentDir(getSortOrderValue(sortOrder));
185        }
186
187        // reset the default action on the form
188        form.setDefaultActionToTake("NONE");
189
190        boolean freshActionList = true;
191        // retrieve cached action list
192        List<? extends ActionItemActionListExtension> actionList = (List<? extends ActionItemActionListExtension>)request.getSession().getAttribute(ACTION_LIST_KEY);
193        plog.log("Time to initialize");
194        try {
195            //UserSession uSession = getUserSession(request);
196            String principalId = null;
197            if (uSession.retrieveObject(KewApiConstants.ACTION_LIST_FILTER_ATTR_NAME) == null) {
198                ActionListFilter filter = new ActionListFilter();
199                filter.setDelegationType(DelegationType.SECONDARY.getCode());
200                filter.setExcludeDelegationType(true);
201                uSession.addObject(KewApiConstants.ACTION_LIST_FILTER_ATTR_NAME, filter);
202            }
203
204            final ActionListFilter filter = (ActionListFilter) uSession.retrieveObject(KewApiConstants.ACTION_LIST_FILTER_ATTR_NAME);
205            /* 'forceListRefresh' variable used to signify that the action list filter has changed
206             * any time the filter changes the action list must be refreshed or filter may not take effect on existing
207             * list items... only exception is if action list has not loaded previous and fetching of the list has not
208             * occurred yet
209             */
210            boolean forceListRefresh = request.getSession().getAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY) != null;
211            if (uSession.retrieveObject(KewApiConstants.HELP_DESK_ACTION_LIST_PRINCIPAL_ATTR_NAME) != null) {
212                principalId = ((PrincipalContract) uSession.retrieveObject(KewApiConstants.HELP_DESK_ACTION_LIST_PRINCIPAL_ATTR_NAME)).getPrincipalId();
213            } else {
214                if (!StringUtils.isEmpty(form.getDocType())) {
215                    filter.setDocumentType(form.getDocType());
216                    filter.setExcludeDocumentType(false);
217                    forceListRefresh = true;
218                }
219                principalId = uSession.getPerson().getPrincipalId();
220            }
221
222            final Preferences preferences = (Preferences) getUserSession().retrieveObject(KewApiConstants.PREFERENCES);
223
224            if (!StringUtils.isEmpty(form.getDelegationId())) {
225                if (!KewApiConstants.DELEGATION_DEFAULT.equals(form.getDelegationId())) {
226                    // If the user can filter by both primary and secondary delegation, and both drop-downs have non-default values assigned,
227                    // then reset the primary delegation drop-down's value when the primary delegation drop-down's value has remained unaltered
228                    // but the secondary drop-down's value has been altered; but if one of these alteration situations does not apply, reset the
229                    // secondary delegation drop-down.
230                    if (StringUtils.isNotBlank(form.getPrimaryDelegateId()) && !KewApiConstants.PRIMARY_DELEGATION_DEFAULT.equals(form.getPrimaryDelegateId())){
231                        if (form.getPrimaryDelegateId().equals(request.getParameter("oldPrimaryDelegateId")) &&
232                                !form.getDelegationId().equals(request.getParameter("oldDelegationId"))) {
233                            form.setPrimaryDelegateId(KewApiConstants.PRIMARY_DELEGATION_DEFAULT);
234                        } else {
235                            form.setDelegationId(KewApiConstants.DELEGATION_DEFAULT);
236                        }
237                    } else if (StringUtils.isNotBlank(filter.getPrimaryDelegateId()) &&
238                            !KewApiConstants.PRIMARY_DELEGATION_DEFAULT.equals(filter.getPrimaryDelegateId())) {
239                        // If the primary delegation drop-down is invisible but a primary delegation filter is in place, and if the secondary delegation
240                        // drop-down has a non-default value selected, then reset the primary delegation filtering.
241                        filter.setPrimaryDelegateId(KewApiConstants.PRIMARY_DELEGATION_DEFAULT);
242                    }
243                }
244                // Enable the secondary delegation filtering.
245                filter.setDelegatorId(form.getDelegationId());
246                filter.setExcludeDelegatorId(false);
247                actionList = null;
248            }
249
250            if (!StringUtils.isEmpty(form.getPrimaryDelegateId())) {
251                // If the secondary delegation drop-down is invisible but a secondary delegation filter is in place, and if the primary delegation
252                // drop-down has a non-default value selected, then reset the secondary delegation filtering.
253                if (StringUtils.isBlank(form.getDelegationId()) && !KewApiConstants.PRIMARY_DELEGATION_DEFAULT.equals(form.getPrimaryDelegateId()) &&
254                        StringUtils.isNotBlank(filter.getDelegatorId()) &&
255                        !KewApiConstants.DELEGATION_DEFAULT.equals(filter.getDelegatorId())) {
256                    filter.setDelegatorId(KewApiConstants.DELEGATION_DEFAULT);
257                }
258                // Enable the primary delegation filtering.
259                filter.setPrimaryDelegateId(form.getPrimaryDelegateId());
260                filter.setExcludeDelegatorId(false);
261                actionList = null;
262            }
263
264            // if the user has changed, we need to refresh the action list
265            if (!principalId.equals(request.getSession().getAttribute(ACTION_LIST_USER_KEY))) {
266                actionList = null;
267            }
268
269            if (isOutboxMode(form, request, preferences)) {
270                actionList = new ArrayList<OutboxItemActionListExtension>(actionListSrv.getOutbox(principalId, filter));
271                form.setOutBoxEmpty(actionList.isEmpty());
272            } else {
273
274                    SimpleDateFormat dFormatter = new SimpleDateFormat(MAX_ACTION_ITEM_DATE_FORMAT);
275                    if (actionList == null) {
276                        List<Object> countAndMaxDate = actionListSrv.getMaxActionItemDateAssignedAndCountForUser(principalId);
277                        if (countAndMaxDate.isEmpty() || countAndMaxDate.get(0) == null ) {
278                            if (countAndMaxDate.isEmpty()) {
279                                countAndMaxDate.add(0, new Date(0));
280                                countAndMaxDate.add(1, 0);
281                            } else {
282                                countAndMaxDate.set(0, new Date(0));
283                            }
284                        }
285                        request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, dFormatter.format(countAndMaxDate.get(0)));
286                        request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, (Integer)countAndMaxDate.get(1));
287                        // fetch the action list
288                        actionList = new ArrayList<ActionItemActionListExtension>(actionListSrv.getActionList(principalId, filter));
289
290                        request.getSession().setAttribute(ACTION_LIST_USER_KEY, principalId);
291                    } else if (forceListRefresh) {
292                        // force a refresh... usually based on filter change or parameter specifying refresh needed
293                        actionList = new ArrayList<ActionItemActionListExtension>(actionListSrv.getActionList(principalId, filter));
294                        request.getSession().setAttribute(ACTION_LIST_USER_KEY, principalId);
295                        List<Object> countAndMaxDate = actionListSrv.getMaxActionItemDateAssignedAndCountForUser(principalId);
296                        if (countAndMaxDate.isEmpty() || countAndMaxDate.get(0) == null ) {
297                            if (countAndMaxDate.isEmpty()) {
298                                countAndMaxDate.add(0, new Date(0));
299                                countAndMaxDate.add(1, 0);
300                            } else {
301                                countAndMaxDate.set(0, new Date(0));
302                            }
303                        }
304                        request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, dFormatter.format(countAndMaxDate.get(0)));
305                        request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, (Integer)countAndMaxDate.get(1));
306
307                    }else if (refreshList(request,principalId)){
308                        actionList = new ArrayList<ActionItemActionListExtension>(actionListSrv.getActionList(principalId, filter));
309                        request.getSession().setAttribute(ACTION_LIST_USER_KEY, principalId);
310
311                    } else {
312                        Boolean update = (Boolean) uSession.retrieveObject(KewApiConstants.UPDATE_ACTION_LIST_ATTR_NAME);
313                        if (update == null || !update) {
314                            freshActionList = false;
315                        }
316                    }
317                    request.getSession().setAttribute(ACTION_LIST_KEY, actionList);
318
319            }
320            // reset the requery action list key
321            request.getSession().setAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY, null);
322
323            // build the drop-down of delegators
324            if (KewApiConstants.DELEGATORS_ON_ACTION_LIST_PAGE.equalsIgnoreCase(preferences.getDelegatorFilter())) {
325                Collection<Recipient> delegators = actionListSrv.findUserSecondaryDelegators(principalId);
326                form.setDelegators(ActionListUtil.getWebFriendlyRecipients(delegators));
327                form.setDelegationId(filter.getDelegatorId());
328            }
329
330            // Build the drop-down of primary delegates.
331            if (KewApiConstants.PRIMARY_DELEGATES_ON_ACTION_LIST_PAGE.equalsIgnoreCase(preferences.getPrimaryDelegateFilter())) {
332                Collection<Recipient> pDelegates = actionListSrv.findUserPrimaryDelegations(principalId);
333                form.setPrimaryDelegates(ActionListUtil.getWebFriendlyRecipients(pDelegates));
334                form.setPrimaryDelegateId(filter.getPrimaryDelegateId());
335            }
336
337            form.setFilterLegend(filter.getFilterLegend());
338            plog.log("Setting attributes");
339
340            int pageSize = getPageSize(preferences);
341            // initialize the action list if necessary
342            if (freshActionList) {
343                plog.log("calling initializeActionList");
344                initializeActionList(actionList, preferences);
345                plog.log("done w/ initializeActionList");
346                // put this in to resolve EN-112 (http://beatles.uits.indiana.edu:8081/jira/browse/EN-112)
347                // if the action list gets "refreshed" in between page switches, we need to be sure and re-sort it, even though we don't have sort criteria on the request
348                if (sortCriterion == null) {
349                    sortCriterion = form.getCurrentSort();
350                    sortOrder = parseSortOrder(form.getCurrentDir());
351                }
352            }
353            // sort the action list if necessary
354            if (sortCriterion != null) {
355                sortActionList(actionList, sortCriterion, sortOrder);
356            }
357
358            plog.log("calling buildCurrentPage");
359            PaginatedList currentPage = buildCurrentPage(actionList, form.getCurrentPage(), form.getCurrentSort(),
360                    form.getCurrentDir(), pageSize, preferences, form);
361            plog.log("done w/ buildCurrentPage");
362            request.setAttribute(ACTION_LIST_PAGE_KEY, currentPage);
363            uSession.addObject(KewApiConstants.UPDATE_ACTION_LIST_ATTR_NAME, Boolean.FALSE);
364            uSession.addObject(KewApiConstants.CURRENT_PAGE_ATTR_NAME, form.getCurrentPage());
365            uSession.addObject(KewApiConstants.SORT_CRITERIA_ATTR_NAME, form.getSort());
366            uSession.addObject(KewApiConstants.SORT_ORDER_ATTR_NAME, form.getCurrentDir());
367            plog.log("finished setting attributes, finishing action list fetch");
368        } catch (Exception e) {
369            LOG.error("Error loading action list.", e);
370        }
371
372        LOG.debug("end start ActionListAction");
373        return mapping.findForward("viewActionList");
374    }
375
376    private boolean refreshList(HttpServletRequest request,String principalId ){
377        List<Object> maxActionItemDateAssignedAndCount = KEWServiceLocator.getActionListService().getMaxActionItemDateAssignedAndCountForUser(
378                principalId);
379        int count = (Integer) maxActionItemDateAssignedAndCount.get(1);
380        int previousCount = 0;
381        Object actionItemCountFromSession = request.getSession().getAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY);
382        if ( actionItemCountFromSession != null ) {
383            previousCount = Integer.parseInt(actionItemCountFromSession.toString());
384        }
385        SimpleDateFormat dFormatter = new SimpleDateFormat(MAX_ACTION_ITEM_DATE_FORMAT);
386        Date maxActionItemDateAssigned = (Date) maxActionItemDateAssignedAndCount.get(0);
387        if ( maxActionItemDateAssigned == null ) {
388            maxActionItemDateAssigned = new Date(0);
389        }
390        Date previousMaxActionItemDateAssigned= null;
391        try{
392            Object dateAttributeFromSession = request.getSession().getAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY);
393            if ( dateAttributeFromSession != null ) {
394                previousMaxActionItemDateAssigned = dFormatter.parse(dateAttributeFromSession.toString());
395            }
396        } catch (ParseException e){
397            LOG.warn( MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY + " in session did not have expected date format.  "
398                    + "Was: " + request.getSession().getAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY), e );
399        }
400        if(previousCount!= count){
401            request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, dFormatter.format(
402                    maxActionItemDateAssigned));
403            request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, count);
404            return true;
405        }else if(previousMaxActionItemDateAssigned == null || previousMaxActionItemDateAssigned.compareTo(maxActionItemDateAssigned)!=0){
406            request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, dFormatter.format(
407                    maxActionItemDateAssigned));
408            request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, count);
409            return true;
410        } else{
411            return false;
412        }
413
414    }
415
416    private SortOrderEnum parseSortOrder(String dir) throws WorkflowException {
417        if ("asc".equals(dir)) {
418            return SortOrderEnum.ASCENDING;
419        } else if ("desc".equals(dir)) {
420            return SortOrderEnum.DESCENDING;
421        }
422        throw new WorkflowException("Invalid sort direction: " + dir);
423    }
424
425    private String getSortOrderValue(SortOrderEnum sortOrder) {
426        if (SortOrderEnum.ASCENDING.equals(sortOrder)) {
427            return "asc";
428        } else if (SortOrderEnum.DESCENDING.equals(sortOrder)) {
429            return "desc";
430        }
431        return null;
432    }
433
434    private static final String OUT_BOX_MODE = "_OUT_BOX_MODE";
435
436    /**
437     * this method is setting 2 props on the {@link ActionListForm} that controls outbox behavior.
438     *  alForm.setViewOutbox("false"); -> this is set by user preferences and the actionlist.outbox.off config prop
439     *  alForm.setShowOutbox(false); -> this is set by user action clicking the ActionList vs. Outbox links.
440     *
441     * @param alForm
442     * @param request
443     * @return boolean indication whether the outbox should be fetched
444     */
445    private boolean isOutboxMode(ActionListForm alForm, HttpServletRequest request, Preferences preferences) {
446
447        boolean outBoxView = false;
448
449        if (! preferences.isUsingOutbox() || ! ConfigContext.getCurrentContextConfig().getOutBoxOn()) {
450            request.getSession().setAttribute(OUT_BOX_MODE, Boolean.FALSE);
451            alForm.setViewOutbox("false");
452            alForm.setShowOutbox(false);
453            return false;
454        }
455
456        alForm.setShowOutbox(true);
457        if (StringUtils.isNotEmpty(alForm.getViewOutbox())) {
458            if (!Boolean.valueOf(alForm.getViewOutbox())) {
459                request.getSession().setAttribute(OUT_BOX_MODE, Boolean.FALSE);
460                outBoxView = false;
461            } else {
462                request.getSession().setAttribute(OUT_BOX_MODE, Boolean.TRUE);
463                outBoxView = true;
464            }
465        } else {
466
467            if (request.getSession().getAttribute(OUT_BOX_MODE) == null) {
468                outBoxView = false;
469            } else {
470                outBoxView = (Boolean) request.getSession().getAttribute(OUT_BOX_MODE);
471            }
472        }
473        if (outBoxView) {
474            alForm.setViewOutbox("true");
475        } else {
476            alForm.setViewOutbox("false");
477        }
478        return outBoxView;
479    }
480
481    private void sortActionList(List<? extends ActionItemActionListExtension> actionList, String sortName, SortOrderEnum sortOrder) {
482        if (StringUtils.isEmpty(sortName)) {
483            return;
484        }
485        Comparator<ActionItemActionListExtension> comparator = new ActionItemComparator(sortName);
486        if (SortOrderEnum.DESCENDING.equals(sortOrder)) {
487            comparator = ComparatorUtils.reversedComparator(comparator);
488        }
489        Collections.sort(actionList, comparator);
490        // re-index the action items
491        int index = 0;
492        for (ActionItemActionListExtension actionItem : actionList) {
493            actionItem.setActionListIndex(index++);
494        }
495    }
496
497    private void initializeActionList(List<? extends ActionItemActionListExtension> actionList, Preferences preferences) {
498        List<String> actionItemProblemIds = new ArrayList<String>();
499        int index = 0;
500        generateActionItemErrors(actionList);
501        for (Iterator<? extends ActionItemActionListExtension> iterator = actionList.iterator(); iterator.hasNext();) {
502            ActionItemActionListExtension actionItem = iterator.next();
503            if (actionItem.getDocumentId() == null) {
504                LOG.error("Somehow there exists an ActionItem with a null document id!  actionItemId=" + actionItem.getId());
505                iterator.remove();
506                continue;
507            }
508            try {
509                actionItem.initialize(preferences);
510                actionItem.setActionListIndex(index);
511                index++;
512            } catch (Exception e) {
513                // if there's a problem loading the action item, we don't want to blow out the whole screen but we will remove it from the list
514                // and display an appropriate error message to the user
515                LOG.error("Error loading action list for action item " + actionItem.getId(), e);
516                iterator.remove();
517                actionItemProblemIds.add(actionItem.getDocumentId());
518            }
519        }
520        generateActionItemErrors(ACTIONITEM_PROP, ACTIONLIST_BAD_ACTION_ITEMS_ERRKEY, actionItemProblemIds);
521    }
522
523    /**
524     * Gets the page size of the Action List.  Uses the user's preferences for page size unless the action list
525     * has been throttled by an application constant, in which case it uses the smaller of the two values.
526     */
527    protected int getPageSize(Preferences preferences) {
528        return Integer.parseInt(preferences.getPageSize());
529    }
530
531    protected PaginatedList buildCurrentPage(List<? extends ActionItemActionListExtension> actionList, Integer page, String sortCriterion, String sortDirection,
532                                             int pageSize, Preferences preferences, ActionListForm form) throws WorkflowException {
533        List<ActionItemActionListExtension> currentPage = new ArrayList<ActionItemActionListExtension>(pageSize);
534
535        boolean haveCustomActions = false;
536        boolean haveDisplayParameters = false;
537
538        final boolean showClearFyi = KewApiConstants.PREFERENCES_YES_VAL.equalsIgnoreCase(preferences.getShowClearFyi());
539
540        // collects all the actions for items on this page
541        Set<ActionType> pageActions = new HashSet<ActionType>();
542
543        List<String> customActionListProblemIds = new ArrayList<String>();
544        SortOrderEnum sortOrder = parseSortOrder(sortDirection);
545        int startIndex = (page - 1) * pageSize;
546        int endIndex = startIndex + pageSize;
547        generateActionItemErrors(actionList);
548
549        LOG.info("Beginning processing of Action List Customizations (total: " + actionList.size() + " Action Items)");
550        long start = System.currentTimeMillis();
551
552        Map<String, ActionItemCustomization>  customizationMap =
553                getActionListCustomizationMediator().getActionListCustomizations(
554                        getUserSession().getPrincipalId(), convertToApiActionItems(actionList)
555                );
556
557        long end = System.currentTimeMillis();
558        LOG.info("Finished processing of Action List Customizations (total time: " + (end - start) + " ms)");
559
560        for (int index = startIndex; index < endIndex && index < actionList.size(); index++) {
561            ActionItemActionListExtension actionItem = actionList.get(index);
562            // evaluate custom action list component for mass actions
563            try {
564                ActionItemCustomization customization = customizationMap.get(actionItem.getId());
565                if (customization != null) {
566                    ActionSet actionSet = customization.getActionSet();
567
568                    // If only it were this easy: actionItem.setCustomActions(customization.getActionSet());
569
570                    Map<String, String> customActions = new LinkedHashMap<String, String>();
571                    customActions.put("NONE", "NONE");
572
573                    for (ActionType actionType : actionListActionTypes) {
574                        if (actionSet.hasAction(actionType.getCode()) &&
575                                isActionCompatibleRequest(actionItem, actionType.getCode())) {
576
577                            final boolean isFyi = ActionType.FYI == actionType; // make the conditional easier to read
578
579                            if (!isFyi || (isFyi && showClearFyi)) { // deal with special FYI preference
580                                customActions.put(actionType.getCode(), actionType.getLabel());
581                                pageActions.add(actionType);
582                            }
583                        }
584                    }
585
586                    if (customActions.size() > 1) {
587                        actionItem.setCustomActions(customActions);
588                        haveCustomActions = true;
589                    }
590
591                    actionItem.setDisplayParameters(customization.getDisplayParameters());
592
593                    haveDisplayParameters = haveDisplayParameters || (actionItem.getDisplayParameters() != null);
594                }
595
596            } catch (Exception e) {
597                // if there's a problem loading the custom action list attribute, let's go ahead and display the vanilla action item
598                LOG.error("Problem loading custom action list attribute", e);
599                customActionListProblemIds.add(actionItem.getDocumentId());
600            }
601            currentPage.add(actionItem);
602        }
603
604        // configure custom actions on form
605        form.setHasCustomActions(haveCustomActions);
606
607        Map<String, String> defaultActions = new LinkedHashMap<String, String>();
608        defaultActions.put("NONE", "NONE");
609
610        for (ActionType actionType : actionListActionTypes) {
611            if (pageActions.contains(actionType)) {
612
613                final boolean isFyi = ActionType.FYI == actionType;
614                // special logic for FYIs:
615                if (isFyi) {
616                    // clearing FYIs can be done in any action list not just a customized one
617                    if(showClearFyi) {
618                        defaultActions.put(actionType.getCode(), actionType.getLabel());
619                    }
620                } else { // all the other actions
621                    defaultActions.put(actionType.getCode(), actionType.getLabel());
622                    form.setCustomActionList(Boolean.TRUE);
623                }
624            }
625        }
626
627        if (defaultActions.size() > 1) {
628            form.setDefaultActions(defaultActions);
629        }
630
631        form.setHasDisplayParameters(haveDisplayParameters);
632
633        generateActionItemErrors(CUSTOMACTIONLIST_PROP, ACTIONLIST_BAD_CUSTOM_ACTION_LIST_ITEMS_ERRKEY, customActionListProblemIds);
634        return new PaginatedActionList(currentPage, actionList.size(), page, pageSize, "actionList", sortCriterion, sortOrder);
635    }
636
637    // convert a List of org.kuali.rice.kew.actionitem.ActionItemS to org.kuali.rice.kew.api.action.ActionItemS
638    private List<org.kuali.rice.kew.api.action.ActionItem> convertToApiActionItems(List<? extends ActionItemActionListExtension> actionList) {
639        List<org.kuali.rice.kew.api.action.ActionItem> apiActionItems = new ArrayList<org.kuali.rice.kew.api.action.ActionItem>(actionList.size());
640
641        for (ActionItemActionListExtension actionItemObj : actionList) {
642            apiActionItems.add(
643                    org.kuali.rice.kew.api.action.ActionItem.Builder.create(actionItemObj).build());
644        }
645        return apiActionItems;
646    }
647
648    private void generateActionItemErrors(String propertyName, String errorKey, List<String> documentIds) {
649        if (!documentIds.isEmpty()) {
650            String documentIdsString = StringUtils.join(documentIds.iterator(), ", ");
651            GlobalVariables.getMessageMap().putError(propertyName, errorKey, documentIdsString);
652        }
653    }
654
655    private void generateActionItemErrors(List<? extends ActionItemActionListExtension> actionList) {
656        for (ActionItemActionListExtension actionItem : actionList) {
657            if(!KewApiConstants.ACTION_REQUEST_CODES.containsKey(actionItem.getActionRequestCd())) {
658                GlobalVariables.getMessageMap().putError(ACTIONREQUESTCD_PROP,ACTIONITEM_ACTIONREQUESTCD_INVALID_ERRKEY,actionItem.getId()+"");
659            }
660        }
661    }
662
663
664    public ActionForward takeMassActions(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
665        ActionListForm actionListForm = (ActionListForm) form;
666        List<? extends ActionItemActionListExtension> actionList = (List<? extends ActionItemActionListExtension>) request.getSession().getAttribute(ACTION_LIST_KEY);
667        if (actionList == null) {
668            return start(mapping, new ActionListForm(), request, response);
669        }
670        ActionMessages messages = new ActionMessages();
671        List<ActionInvocation> invocations = new ArrayList<ActionInvocation>();
672        int index = 0;
673        for (Object element : actionListForm.getActionsToTake()) {
674            ActionToTake actionToTake = (ActionToTake) element;
675            if (actionToTake != null && actionToTake.getActionTakenCd() != null &&
676                    !"".equals(actionToTake.getActionTakenCd()) &&
677                    !"NONE".equalsIgnoreCase(actionToTake.getActionTakenCd()) &&
678                    actionToTake.getActionItemId() != null) {
679                ActionItemActionListExtension actionItem = getActionItemFromActionList(actionList, actionToTake.getActionItemId());
680                if (actionItem == null) {
681                    LOG.warn("Could not locate the ActionItem to take mass action against in the action list: " + actionToTake.getActionItemId());
682                    continue;
683                }
684                invocations.add(ActionInvocation.create(ActionType.fromCode(actionToTake.getActionTakenCd()), actionItem.getId()));
685            }
686            index++;
687        }
688        KEWServiceLocator.getWorkflowDocumentService().takeMassActions(getUserSession().getPrincipalId(), invocations);
689        messages.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("general.routing.processed"));
690        saveMessages(request, messages);
691        ActionListForm cleanForm = new ActionListForm();
692        request.setAttribute(mapping.getName(), cleanForm);
693        request.getSession().setAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY, "true");
694        return start(mapping, cleanForm, request, response);
695    }
696
697    protected ActionItemActionListExtension getActionItemFromActionList(List<? extends ActionItemActionListExtension> actionList, String actionItemId) {
698        for (ActionItemActionListExtension actionItem : actionList) {
699            if (actionItem.getId().equals(actionItemId)) {
700                return actionItem;
701            }
702        }
703        return null;
704    }
705
706    public ActionForward helpDeskActionListLogin(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
707        ActionListForm actionListForm = (ActionListForm) form;
708        String name = actionListForm.getHelpDeskActionListUserName();
709        if (!"true".equals(request.getAttribute("helpDeskActionList"))) {
710            throw new AuthorizationException(getUserSession().getPrincipalId(), "helpDeskActionListLogin", getClass().getSimpleName());
711        }
712        try
713        {
714            final Principal helpDeskActionListPrincipal = KEWServiceLocator.getIdentityHelperService().getPrincipalByPrincipalName(name);
715            final Person helpDeskActionListPerson = KEWServiceLocator.getIdentityHelperService().getPersonByPrincipalName(name);
716
717            GlobalVariables.getUserSession().addObject(KewApiConstants.HELP_DESK_ACTION_LIST_PRINCIPAL_ATTR_NAME, helpDeskActionListPrincipal);
718            GlobalVariables.getUserSession().addObject(KewApiConstants.HELP_DESK_ACTION_LIST_PERSON_ATTR_NAME, helpDeskActionListPerson);
719        }
720        catch (RiceRuntimeException rre)
721        {
722            GlobalVariables.getMessageMap().putError(HELPDESK_ACTIONLIST_USERNAME, HELPDESK_LOGIN_INVALID_ERRKEY, name);
723        }
724        catch (RiceIllegalArgumentException e) {
725            GlobalVariables.getMessageMap().putError(HELPDESK_ACTIONLIST_USERNAME, HELPDESK_LOGIN_INVALID_ERRKEY, name);
726        }
727        catch (NullPointerException npe)
728        {
729            GlobalVariables.getMessageMap().putError("null", HELPDESK_LOGIN_EMPTY_ERRKEY, name);
730        }
731        actionListForm.setDelegator(null);
732        request.getSession().setAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY, "true");
733        return start(mapping, form, request, response);
734    }
735
736    public ActionForward clearFilter(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
737        LOG.debug("clearFilter ActionListAction");
738        final org.kuali.rice.krad.UserSession commonUserSession = getUserSession();
739        commonUserSession.removeObject(KewApiConstants.ACTION_LIST_FILTER_ATTR_NAME);
740        request.getSession().setAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY, "true");
741        LOG.debug("end clearFilter ActionListAction");
742        return start(mapping, form, request, response);
743    }
744
745    public ActionForward clearHelpDeskActionListUser(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
746        LOG.debug("clearHelpDeskActionListUser ActionListAction");
747        GlobalVariables.getUserSession().removeObject(KewApiConstants.HELP_DESK_ACTION_LIST_PRINCIPAL_ATTR_NAME);
748        GlobalVariables.getUserSession().removeObject(KewApiConstants.HELP_DESK_ACTION_LIST_PERSON_ATTR_NAME);
749        LOG.debug("end clearHelpDeskActionListUser ActionListAction");
750        return start(mapping, form, request, response);
751    }
752
753    /**
754     * Generates an Action List count.
755     */
756    public ActionForward count(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
757        ActionListForm alForm = (ActionListForm)form;
758        Person user = getUserSession().getPerson();
759        alForm.setCount(KEWServiceLocator.getActionListService().getCount(user.getPrincipalId()));
760        LOG.info("Fetched Action List count of " + alForm.getCount() + " for user " + user.getPrincipalName());
761        return mapping.findForward("count");
762    }
763
764    public ActionForward removeOutboxItems(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
765        ActionListForm alForm = (ActionListForm)form;
766        if (alForm.getOutboxItems() != null) {
767            KEWServiceLocator.getActionListService().removeOutboxItems(getUserSession().getPrincipalId(), Arrays.asList(alForm.getOutboxItems()));
768        }
769
770        alForm.setViewOutbox("true");
771        return start(mapping, form, request, response);
772    }
773
774    /**
775     * Navigate to the Action List Filter page, preserving any newly-modified primary/secondary delegation filters as necessary.
776     */
777    public ActionForward viewFilter(ActionMapping mapping, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception {
778        start(mapping, actionForm, request, response);
779        return mapping.findForward("viewFilter");
780    }
781
782    /**
783     * Navigate to the user's Preferences page, preserving any newly-modified primary/secondary delegation filters as necessary.
784     */
785    public ActionForward viewPreferences(ActionMapping mapping, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception {
786        start(mapping, actionForm, request, response);
787        return mapping.findForward("viewPreferences");
788    }
789
790    private boolean isActionCompatibleRequest(ActionItemActionListExtension actionItem, String actionTakenCode) {
791        boolean actionCompatible = false;
792        String requestCd = actionItem.getActionRequestCd();
793
794        //FYI request matches FYI
795        if (KewApiConstants.ACTION_REQUEST_FYI_REQ.equals(requestCd) && KewApiConstants.ACTION_TAKEN_FYI_CD.equals(actionTakenCode)) {
796            actionCompatible = true || actionCompatible;
797        }
798
799        // ACK request matches ACK
800        if (KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ.equals(requestCd) && KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD.equals(actionTakenCode)) {
801            actionCompatible = true || actionCompatible;
802        }
803
804        // APPROVE request matches all but FYI and ACK
805        if (KewApiConstants.ACTION_REQUEST_APPROVE_REQ.equals(requestCd) && !(KewApiConstants.ACTION_TAKEN_FYI_CD.equals(actionTakenCode) || KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD.equals(actionTakenCode))) {
806            actionCompatible = true || actionCompatible;
807        }
808
809        // COMPLETE request matches all but FYI and ACK
810        if (KewApiConstants.ACTION_REQUEST_COMPLETE_REQ.equals(requestCd) && !(KewApiConstants.ACTION_TAKEN_FYI_CD.equals(actionTakenCode) || KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD.equals(actionTakenCode))) {
811            actionCompatible = true || actionCompatible;
812        }
813
814        return actionCompatible;
815    }
816
817    private UserSession getUserSession(){
818        return GlobalVariables.getUserSession();
819    }
820
821    private static class ActionItemComparator implements Comparator<ActionItemActionListExtension> {
822
823        private static final String ACTION_LIST_DEFAULT_SORT = "routeHeader.createDate";
824
825        private final String sortName;
826
827        public ActionItemComparator(String sortName) {
828            if (StringUtils.isEmpty(sortName)) {
829                sortName = ACTION_LIST_DEFAULT_SORT;
830            }
831            this.sortName = sortName;
832        }
833
834        @Override
835        public int compare(ActionItemActionListExtension actionItem1, ActionItemActionListExtension actionItem2) {
836            try {
837                // invoke the power of the lookup functionality provided by the display tag library, this LookupUtil method allows for us
838                // to evaulate nested bean properties (like workgroup.groupNameId.nameId) in a null-safe manner.  For example, in the
839                // example if workgroup evaluated to NULL then LookupUtil.getProperty would return null rather than blowing an exception
840                Object property1 = LookupUtil.getProperty(actionItem1, sortName);
841                Object property2 = LookupUtil.getProperty(actionItem2, sortName);
842                if (property1 == null && property2 == null) {
843                    return 0;
844                } else if (property1 == null) {
845                    return -1;
846                } else if (property2 == null) {
847                    return 1;
848                }
849                if (property1 instanceof Comparable) {
850                    return ((Comparable)property1).compareTo(property2);
851                }
852                return property1.toString().compareTo(property2.toString());
853            } catch (Exception e) {
854                if (e instanceof RuntimeException) {
855                    throw (RuntimeException) e;
856                }
857                throw new RuntimeException("Could not sort for the given sort name: " + sortName, e);
858            }
859        }
860    }
861
862    // Lazy initialization holder class (see Effective Java Item #71)
863    private static class ActionListCustomizationMediatorHolder {
864        static final ActionListCustomizationMediator actionListCustomizationMediator =
865                KewFrameworkServiceLocator.getActionListCustomizationMediator();
866    }
867
868    private ActionListCustomizationMediator getActionListCustomizationMediator() {
869        return ActionListCustomizationMediatorHolder.actionListCustomizationMediator;
870    }
871
872    /**
873     * Simple class which defines the key of a partition of Action Items associated with an Application ID.
874     *
875     * <p>This class allows direct field access since it is intended for internal use only.</p>
876     */
877    private static final class PartitionKey {
878        String applicationId;
879        Set<String> customActionListAttributeNames;
880
881        PartitionKey(String applicationId, Collection<ExtensionDefinition> extensionDefinitions) {
882            this.applicationId = applicationId;
883            this.customActionListAttributeNames = new HashSet<String>();
884            for (ExtensionDefinition extensionDefinition : extensionDefinitions) {
885                this.customActionListAttributeNames.add(extensionDefinition.getName());
886            }
887        }
888
889        List<String> getCustomActionListAttributeNameList() {
890            return new ArrayList<String>(customActionListAttributeNames);
891        }
892
893        @Override
894        public boolean equals(Object o) {
895            if (!(o instanceof PartitionKey)) {
896                return false;
897            }
898            PartitionKey key = (PartitionKey) o;
899            EqualsBuilder builder = new EqualsBuilder();
900            builder.append(applicationId, key.applicationId);
901            builder.append(customActionListAttributeNames, key.customActionListAttributeNames);
902            return builder.isEquals();
903        }
904
905        @Override
906        public int hashCode() {
907            HashCodeBuilder builder = new HashCodeBuilder();
908            builder.append(applicationId);
909            builder.append(customActionListAttributeNames);
910            return builder.hashCode();
911        }
912    }
913}