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}