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.kew.actionitem.ActionItem;
031import org.kuali.rice.kew.actionitem.ActionItemBase;
032import org.kuali.rice.kew.actionitem.OutboxItem;
033import org.kuali.rice.kew.actionlist.ActionListFilter;
034import org.kuali.rice.kew.actionlist.ActionToTake;
035import org.kuali.rice.kew.actionlist.PaginatedActionList;
036import org.kuali.rice.kew.actionlist.service.ActionListService;
037import org.kuali.rice.kew.actionrequest.Recipient;
038import org.kuali.rice.kew.api.KewApiConstants;
039import org.kuali.rice.kew.api.action.ActionInvocation;
040import org.kuali.rice.kew.api.action.ActionItemCustomization;
041import org.kuali.rice.kew.api.action.ActionSet;
042import org.kuali.rice.kew.api.action.ActionType;
043import org.kuali.rice.kew.api.exception.WorkflowException;
044import org.kuali.rice.kew.api.extension.ExtensionDefinition;
045import org.kuali.rice.kew.api.preferences.Preferences;
046import org.kuali.rice.kew.framework.KewFrameworkServiceLocator;
047import org.kuali.rice.kew.framework.actionlist.ActionListCustomizationMediator;
048import org.kuali.rice.kew.service.KEWServiceLocator;
049import org.kuali.rice.kew.util.PerformanceLogger;
050import org.kuali.rice.kim.api.identity.Person;
051import org.kuali.rice.kim.api.identity.principal.Principal;
052import org.kuali.rice.kim.api.identity.principal.PrincipalContract;
053import org.kuali.rice.kns.web.struts.action.KualiAction;
054import org.kuali.rice.kns.web.ui.ExtraButton;
055import org.kuali.rice.krad.UserSession;
056import org.kuali.rice.krad.exception.AuthorizationException;
057import org.kuali.rice.krad.util.GlobalVariables;
058
059import javax.servlet.http.HttpServletRequest;
060import javax.servlet.http.HttpServletResponse;
061import java.text.ParseException;
062import java.text.SimpleDateFormat;
063import java.util.*;
064
065/**
066 * Action doing Action list stuff
067 *
068 * @author Kuali Rice Team (rice.collab@kuali.org)a
069 *
070 */
071public class ActionListAction extends KualiAction {
072
073    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ActionListAction.class);
074    protected static final String MAX_ACTION_ITEM_DATE_FORMAT = "yyyy-MM-dd hh:mm:ss.S";
075
076    private static final String ACTION_LIST_KEY = "actionList";
077    private static final String ACTION_LIST_PAGE_KEY = "actionListPage";
078    private static final String ACTION_LIST_USER_KEY = "actionList.user";
079    /*private static final String REQUERY_ACTION_LIST_KEY = "requeryActionList";*/
080    private static final String ACTION_ITEM_COUNT_FOR_USER_KEY = "actionList.count";
081    private static final String MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY = "actionList.maxActionItemDateAssigned";
082
083    private static final String ACTIONREQUESTCD_PROP = "actionRequestCd";
084    private static final String CUSTOMACTIONLIST_PROP = "customActionList";
085    private static final String ACTIONITEM_PROP = "actionitem";
086    private static final String HELPDESK_ACTIONLIST_USERNAME = "helpDeskActionListUserName";
087
088    private static final String ACTIONITEM_ACTIONREQUESTCD_INVALID_ERRKEY = "actionitem.actionrequestcd.invalid";
089    private static final String ACTIONLIST_BAD_CUSTOM_ACTION_LIST_ITEMS_ERRKEY = "actionlist.badCustomActionListItems";
090    private static final String ACTIONLIST_BAD_ACTION_ITEMS_ERRKEY = "actionlist.badActionItems";
091    private static final String HELPDESK_LOGIN_EMPTY_ERRKEY = "helpdesk.login.empty";
092    private static final String HELPDESK_LOGIN_INVALID_ERRKEY = "helpdesk.login.invalid";
093
094    private static final ActionType [] actionListActionTypes =
095            { ActionType.APPROVE, ActionType.DISAPPROVE, ActionType.CANCEL, ActionType.ACKNOWLEDGE, ActionType.FYI };
096
097    @Override
098    public ActionForward execute(ActionMapping mapping, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception {
099        ActionListForm frm = (ActionListForm)actionForm;
100        request.setAttribute("Constants", getServlet().getServletContext().getAttribute("KewApiConstants"));
101        request.setAttribute("preferences", getUserSession().retrieveObject(KewApiConstants.PREFERENCES));
102        frm.setHeaderButtons(getHeaderButtons());
103        return super.execute(mapping, actionForm, request, response);
104    }
105
106    private List<ExtraButton> getHeaderButtons(){
107        List<ExtraButton> headerButtons = new ArrayList<ExtraButton>();
108        ExtraButton eb = new ExtraButton();
109        String krBaseUrl = ConfigContext.getCurrentContextConfig().getKRBaseURL();
110        eb.setExtraButtonSource( krBaseUrl + "/images/tinybutton-preferences.gif");
111        eb.setExtraButtonOnclick("Preferences.do?returnMapping=viewActionList");
112
113        headerButtons.add(eb);
114        eb = new ExtraButton();
115        eb.setExtraButtonSource(krBaseUrl + "/images/tinybutton-refresh.gif");
116        eb.setExtraButtonProperty("methodToCall.refresh");
117
118        headerButtons.add(eb);
119        eb = new ExtraButton();
120        eb.setExtraButtonSource(krBaseUrl + "/images/tinybutton-filter.gif");
121        eb.setExtraButtonOnclick("javascript: window.open('ActionListFilter.do?methodToCall=start');");
122        headerButtons.add(eb);
123
124
125        return headerButtons;
126    }
127
128    @Override
129    public ActionForward refresh(ActionMapping mapping,
130                                 ActionForm form,
131                                         HttpServletRequest request,
132                                         HttpServletResponse response) throws Exception {
133        request.getSession().setAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY, "true");
134        return start(mapping, form, request, response);
135    }
136
137    @Override
138    protected ActionForward defaultDispatch(ActionMapping mapping,
139                                            ActionForm form, HttpServletRequest request,
140                                            HttpServletResponse response) throws Exception {
141        return start(mapping, form, request, response);
142    }
143
144    public ActionForward start(ActionMapping mapping, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception {
145        PerformanceLogger plog = new PerformanceLogger();
146        plog.log("Starting ActionList fetch");
147        ActionListForm form = (ActionListForm) actionForm;
148        ActionListService actionListSrv = KEWServiceLocator.getActionListService();
149
150
151        // process display tag parameters
152        Integer page = form.getPage();
153        String sortCriterion = form.getSort();
154        SortOrderEnum sortOrder = SortOrderEnum.ASCENDING;
155        final UserSession uSession = getUserSession();
156
157        if (form.getDir() != null) {
158            sortOrder = parseSortOrder(form.getDir());
159        }
160        else if ( !StringUtils.isEmpty((String) uSession.retrieveObject(KewApiConstants.SORT_ORDER_ATTR_NAME)))     {
161            sortOrder = parseSortOrder((String) uSession.retrieveObject(KewApiConstants.SORT_ORDER_ATTR_NAME));
162        }
163        // if both the page and the sort criteria are null, that means its the first entry into the page, use defaults
164        if (page == null && sortCriterion == null) {
165            page = 1;
166            sortCriterion = ActionItemComparator.ACTION_LIST_DEFAULT_SORT;
167        }
168        else if ( !StringUtils.isEmpty((String) uSession.retrieveObject(KewApiConstants.SORT_CRITERIA_ATTR_NAME)))     {
169            sortCriterion = (String) uSession.retrieveObject(KewApiConstants.SORT_CRITERIA_ATTR_NAME);
170        }
171        // if the page is still null, that means the user just performed a sort action, pull the currentPage off of the form
172        if (page == null) {
173            page = form.getCurrentPage();
174        }
175
176        // update the values of the "current" display tag parameters
177        form.setCurrentPage(page);
178        if (!StringUtils.isEmpty(sortCriterion)) {
179            form.setCurrentSort(sortCriterion);
180            form.setCurrentDir(getSortOrderValue(sortOrder));
181        }
182
183        // reset the default action on the form
184        form.setDefaultActionToTake("NONE");
185
186        boolean freshActionList = true;
187        // retrieve cached action list
188        List<? extends ActionItemBase> actionList = (List<? extends ActionItemBase>)request.getSession().getAttribute(ACTION_LIST_KEY);
189        plog.log("Time to initialize");
190        try {
191            //UserSession uSession = getUserSession(request);
192            String principalId = null;
193            if (uSession.retrieveObject(KewApiConstants.ACTION_LIST_FILTER_ATTR_NAME) == null) {
194                ActionListFilter filter = new ActionListFilter();
195                filter.setDelegationType(DelegationType.SECONDARY.getCode());
196                filter.setExcludeDelegationType(true);
197                uSession.addObject(KewApiConstants.ACTION_LIST_FILTER_ATTR_NAME, filter);
198            }
199
200            final ActionListFilter filter = (ActionListFilter) uSession.retrieveObject(KewApiConstants.ACTION_LIST_FILTER_ATTR_NAME);
201            /* 'forceListRefresh' variable used to signify that the action list filter has changed
202             * any time the filter changes the action list must be refreshed or filter may not take effect on existing
203             * list items... only exception is if action list has not loaded previous and fetching of the list has not
204             * occurred yet
205             */
206            boolean forceListRefresh = request.getSession().getAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY) != null;
207            if (uSession.retrieveObject(KewApiConstants.HELP_DESK_ACTION_LIST_PRINCIPAL_ATTR_NAME) != null) {
208                principalId = ((PrincipalContract) uSession.retrieveObject(KewApiConstants.HELP_DESK_ACTION_LIST_PRINCIPAL_ATTR_NAME)).getPrincipalId();
209            } else {
210                if (!StringUtils.isEmpty(form.getDocType())) {
211                    filter.setDocumentType(form.getDocType());
212                    filter.setExcludeDocumentType(false);
213                    forceListRefresh = true;
214                }
215                principalId = uSession.getPerson().getPrincipalId();
216            }
217
218            final Preferences preferences = (Preferences) getUserSession().retrieveObject(KewApiConstants.PREFERENCES);
219
220            if (!StringUtils.isEmpty(form.getDelegationId())) {
221                if (!KewApiConstants.DELEGATION_DEFAULT.equals(form.getDelegationId())) {
222                    // If the user can filter by both primary and secondary delegation, and both drop-downs have non-default values assigned,
223                    // then reset the primary delegation drop-down's value when the primary delegation drop-down's value has remained unaltered
224                    // but the secondary drop-down's value has been altered; but if one of these alteration situations does not apply, reset the
225                    // secondary delegation drop-down.
226                    if (StringUtils.isNotBlank(form.getPrimaryDelegateId()) && !KewApiConstants.PRIMARY_DELEGATION_DEFAULT.equals(form.getPrimaryDelegateId())){
227                        if (form.getPrimaryDelegateId().equals(request.getParameter("oldPrimaryDelegateId")) &&
228                                !form.getDelegationId().equals(request.getParameter("oldDelegationId"))) {
229                            form.setPrimaryDelegateId(KewApiConstants.PRIMARY_DELEGATION_DEFAULT);
230                        } else {
231                            form.setDelegationId(KewApiConstants.DELEGATION_DEFAULT);
232                        }
233                    } else if (StringUtils.isNotBlank(filter.getPrimaryDelegateId()) &&
234                            !KewApiConstants.PRIMARY_DELEGATION_DEFAULT.equals(filter.getPrimaryDelegateId())) {
235                        // If the primary delegation drop-down is invisible but a primary delegation filter is in place, and if the secondary delegation
236                        // drop-down has a non-default value selected, then reset the primary delegation filtering.
237                        filter.setPrimaryDelegateId(KewApiConstants.PRIMARY_DELEGATION_DEFAULT);
238                    }
239                }
240                // Enable the secondary delegation filtering.
241                filter.setDelegatorId(form.getDelegationId());
242                filter.setExcludeDelegatorId(false);
243                actionList = null;
244            }
245
246            if (!StringUtils.isEmpty(form.getPrimaryDelegateId())) {
247                // If the secondary delegation drop-down is invisible but a secondary delegation filter is in place, and if the primary delegation
248                // drop-down has a non-default value selected, then reset the secondary delegation filtering.
249                if (StringUtils.isBlank(form.getDelegationId()) && !KewApiConstants.PRIMARY_DELEGATION_DEFAULT.equals(form.getPrimaryDelegateId()) &&
250                        StringUtils.isNotBlank(filter.getDelegatorId()) &&
251                        !KewApiConstants.DELEGATION_DEFAULT.equals(filter.getDelegatorId())) {
252                    filter.setDelegatorId(KewApiConstants.DELEGATION_DEFAULT);
253                }
254                // Enable the primary delegation filtering.
255                filter.setPrimaryDelegateId(form.getPrimaryDelegateId());
256                filter.setExcludeDelegatorId(false);
257                actionList = null;
258            }
259
260            // if the user has changed, we need to refresh the action list
261            if (!principalId.equals(request.getSession().getAttribute(ACTION_LIST_USER_KEY))) {
262                actionList = null;
263            }
264
265            if (isOutboxMode(form, request, preferences)) {
266                actionList = new ArrayList<OutboxItem>(actionListSrv.getOutbox(principalId, filter));
267                form.setOutBoxEmpty(actionList.isEmpty());
268            } else {
269
270                    SimpleDateFormat dFormatter = new SimpleDateFormat(MAX_ACTION_ITEM_DATE_FORMAT);
271                    if (actionList == null) {
272                        List<Object> countAndMaxDate = actionListSrv.getMaxActionItemDateAssignedAndCountForUser(principalId);
273                        if (countAndMaxDate.isEmpty() || countAndMaxDate.get(0) == null ) {
274                            if (countAndMaxDate.isEmpty()) {
275                                countAndMaxDate.add(0, new Date(0));
276                                countAndMaxDate.add(1, 0);
277                            } else {
278                                countAndMaxDate.set(0, new Date(0));
279                            }
280                        }
281                        request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, dFormatter.format(countAndMaxDate.get(0)));
282                        request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, (Long)countAndMaxDate.get(1));
283                        // fetch the action list
284                        actionList = new ArrayList<ActionItem>(actionListSrv.getActionList(principalId, filter));
285
286                        request.getSession().setAttribute(ACTION_LIST_USER_KEY, principalId);
287                    } else if (forceListRefresh) {
288                        // force a refresh... usually based on filter change or parameter specifying refresh needed
289                        actionList = new ArrayList<ActionItem>(actionListSrv.getActionList(principalId, filter));
290                        request.getSession().setAttribute(ACTION_LIST_USER_KEY, principalId);
291                        List<Object> countAndMaxDate = actionListSrv.getMaxActionItemDateAssignedAndCountForUser(principalId);
292                        if (countAndMaxDate.isEmpty() || countAndMaxDate.get(0) == null ) {
293                            if (countAndMaxDate.isEmpty()) {
294                                countAndMaxDate.add(0, new Date(0));
295                                countAndMaxDate.add(1, 0);
296                            } else {
297                                countAndMaxDate.set(0, new Date(0));
298                            }
299                        }
300                        request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, dFormatter.format(countAndMaxDate.get(0)));
301                        request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, (Long)countAndMaxDate.get(1));
302
303                    }else if (refreshList(request,principalId)){
304                        actionList = new ArrayList<ActionItem>(actionListSrv.getActionList(principalId, filter));
305                        request.getSession().setAttribute(ACTION_LIST_USER_KEY, principalId);
306
307                    } else {
308                        Boolean update = (Boolean) uSession.retrieveObject(KewApiConstants.UPDATE_ACTION_LIST_ATTR_NAME);
309                        if (update == null || !update) {
310                            freshActionList = false;
311                        }
312                    }
313                    request.getSession().setAttribute(ACTION_LIST_KEY, actionList);
314
315            }
316            // reset the requery action list key
317            request.getSession().setAttribute(KewApiConstants.REQUERY_ACTION_LIST_KEY, null);
318
319            // build the drop-down of delegators
320            if (KewApiConstants.DELEGATORS_ON_ACTION_LIST_PAGE.equalsIgnoreCase(preferences.getDelegatorFilter())) {
321                Collection<Recipient> delegators = actionListSrv.findUserSecondaryDelegators(principalId);
322                form.setDelegators(ActionListUtil.getWebFriendlyRecipients(delegators));
323                form.setDelegationId(filter.getDelegatorId());
324            }
325
326            // Build the drop-down of primary delegates.
327            if (KewApiConstants.PRIMARY_DELEGATES_ON_ACTION_LIST_PAGE.equalsIgnoreCase(preferences.getPrimaryDelegateFilter())) {
328                Collection<Recipient> pDelegates = actionListSrv.findUserPrimaryDelegations(principalId);
329                form.setPrimaryDelegates(ActionListUtil.getWebFriendlyRecipients(pDelegates));
330                form.setPrimaryDelegateId(filter.getPrimaryDelegateId());
331            }
332
333            form.setFilterLegend(filter.getFilterLegend());
334            plog.log("Setting attributes");
335
336            int pageSize = getPageSize(preferences);
337            // initialize the action list if necessary
338            if (freshActionList) {
339                plog.log("calling initializeActionList");
340                initializeActionList(actionList, preferences);
341                plog.log("done w/ initializeActionList");
342                // put this in to resolve EN-112 (http://beatles.uits.indiana.edu:8081/jira/browse/EN-112)
343                // 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
344                if (sortCriterion == null) {
345                    sortCriterion = form.getCurrentSort();
346                    sortOrder = parseSortOrder(form.getCurrentDir());
347                }
348            }
349            // sort the action list if necessary
350            if (sortCriterion != null) {
351                sortActionList(actionList, sortCriterion, sortOrder);
352            }
353
354            plog.log("calling buildCurrentPage");
355            PaginatedList currentPage = buildCurrentPage(actionList, form.getCurrentPage(), form.getCurrentSort(),
356                    form.getCurrentDir(), pageSize, preferences, form);
357            plog.log("done w/ buildCurrentPage");
358            request.setAttribute(ACTION_LIST_PAGE_KEY, currentPage);
359            synchronized(uSession) {
360                uSession.addObject(KewApiConstants.UPDATE_ACTION_LIST_ATTR_NAME, Boolean.FALSE);
361                uSession.addObject(KewApiConstants.CURRENT_PAGE_ATTR_NAME, form.getCurrentPage());
362                uSession.addObject(KewApiConstants.SORT_CRITERIA_ATTR_NAME, form.getSort());
363                uSession.addObject(KewApiConstants.SORT_ORDER_ATTR_NAME, form.getCurrentDir());
364            }
365            plog.log("finished setting attributes, finishing action list fetch");
366        } catch (Exception e) {
367            LOG.error("Error loading action list.", e);
368        }
369
370        LOG.debug("end start ActionListAction");
371        return mapping.findForward("viewActionList");
372    }
373
374    /**
375     * Sets the maxActionItemDate and actionItemcount for user in the session
376     * @param request
377     * @param principalId
378     * @param actionListSrv
379     */
380    private void setCountAndMaxDate(HttpServletRequest request,String principalId,ActionListService actionListSrv ){
381        SimpleDateFormat dFormatter = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.S");
382        List<Object> countAndMaxDate = actionListSrv.getMaxActionItemDateAssignedAndCountForUser(principalId);
383        String maxActionItemDateAssignedForUserKey = "";
384        if(countAndMaxDate.get(0)!= null){
385           maxActionItemDateAssignedForUserKey = dFormatter.format(countAndMaxDate.get(0));
386        }
387        request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, maxActionItemDateAssignedForUserKey);
388        request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, (Long)countAndMaxDate.get(1));
389    }
390
391    private boolean refreshList(HttpServletRequest request,String principalId ){
392        List<Object> maxActionItemDateAssignedAndCount = KEWServiceLocator.getActionListService().getMaxActionItemDateAssignedAndCountForUser(
393                principalId);
394        long count = (Long) maxActionItemDateAssignedAndCount.get(1);
395        int previousCount = 0;
396        Object actionItemCountFromSession = request.getSession().getAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY);
397        if ( actionItemCountFromSession != null ) {
398            previousCount = Integer.parseInt(actionItemCountFromSession.toString());
399        }
400        SimpleDateFormat dFormatter = new SimpleDateFormat(MAX_ACTION_ITEM_DATE_FORMAT);
401        Date maxActionItemDateAssigned = (Date) maxActionItemDateAssignedAndCount.get(0);
402        if ( maxActionItemDateAssigned == null ) {
403            maxActionItemDateAssigned = new Date(0);
404        }
405        Date previousMaxActionItemDateAssigned= null;
406        try{
407            Object dateAttributeFromSession = request.getSession().getAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY);
408            if ( dateAttributeFromSession != null ) {
409                previousMaxActionItemDateAssigned = dFormatter.parse(dateAttributeFromSession.toString());
410            }
411        } catch (ParseException e){
412            LOG.warn( MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY + " in session did not have expected date format.  "
413                    + "Was: " + request.getSession().getAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY), e );
414        }
415        if(previousCount!= count){
416            request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, dFormatter.format(
417                    maxActionItemDateAssigned));
418            request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, count);
419            return true;
420        }else if(previousMaxActionItemDateAssigned == null || previousMaxActionItemDateAssigned.compareTo(maxActionItemDateAssigned)!=0){
421            request.getSession().setAttribute(MAX_ACTION_ITEM_DATE_ASSIGNED_FOR_USER_KEY, dFormatter.format(
422                    maxActionItemDateAssigned));
423            request.getSession().setAttribute(ACTION_ITEM_COUNT_FOR_USER_KEY, count);
424            return true;
425        } else{
426            return false;
427        }
428
429    }
430
431    private SortOrderEnum parseSortOrder(String dir) throws WorkflowException {
432        if ("asc".equals(dir)) {
433            return SortOrderEnum.ASCENDING;
434        } else if ("desc".equals(dir)) {
435            return SortOrderEnum.DESCENDING;
436        }
437        throw new WorkflowException("Invalid sort direction: " + dir);
438    }
439
440    private String getSortOrderValue(SortOrderEnum sortOrder) {
441        if (SortOrderEnum.ASCENDING.equals(sortOrder)) {
442            return "asc";
443        } else if (SortOrderEnum.DESCENDING.equals(sortOrder)) {
444            return "desc";
445        }
446        return null;
447    }
448
449    private static final String OUT_BOX_MODE = "_OUT_BOX_MODE";
450
451    /**
452     * this method is setting 2 props on the {@link ActionListForm} that controls outbox behavior.
453     *  alForm.setViewOutbox("false"); -> this is set by user preferences and the actionlist.outbox.off config prop
454     *  alForm.setShowOutbox(false); -> this is set by user action clicking the ActionList vs. Outbox links.
455     *
456     * @param alForm
457     * @param request
458     * @return boolean indication whether the outbox should be fetched
459     */
460    private boolean isOutboxMode(ActionListForm alForm, HttpServletRequest request, Preferences preferences) {
461
462        boolean outBoxView = false;
463
464        if (! preferences.isUsingOutbox() || ! ConfigContext.getCurrentContextConfig().getOutBoxOn()) {
465            request.getSession().setAttribute(OUT_BOX_MODE, Boolean.FALSE);
466            alForm.setViewOutbox("false");
467            alForm.setShowOutbox(false);
468            return false;
469        }
470
471        alForm.setShowOutbox(true);
472        if (StringUtils.isNotEmpty(alForm.getViewOutbox())) {
473            if (!Boolean.valueOf(alForm.getViewOutbox())) {
474                request.getSession().setAttribute(OUT_BOX_MODE, Boolean.FALSE);
475                outBoxView = false;
476            } else {
477                request.getSession().setAttribute(OUT_BOX_MODE, Boolean.TRUE);
478                outBoxView = true;
479            }
480        } else {
481
482            if (request.getSession().getAttribute(OUT_BOX_MODE) == null) {
483                outBoxView = false;
484            } else {
485                outBoxView = (Boolean) request.getSession().getAttribute(OUT_BOX_MODE);
486            }
487        }
488        if (outBoxView) {
489            alForm.setViewOutbox("true");
490        } else {
491            alForm.setViewOutbox("false");
492        }
493        return outBoxView;
494    }
495
496    private void sortActionList(List<? extends ActionItemBase> actionList, String sortName, SortOrderEnum sortOrder) {
497        if (StringUtils.isEmpty(sortName)) {
498            return;
499        }
500
501        Comparator<ActionItemBase> comparator = new ActionItemComparator(sortName);
502        if (SortOrderEnum.DESCENDING.equals(sortOrder)) {
503            comparator = ComparatorUtils.reversedComparator(comparator);
504        }
505
506        Collections.sort(actionList, comparator);
507
508        // re-index the action items
509        int index = 0;
510        for (ActionItemBase actionItem : actionList) {
511            actionItem.setActionListIndex(index++);
512        }
513    }
514
515    private void initializeActionList(List<? extends ActionItemBase> actionList, Preferences preferences) {
516        List<String> actionItemProblemIds = new ArrayList<String>();
517        int index = 0;
518        generateActionItemErrors(actionList);
519        for (Iterator<? extends ActionItemBase> iterator = actionList.iterator(); iterator.hasNext();) {
520            ActionItemBase actionItem = iterator.next();
521            if (actionItem.getDocumentId() == null) {
522                LOG.error("Somehow there exists an ActionItem with a null document id!  actionItemId=" + actionItem.getId());
523                iterator.remove();
524                continue;
525            }
526            try {
527                actionItem.initialize(preferences);
528                actionItem.setActionListIndex(index);
529                index++;
530            } catch (Exception e) {
531                // 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
532                // and display an appropriate error message to the user
533                LOG.error("Error loading action list for action item " + actionItem.getId(), e);
534                iterator.remove();
535                actionItemProblemIds.add(actionItem.getDocumentId());
536            }
537        }
538        generateActionItemErrors(ACTIONITEM_PROP, ACTIONLIST_BAD_ACTION_ITEMS_ERRKEY, actionItemProblemIds);
539    }
540
541    /**
542     * Gets the page size of the Action List.  Uses the user's preferences for page size unless the action list
543     * has been throttled by an application constant, in which case it uses the smaller of the two values.
544     */
545    protected int getPageSize(Preferences preferences) {
546        return Integer.parseInt(preferences.getPageSize());
547    }
548
549    protected PaginatedList buildCurrentPage(List<? extends ActionItemBase> actionList, Integer page, String sortCriterion, String sortDirection,
550                                             int pageSize, Preferences preferences, ActionListForm form) throws WorkflowException {
551        List<ActionItemBase> currentPage = new ArrayList<ActionItemBase>(pageSize);
552
553        boolean haveCustomActions = false;
554        boolean haveDisplayParameters = false;
555
556        final boolean showClearFyi = KewApiConstants.PREFERENCES_YES_VAL.equalsIgnoreCase(preferences.getShowClearFyi());
557
558        // collects all the actions for items on this page
559        Set<ActionType> pageActions = new HashSet<ActionType>();
560
561        List<String> customActionListProblemIds = new ArrayList<String>();
562        SortOrderEnum sortOrder = parseSortOrder(sortDirection);
563        int startIndex = (page - 1) * pageSize;
564        int endIndex = startIndex + pageSize;
565        generateActionItemErrors(actionList);
566
567        LOG.info("Beginning processing of Action List Customizations (total: " + actionList.size() + " Action Items)");
568        long start = System.currentTimeMillis();
569
570        Map<String, ActionItemCustomization>  customizationMap = new HashMap<String, ActionItemCustomization>();
571        if (!StringUtils.equalsIgnoreCase("true", form.getViewOutbox())) {
572            customizationMap = 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            ActionItemBase 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 && !StringUtils.equalsIgnoreCase("true", form.getViewOutbox())) {
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 ActionItemBase> 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 (ActionItemBase 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 ActionItemBase> actionList) {
675        for (ActionItemBase 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 ActionItemBase> actionList = (List<? extends ActionItemBase>) 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                ActionItemBase 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 ActionItemBase getActionItemFromActionList(List<? extends ActionItemBase> actionList, String actionItemId) {
717        for (ActionItemBase 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        ActionRedirect redirect = new ActionRedirect(mapping.findForward("viewFilter"));
799        redirect.addParameter("targetSpec", ((ActionListForm)actionForm).getTargetSpec());
800        return redirect;
801    }
802
803    /**
804     * Navigate to the user's Preferences page, preserving any newly-modified primary/secondary delegation filters as necessary.
805     */
806    public ActionForward viewPreferences(ActionMapping mapping, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception {
807        start(mapping, actionForm, request, response);
808        ActionRedirect redirect = new ActionRedirect(mapping.findForward("viewPreferences"));
809        redirect.addParameter("targetSpec", ((ActionListForm)actionForm).getTargetSpec());
810        return redirect;
811    }
812
813    private boolean isActionCompatibleRequest(ActionItemBase actionItem, String actionTakenCode) {
814        boolean actionCompatible = false;
815        String requestCd = actionItem.getActionRequestCd();
816
817        //FYI request matches FYI
818        if (KewApiConstants.ACTION_REQUEST_FYI_REQ.equals(requestCd) && KewApiConstants.ACTION_TAKEN_FYI_CD.equals(actionTakenCode)) {
819            actionCompatible = true || actionCompatible;
820        }
821
822        // ACK request matches ACK
823        if (KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ.equals(requestCd) && KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD.equals(actionTakenCode)) {
824            actionCompatible = true || actionCompatible;
825        }
826
827        // APPROVE request matches all but FYI and ACK
828        if (KewApiConstants.ACTION_REQUEST_APPROVE_REQ.equals(requestCd) && !(KewApiConstants.ACTION_TAKEN_FYI_CD.equals(actionTakenCode) || KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD.equals(actionTakenCode))) {
829            actionCompatible = true || actionCompatible;
830        }
831
832        // COMPLETE request matches all but FYI and ACK
833        if (KewApiConstants.ACTION_REQUEST_COMPLETE_REQ.equals(requestCd) && !(KewApiConstants.ACTION_TAKEN_FYI_CD.equals(actionTakenCode) || KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD.equals(actionTakenCode))) {
834            actionCompatible = true || actionCompatible;
835        }
836
837        return actionCompatible;
838    }
839
840    private UserSession getUserSession(){
841        return GlobalVariables.getUserSession();
842    }
843
844    private static class ActionItemComparator implements Comparator<ActionItemBase> {
845
846        private static final String ACTION_LIST_DEFAULT_SORT = "routeHeader.createDate";
847
848        private final String sortName;
849
850        public ActionItemComparator(String sortName) {
851            if (StringUtils.isEmpty(sortName)) {
852                sortName = ACTION_LIST_DEFAULT_SORT;
853            }
854            this.sortName = sortName;
855        }
856
857        @Override
858        public int compare(ActionItemBase actionItem1, ActionItemBase actionItem2) {
859            try {
860                // invoke the power of the lookup functionality provided by the display tag library, this LookupUtil method allows for us
861                // to evaulate nested bean properties (like workgroup.groupNameId.nameId) in a null-safe manner.  For example, in the
862                // example if workgroup evaluated to NULL then LookupUtil.getProperty would return null rather than blowing an exception
863                Object property1 = LookupUtil.getProperty(actionItem1, sortName);
864                Object property2 = LookupUtil.getProperty(actionItem2, sortName);
865                if (property1 == null && property2 == null) {
866                    return 0;
867                } else if (property1 == null) {
868                    return -1;
869                } else if (property2 == null) {
870                    return 1;
871                }
872                if (property1 instanceof Comparable) {
873                    return ((Comparable)property1).compareTo(property2);
874                }
875                return property1.toString().compareTo(property2.toString());
876            } catch (Exception e) {
877                if (e instanceof RuntimeException) {
878                    throw (RuntimeException) e;
879                }
880                throw new RuntimeException("Could not sort for the given sort name: " + sortName, e);
881            }
882        }
883    }
884
885    // Lazy initialization holder class (see Effective Java Item #71)
886    private static class ActionListCustomizationMediatorHolder {
887        static final ActionListCustomizationMediator actionListCustomizationMediator =
888                KewFrameworkServiceLocator.getActionListCustomizationMediator();
889    }
890
891    private ActionListCustomizationMediator getActionListCustomizationMediator() {
892        return ActionListCustomizationMediatorHolder.actionListCustomizationMediator;
893    }
894
895    /**
896     * Simple class which defines the key of a partition of Action Items associated with an Application ID.
897     *
898     * <p>This class allows direct field access since it is intended for internal use only.</p>
899     */
900    private static final class PartitionKey {
901        String applicationId;
902        Set<String> customActionListAttributeNames;
903
904        PartitionKey(String applicationId, Collection<ExtensionDefinition> extensionDefinitions) {
905            this.applicationId = applicationId;
906            this.customActionListAttributeNames = new HashSet<String>();
907            for (ExtensionDefinition extensionDefinition : extensionDefinitions) {
908                this.customActionListAttributeNames.add(extensionDefinition.getName());
909            }
910        }
911
912        List<String> getCustomActionListAttributeNameList() {
913            return new ArrayList<String>(customActionListAttributeNames);
914        }
915
916        @Override
917        public boolean equals(Object o) {
918            if (!(o instanceof PartitionKey)) {
919                return false;
920            }
921            PartitionKey key = (PartitionKey) o;
922            EqualsBuilder builder = new EqualsBuilder();
923            builder.append(applicationId, key.applicationId);
924            builder.append(customActionListAttributeNames, key.customActionListAttributeNames);
925            return builder.isEquals();
926        }
927
928        @Override
929        public int hashCode() {
930            HashCodeBuilder builder = new HashCodeBuilder();
931            builder.append(applicationId);
932            builder.append(customActionListAttributeNames);
933            return builder.hashCode();
934        }
935    }
936}