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            synchronized(uSession) {
364                uSession.addObject(KewApiConstants.UPDATE_ACTION_LIST_ATTR_NAME, Boolean.FALSE);
365                uSession.addObject(KewApiConstants.CURRENT_PAGE_ATTR_NAME, form.getCurrentPage());
366                uSession.addObject(KewApiConstants.SORT_CRITERIA_ATTR_NAME, form.getSort());
367                uSession.addObject(KewApiConstants.SORT_ORDER_ATTR_NAME, form.getCurrentDir());
368            }
369            plog.log("finished setting attributes, finishing action list fetch");
370        } catch (Exception e) {
371            LOG.error("Error loading action list.", e);
372        }
373
374        LOG.debug("end start ActionListAction");
375        return mapping.findForward("viewActionList");
376    }
377
378    /**
379     * Sets the maxActionItemDate and actionItemcount for user in the session
380     * @param request
381     * @param principalId
382     * @param actionListSrv
383     */
384    private void setCountAndMaxDate(HttpServletRequest request,String principalId,ActionListService actionListSrv ){
385        SimpleDateFormat dFormatter = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.S");
386        List<Object> countAndMaxDate = actionListSrv.getMaxActionItemDateAssignedAndCountForUser(principalId);
387        String maxActionItemDateAssignedForUserKey = "";
388        if(countAndMaxDate.get(0)!= null){
389           maxActionItemDateAssignedForUserKey = dFormatter.format(countAndMaxDate.get(0));
390        }
391        request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, maxActionItemDateAssignedForUserKey);
392        request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, (Integer)countAndMaxDate.get(1));
393    }
394
395    private boolean refreshList(HttpServletRequest request,String principalId ){
396        List<Object> maxActionItemDateAssignedAndCount = KEWServiceLocator.getActionListService().getMaxActionItemDateAssignedAndCountForUser(
397                principalId);
398        int count = (Integer) maxActionItemDateAssignedAndCount.get(1);
399        int previousCount = 0;
400        Object actionItemCountFromSession = request.getSession().getAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY);
401        if ( actionItemCountFromSession != null ) {
402            previousCount = Integer.parseInt(actionItemCountFromSession.toString());
403        }
404        SimpleDateFormat dFormatter = new SimpleDateFormat(MAX_ACTION_ITEM_DATE_FORMAT);
405        Date maxActionItemDateAssigned = (Date) maxActionItemDateAssignedAndCount.get(0);
406        if ( maxActionItemDateAssigned == null ) {
407            maxActionItemDateAssigned = new Date(0);
408        }
409        Date previousMaxActionItemDateAssigned= null;
410        try{
411            Object dateAttributeFromSession = request.getSession().getAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY);
412            if ( dateAttributeFromSession != null ) {
413                previousMaxActionItemDateAssigned = dFormatter.parse(dateAttributeFromSession.toString());
414            }
415        } catch (ParseException e){
416            LOG.warn( MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY + " in session did not have expected date format.  "
417                    + "Was: " + request.getSession().getAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY), e );
418        }
419        if(previousCount!= count){
420            request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, dFormatter.format(
421                    maxActionItemDateAssigned));
422            request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, count);
423            return true;
424        }else if(previousMaxActionItemDateAssigned == null || previousMaxActionItemDateAssigned.compareTo(maxActionItemDateAssigned)!=0){
425            request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, dFormatter.format(
426                    maxActionItemDateAssigned));
427            request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, count);
428            return true;
429        } else{
430            return false;
431        }
432
433    }
434
435    private SortOrderEnum parseSortOrder(String dir) throws WorkflowException {
436        if ("asc".equals(dir)) {
437            return SortOrderEnum.ASCENDING;
438        } else if ("desc".equals(dir)) {
439            return SortOrderEnum.DESCENDING;
440        }
441        throw new WorkflowException("Invalid sort direction: " + dir);
442    }
443
444    private String getSortOrderValue(SortOrderEnum sortOrder) {
445        if (SortOrderEnum.ASCENDING.equals(sortOrder)) {
446            return "asc";
447        } else if (SortOrderEnum.DESCENDING.equals(sortOrder)) {
448            return "desc";
449        }
450        return null;
451    }
452
453    private static final String OUT_BOX_MODE = "_OUT_BOX_MODE";
454
455    /**
456     * this method is setting 2 props on the {@link ActionListForm} that controls outbox behavior.
457     *  alForm.setViewOutbox("false"); -> this is set by user preferences and the actionlist.outbox.off config prop
458     *  alForm.setShowOutbox(false); -> this is set by user action clicking the ActionList vs. Outbox links.
459     *
460     * @param alForm
461     * @param request
462     * @return boolean indication whether the outbox should be fetched
463     */
464    private boolean isOutboxMode(ActionListForm alForm, HttpServletRequest request, Preferences preferences) {
465
466        boolean outBoxView = false;
467
468        if (! preferences.isUsingOutbox() || ! ConfigContext.getCurrentContextConfig().getOutBoxOn()) {
469            request.getSession().setAttribute(OUT_BOX_MODE, Boolean.FALSE);
470            alForm.setViewOutbox("false");
471            alForm.setShowOutbox(false);
472            return false;
473        }
474
475        alForm.setShowOutbox(true);
476        if (StringUtils.isNotEmpty(alForm.getViewOutbox())) {
477            if (!Boolean.valueOf(alForm.getViewOutbox())) {
478                request.getSession().setAttribute(OUT_BOX_MODE, Boolean.FALSE);
479                outBoxView = false;
480            } else {
481                request.getSession().setAttribute(OUT_BOX_MODE, Boolean.TRUE);
482                outBoxView = true;
483            }
484        } else {
485
486            if (request.getSession().getAttribute(OUT_BOX_MODE) == null) {
487                outBoxView = false;
488            } else {
489                outBoxView = (Boolean) request.getSession().getAttribute(OUT_BOX_MODE);
490            }
491        }
492        if (outBoxView) {
493            alForm.setViewOutbox("true");
494        } else {
495            alForm.setViewOutbox("false");
496        }
497        return outBoxView;
498    }
499
500    private void sortActionList(List<? extends ActionItemActionListExtension> actionList, String sortName, SortOrderEnum sortOrder) {
501        if (StringUtils.isEmpty(sortName)) {
502            return;
503        }
504        Comparator<ActionItemActionListExtension> comparator = new ActionItemComparator(sortName);
505        if (SortOrderEnum.DESCENDING.equals(sortOrder)) {
506            comparator = ComparatorUtils.reversedComparator(comparator);
507        }
508        Collections.sort(actionList, comparator);
509        // re-index the action items
510        int index = 0;
511        for (ActionItemActionListExtension actionItem : actionList) {
512            actionItem.setActionListIndex(index++);
513        }
514    }
515
516    private void initializeActionList(List<? extends ActionItemActionListExtension> actionList, Preferences preferences) {
517        List<String> actionItemProblemIds = new ArrayList<String>();
518        int index = 0;
519        generateActionItemErrors(actionList);
520        for (Iterator<? extends ActionItemActionListExtension> iterator = actionList.iterator(); iterator.hasNext();) {
521            ActionItemActionListExtension actionItem = iterator.next();
522            if (actionItem.getDocumentId() == null) {
523                LOG.error("Somehow there exists an ActionItem with a null document id!  actionItemId=" + actionItem.getId());
524                iterator.remove();
525                continue;
526            }
527            try {
528                actionItem.initialize(preferences);
529                actionItem.setActionListIndex(index);
530                index++;
531            } catch (Exception e) {
532                // 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
533                // and display an appropriate error message to the user
534                LOG.error("Error loading action list for action item " + actionItem.getId(), e);
535                iterator.remove();
536                actionItemProblemIds.add(actionItem.getDocumentId());
537            }
538        }
539        generateActionItemErrors(ACTIONITEM_PROP, ACTIONLIST_BAD_ACTION_ITEMS_ERRKEY, actionItemProblemIds);
540    }
541
542    /**
543     * Gets the page size of the Action List.  Uses the user's preferences for page size unless the action list
544     * has been throttled by an application constant, in which case it uses the smaller of the two values.
545     */
546    protected int getPageSize(Preferences preferences) {
547        return Integer.parseInt(preferences.getPageSize());
548    }
549
550    protected PaginatedList buildCurrentPage(List<? extends ActionItemActionListExtension> actionList, Integer page, String sortCriterion, String sortDirection,
551                                             int pageSize, Preferences preferences, ActionListForm form) throws WorkflowException {
552        List<ActionItemActionListExtension> currentPage = new ArrayList<ActionItemActionListExtension>(pageSize);
553
554        boolean haveCustomActions = false;
555        boolean haveDisplayParameters = false;
556
557        final boolean showClearFyi = KewApiConstants.PREFERENCES_YES_VAL.equalsIgnoreCase(preferences.getShowClearFyi());
558
559        // collects all the actions for items on this page
560        Set<ActionType> pageActions = new HashSet<ActionType>();
561
562        List<String> customActionListProblemIds = new ArrayList<String>();
563        SortOrderEnum sortOrder = parseSortOrder(sortDirection);
564        int startIndex = (page - 1) * pageSize;
565        int endIndex = startIndex + pageSize;
566        generateActionItemErrors(actionList);
567
568        LOG.info("Beginning processing of Action List Customizations (total: " + actionList.size() + " Action Items)");
569        long start = System.currentTimeMillis();
570
571        Map<String, ActionItemCustomization>  customizationMap =
572                getActionListCustomizationMediator().getActionListCustomizations(
573                        getUserSession().getPrincipalId(), convertToApiActionItems(actionList)
574                );
575
576        long end = System.currentTimeMillis();
577        LOG.info("Finished processing of Action List Customizations (total time: " + (end - start) + " ms)");
578
579        for (int index = startIndex; index < endIndex && index < actionList.size(); index++) {
580            ActionItemActionListExtension actionItem = actionList.get(index);
581            // evaluate custom action list component for mass actions
582            try {
583                ActionItemCustomization customization = customizationMap.get(actionItem.getId());
584                if (customization != null) {
585                    ActionSet actionSet = customization.getActionSet();
586
587                    // If only it were this easy: actionItem.setCustomActions(customization.getActionSet());
588
589                    Map<String, String> customActions = new LinkedHashMap<String, String>();
590                    customActions.put("NONE", "NONE");
591
592                    for (ActionType actionType : actionListActionTypes) {
593                        if (actionSet.hasAction(actionType.getCode()) &&
594                                isActionCompatibleRequest(actionItem, actionType.getCode())) {
595
596                            final boolean isFyi = ActionType.FYI == actionType; // make the conditional easier to read
597
598                            if (!isFyi || (isFyi && showClearFyi)) { // deal with special FYI preference
599                                customActions.put(actionType.getCode(), actionType.getLabel());
600                                pageActions.add(actionType);
601                            }
602                        }
603                    }
604
605                    if (customActions.size() > 1) {
606                        actionItem.setCustomActions(customActions);
607                        haveCustomActions = true;
608                    }
609
610                    actionItem.setDisplayParameters(customization.getDisplayParameters());
611
612                    haveDisplayParameters = haveDisplayParameters || (actionItem.getDisplayParameters() != null);
613                }
614
615            } catch (Exception e) {
616                // if there's a problem loading the custom action list attribute, let's go ahead and display the vanilla action item
617                LOG.error("Problem loading custom action list attribute", e);
618                customActionListProblemIds.add(actionItem.getDocumentId());
619            }
620            currentPage.add(actionItem);
621        }
622
623        // configure custom actions on form
624        form.setHasCustomActions(haveCustomActions);
625
626        Map<String, String> defaultActions = new LinkedHashMap<String, String>();
627        defaultActions.put("NONE", "NONE");
628
629        for (ActionType actionType : actionListActionTypes) {
630            if (pageActions.contains(actionType)) {
631
632                final boolean isFyi = ActionType.FYI == actionType;
633                // special logic for FYIs:
634                if (isFyi) {
635                    // clearing FYIs can be done in any action list not just a customized one
636                    if(showClearFyi) {
637                        defaultActions.put(actionType.getCode(), actionType.getLabel());
638                    }
639                } else { // all the other actions
640                    defaultActions.put(actionType.getCode(), actionType.getLabel());
641                    form.setCustomActionList(Boolean.TRUE);
642                }
643            }
644        }
645
646        if (defaultActions.size() > 1) {
647            form.setDefaultActions(defaultActions);
648        }
649
650        form.setHasDisplayParameters(haveDisplayParameters);
651
652        generateActionItemErrors(CUSTOMACTIONLIST_PROP, ACTIONLIST_BAD_CUSTOM_ACTION_LIST_ITEMS_ERRKEY, customActionListProblemIds);
653        return new PaginatedActionList(currentPage, actionList.size(), page, pageSize, "actionList", sortCriterion, sortOrder);
654    }
655
656    // convert a List of org.kuali.rice.kew.actionitem.ActionItemS to org.kuali.rice.kew.api.action.ActionItemS
657    private List<org.kuali.rice.kew.api.action.ActionItem> convertToApiActionItems(List<? extends ActionItemActionListExtension> actionList) {
658        List<org.kuali.rice.kew.api.action.ActionItem> apiActionItems = new ArrayList<org.kuali.rice.kew.api.action.ActionItem>(actionList.size());
659
660        for (ActionItemActionListExtension actionItemObj : actionList) {
661            apiActionItems.add(
662                    org.kuali.rice.kew.api.action.ActionItem.Builder.create(actionItemObj).build());
663        }
664        return apiActionItems;
665    }
666
667    private void generateActionItemErrors(String propertyName, String errorKey, List<String> documentIds) {
668        if (!documentIds.isEmpty()) {
669            String documentIdsString = StringUtils.join(documentIds.iterator(), ", ");
670            GlobalVariables.getMessageMap().putError(propertyName, errorKey, documentIdsString);
671        }
672    }
673
674    private void generateActionItemErrors(List<? extends ActionItemActionListExtension> actionList) {
675        for (ActionItemActionListExtension actionItem : actionList) {
676            if(!KewApiConstants.ACTION_REQUEST_CODES.containsKey(actionItem.getActionRequestCd())) {
677                GlobalVariables.getMessageMap().putError(ACTIONREQUESTCD_PROP,ACTIONITEM_ACTIONREQUESTCD_INVALID_ERRKEY,actionItem.getId()+"");
678            }
679        }
680    }
681
682
683    public ActionForward takeMassActions(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
684        ActionListForm actionListForm = (ActionListForm) form;
685        List<? extends ActionItemActionListExtension> actionList = (List<? extends ActionItemActionListExtension>) request.getSession().getAttribute(ACTION_LIST_KEY);
686        if (actionList == null) {
687            return start(mapping, new ActionListForm(), request, response);
688        }
689        ActionMessages messages = new ActionMessages();
690        List<ActionInvocation> invocations = new ArrayList<ActionInvocation>();
691        int index = 0;
692        for (Object element : actionListForm.getActionsToTake()) {
693            ActionToTake actionToTake = (ActionToTake) element;
694            if (actionToTake != null && actionToTake.getActionTakenCd() != null &&
695                    !"".equals(actionToTake.getActionTakenCd()) &&
696                    !"NONE".equalsIgnoreCase(actionToTake.getActionTakenCd()) &&
697                    actionToTake.getActionItemId() != null) {
698                ActionItemActionListExtension actionItem = getActionItemFromActionList(actionList, actionToTake.getActionItemId());
699                if (actionItem == null) {
700                    LOG.warn("Could not locate the ActionItem to take mass action against in the action list: " + actionToTake.getActionItemId());
701                    continue;
702                }
703                invocations.add(ActionInvocation.create(ActionType.fromCode(actionToTake.getActionTakenCd()), actionItem.getId()));
704            }
705            index++;
706        }
707        KEWServiceLocator.getWorkflowDocumentService().takeMassActions(getUserSession().getPrincipalId(), invocations);
708        messages.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("general.routing.processed"));
709        saveMessages(request, messages);
710        ActionListForm cleanForm = new ActionListForm();
711        request.setAttribute(mapping.getName(), cleanForm);
712        request.getSession().setAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY, "true");
713        return start(mapping, cleanForm, request, response);
714    }
715
716    protected ActionItemActionListExtension getActionItemFromActionList(List<? extends ActionItemActionListExtension> actionList, String actionItemId) {
717        for (ActionItemActionListExtension actionItem : actionList) {
718            if (actionItem.getId().equals(actionItemId)) {
719                return actionItem;
720            }
721        }
722        return null;
723    }
724
725    public ActionForward helpDeskActionListLogin(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
726        ActionListForm actionListForm = (ActionListForm) form;
727        String name = actionListForm.getHelpDeskActionListUserName();
728        if (!"true".equals(request.getAttribute("helpDeskActionList"))) {
729            throw new AuthorizationException(getUserSession().getPrincipalId(), "helpDeskActionListLogin", getClass().getSimpleName());
730        }
731        try
732        {
733            final Principal helpDeskActionListPrincipal = KEWServiceLocator.getIdentityHelperService().getPrincipalByPrincipalName(name);
734            final Person helpDeskActionListPerson = KEWServiceLocator.getIdentityHelperService().getPersonByPrincipalName(name);
735
736            GlobalVariables.getUserSession().addObject(KewApiConstants.HELP_DESK_ACTION_LIST_PRINCIPAL_ATTR_NAME, helpDeskActionListPrincipal);
737            GlobalVariables.getUserSession().addObject(KewApiConstants.HELP_DESK_ACTION_LIST_PERSON_ATTR_NAME, helpDeskActionListPerson);
738        }
739        catch (RiceRuntimeException rre)
740        {
741            GlobalVariables.getMessageMap().putError(HELPDESK_ACTIONLIST_USERNAME, HELPDESK_LOGIN_INVALID_ERRKEY, name);
742        }
743        catch (RiceIllegalArgumentException e) {
744            GlobalVariables.getMessageMap().putError(HELPDESK_ACTIONLIST_USERNAME, HELPDESK_LOGIN_INVALID_ERRKEY, name);
745        }
746        catch (NullPointerException npe)
747        {
748            GlobalVariables.getMessageMap().putError("null", HELPDESK_LOGIN_EMPTY_ERRKEY, name);
749        }
750        actionListForm.setDelegator(null);
751        request.getSession().setAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY, "true");
752        return start(mapping, form, request, response);
753    }
754
755    public ActionForward clearFilter(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
756        LOG.debug("clearFilter ActionListAction");
757        final org.kuali.rice.krad.UserSession commonUserSession = getUserSession();
758        commonUserSession.removeObject(KewApiConstants.ACTION_LIST_FILTER_ATTR_NAME);
759        request.getSession().setAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY, "true");
760        LOG.debug("end clearFilter ActionListAction");
761        return start(mapping, form, request, response);
762    }
763
764    public ActionForward clearHelpDeskActionListUser(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
765        LOG.debug("clearHelpDeskActionListUser ActionListAction");
766        GlobalVariables.getUserSession().removeObject(KewApiConstants.HELP_DESK_ACTION_LIST_PRINCIPAL_ATTR_NAME);
767        GlobalVariables.getUserSession().removeObject(KewApiConstants.HELP_DESK_ACTION_LIST_PERSON_ATTR_NAME);
768        LOG.debug("end clearHelpDeskActionListUser ActionListAction");
769        return start(mapping, form, request, response);
770    }
771
772    /**
773     * Generates an Action List count.
774     */
775    public ActionForward count(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
776        ActionListForm alForm = (ActionListForm)form;
777        Person user = getUserSession().getPerson();
778        alForm.setCount(KEWServiceLocator.getActionListService().getCount(user.getPrincipalId()));
779        LOG.info("Fetched Action List count of " + alForm.getCount() + " for user " + user.getPrincipalName());
780        return mapping.findForward("count");
781    }
782
783    public ActionForward removeOutboxItems(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
784        ActionListForm alForm = (ActionListForm)form;
785        if (alForm.getOutboxItems() != null) {
786            KEWServiceLocator.getActionListService().removeOutboxItems(getUserSession().getPrincipalId(), Arrays.asList(alForm.getOutboxItems()));
787        }
788
789        alForm.setViewOutbox("true");
790        return start(mapping, form, request, response);
791    }
792
793    /**
794     * Navigate to the Action List Filter page, preserving any newly-modified primary/secondary delegation filters as necessary.
795     */
796    public ActionForward viewFilter(ActionMapping mapping, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception {
797        start(mapping, actionForm, request, response);
798        return mapping.findForward("viewFilter");
799    }
800
801    /**
802     * Navigate to the user's Preferences page, preserving any newly-modified primary/secondary delegation filters as necessary.
803     */
804    public ActionForward viewPreferences(ActionMapping mapping, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception {
805        start(mapping, actionForm, request, response);
806        return mapping.findForward("viewPreferences");
807    }
808
809    private boolean isActionCompatibleRequest(ActionItemActionListExtension actionItem, String actionTakenCode) {
810        boolean actionCompatible = false;
811        String requestCd = actionItem.getActionRequestCd();
812
813        //FYI request matches FYI
814        if (KewApiConstants.ACTION_REQUEST_FYI_REQ.equals(requestCd) && KewApiConstants.ACTION_TAKEN_FYI_CD.equals(actionTakenCode)) {
815            actionCompatible = true || actionCompatible;
816        }
817
818        // ACK request matches ACK
819        if (KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ.equals(requestCd) && KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD.equals(actionTakenCode)) {
820            actionCompatible = true || actionCompatible;
821        }
822
823        // APPROVE request matches all but FYI and ACK
824        if (KewApiConstants.ACTION_REQUEST_APPROVE_REQ.equals(requestCd) && !(KewApiConstants.ACTION_TAKEN_FYI_CD.equals(actionTakenCode) || KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD.equals(actionTakenCode))) {
825            actionCompatible = true || actionCompatible;
826        }
827
828        // COMPLETE request matches all but FYI and ACK
829        if (KewApiConstants.ACTION_REQUEST_COMPLETE_REQ.equals(requestCd) && !(KewApiConstants.ACTION_TAKEN_FYI_CD.equals(actionTakenCode) || KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD.equals(actionTakenCode))) {
830            actionCompatible = true || actionCompatible;
831        }
832
833        return actionCompatible;
834    }
835
836    private UserSession getUserSession(){
837        return GlobalVariables.getUserSession();
838    }
839
840    private static class ActionItemComparator implements Comparator<ActionItemActionListExtension> {
841
842        private static final String ACTION_LIST_DEFAULT_SORT = "routeHeader.createDate";
843
844        private final String sortName;
845
846        public ActionItemComparator(String sortName) {
847            if (StringUtils.isEmpty(sortName)) {
848                sortName = ACTION_LIST_DEFAULT_SORT;
849            }
850            this.sortName = sortName;
851        }
852
853        @Override
854        public int compare(ActionItemActionListExtension actionItem1, ActionItemActionListExtension actionItem2) {
855            try {
856                // invoke the power of the lookup functionality provided by the display tag library, this LookupUtil method allows for us
857                // to evaulate nested bean properties (like workgroup.groupNameId.nameId) in a null-safe manner.  For example, in the
858                // example if workgroup evaluated to NULL then LookupUtil.getProperty would return null rather than blowing an exception
859                Object property1 = LookupUtil.getProperty(actionItem1, sortName);
860                Object property2 = LookupUtil.getProperty(actionItem2, sortName);
861                if (property1 == null && property2 == null) {
862                    return 0;
863                } else if (property1 == null) {
864                    return -1;
865                } else if (property2 == null) {
866                    return 1;
867                }
868                if (property1 instanceof Comparable) {
869                    return ((Comparable)property1).compareTo(property2);
870                }
871                return property1.toString().compareTo(property2.toString());
872            } catch (Exception e) {
873                if (e instanceof RuntimeException) {
874                    throw (RuntimeException) e;
875                }
876                throw new RuntimeException("Could not sort for the given sort name: " + sortName, e);
877            }
878        }
879    }
880
881    // Lazy initialization holder class (see Effective Java Item #71)
882    private static class ActionListCustomizationMediatorHolder {
883        static final ActionListCustomizationMediator actionListCustomizationMediator =
884                KewFrameworkServiceLocator.getActionListCustomizationMediator();
885    }
886
887    private ActionListCustomizationMediator getActionListCustomizationMediator() {
888        return ActionListCustomizationMediatorHolder.actionListCustomizationMediator;
889    }
890
891    /**
892     * Simple class which defines the key of a partition of Action Items associated with an Application ID.
893     *
894     * <p>This class allows direct field access since it is intended for internal use only.</p>
895     */
896    private static final class PartitionKey {
897        String applicationId;
898        Set<String> customActionListAttributeNames;
899
900        PartitionKey(String applicationId, Collection<ExtensionDefinition> extensionDefinitions) {
901            this.applicationId = applicationId;
902            this.customActionListAttributeNames = new HashSet<String>();
903            for (ExtensionDefinition extensionDefinition : extensionDefinitions) {
904                this.customActionListAttributeNames.add(extensionDefinition.getName());
905            }
906        }
907
908        List<String> getCustomActionListAttributeNameList() {
909            return new ArrayList<String>(customActionListAttributeNames);
910        }
911
912        @Override
913        public boolean equals(Object o) {
914            if (!(o instanceof PartitionKey)) {
915                return false;
916            }
917            PartitionKey key = (PartitionKey) o;
918            EqualsBuilder builder = new EqualsBuilder();
919            builder.append(applicationId, key.applicationId);
920            builder.append(customActionListAttributeNames, key.customActionListAttributeNames);
921            return builder.isEquals();
922        }
923
924        @Override
925        public int hashCode() {
926            HashCodeBuilder builder = new HashCodeBuilder();
927            builder.append(applicationId);
928            builder.append(customActionListAttributeNames);
929            return builder.hashCode();
930        }
931    }
932}