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