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}