View Javadoc
1   /**
2    * Copyright 2005-2015 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.actionlist.service.impl;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.core.api.config.property.ConfigContext;
21  import org.kuali.rice.core.api.criteria.Predicate;
22  import org.kuali.rice.core.api.criteria.QueryByCriteria;
23  import org.kuali.rice.core.api.criteria.QueryResults;
24  import org.kuali.rice.core.api.datetime.DateTimeService;
25  import org.kuali.rice.core.api.delegation.DelegationType;
26  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
27  import org.kuali.rice.kew.actionitem.ActionItem;
28  import org.kuali.rice.kew.actionitem.ActionItemBase;
29  import org.kuali.rice.kew.actionitem.OutboxItem;
30  import org.kuali.rice.kew.actionlist.ActionListFilter;
31  import org.kuali.rice.kew.actionlist.dao.ActionListDAO;
32  import org.kuali.rice.kew.actionlist.dao.impl.ActionListPriorityComparator;
33  import org.kuali.rice.kew.actionlist.service.ActionListService;
34  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
35  import org.kuali.rice.kew.actionrequest.KimGroupRecipient;
36  import org.kuali.rice.kew.actionrequest.Recipient;
37  import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
38  import org.kuali.rice.kew.actiontaken.ActionTakenValue;
39  import org.kuali.rice.kew.api.KewApiConstants;
40  import org.kuali.rice.kew.doctype.bo.DocumentType;
41  import org.kuali.rice.kew.doctype.service.DocumentTypeService;
42  import org.kuali.rice.kew.notification.service.NotificationService;
43  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
44  import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
45  import org.kuali.rice.kew.useroptions.UserOptions;
46  import org.kuali.rice.kew.useroptions.UserOptionsService;
47  import org.kuali.rice.kew.util.WebFriendlyRecipient;
48  import org.kuali.rice.kim.api.group.GroupService;
49  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
50  import org.kuali.rice.krad.data.DataObjectService;
51  import org.kuali.rice.krad.util.KRADConstants;
52  
53  import java.sql.Timestamp;
54  import java.util.ArrayList;
55  import java.util.Calendar;
56  import java.util.Collection;
57  import java.util.Collections;
58  import java.util.Date;
59  import java.util.HashMap;
60  import java.util.List;
61  import java.util.Map;
62  
63  import static org.kuali.rice.core.api.criteria.PredicateFactory.*;
64  
65  /**
66   * Default implementation of the {@link ActionListService}.
67   *
68   * @author Kuali Rice Team (rice.collab@kuali.org)
69   */
70  public class ActionListServiceImpl implements ActionListService {
71  
72      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ActionListServiceImpl.class);
73      private static final Integer DEFAULT_OUTBOX_ITEM_LIMIT = Integer.valueOf(10000);
74  
75      protected DataObjectService dataObjectService;
76      protected NotificationService notificationService;
77      protected DateTimeService dateTimeService;
78      protected ActionRequestService actionRequestService;
79      protected DocumentTypeService documentTypeService;
80      protected UserOptionsService userOptionsService;
81      protected RouteHeaderService routeHeaderService;
82  
83      protected ActionListDAO actionListDAO;
84  
85      @Override
86      public Collection<Recipient> findUserSecondaryDelegators(String principalId) {
87  
88          QueryByCriteria query = QueryByCriteria.Builder.fromPredicates(
89                  equal("principalId", principalId),
90                  equal("delegationType", DelegationType.SECONDARY.getCode()),
91                  or(isNotNull("delegatorPrincipalId"), isNotNull("delegatorGroupId")));
92  
93          QueryResults<ActionItem> results = dataObjectService.findMatching(ActionItem.class, query);
94  
95          Map<Object, Recipient> delegators = new HashMap<Object, Recipient>(results.getResults().size());
96  
97          for ( ActionItem actionItem : results.getResults() ) {
98              String delegatorPrincipalId = actionItem.getDelegatorPrincipalId();
99              String delegatorGroupId = actionItem.getDelegatorGroupId();
100 
101             if (delegatorPrincipalId != null && !delegators.containsKey(delegatorPrincipalId)) {
102                 delegators.put(delegatorPrincipalId,new WebFriendlyRecipient(KimApiServiceLocator.getPersonService().getPerson(delegatorPrincipalId)));
103             } else if (delegatorGroupId != null && !delegators.containsKey(delegatorGroupId)) {
104                 delegators.put(delegatorGroupId, new KimGroupRecipient(KimApiServiceLocator.getGroupService().getGroup(delegatorGroupId)));
105             }
106         }
107 
108         return delegators.values();
109     }
110 
111     @Override
112     public Collection<Recipient> findUserPrimaryDelegations(String principalId) {
113         List<String> workgroupIds = KimApiServiceLocator.getGroupService().getGroupIdsByPrincipalId(principalId);
114 
115         Predicate whoPredicate = null;
116         if (CollectionUtils.isNotEmpty(workgroupIds)) {
117             whoPredicate = or( equal("delegatorPrincipalId", principalId), in("delegatorGroupId", workgroupIds ) );
118         } else {
119             whoPredicate = equal("delegatorPrincipalId", principalId);
120         }
121         QueryByCriteria query = QueryByCriteria.Builder.fromPredicates(whoPredicate, equal("delegationType", DelegationType.PRIMARY.getCode() ) );
122 
123         QueryResults<ActionItem> results = dataObjectService.findMatching(ActionItem.class, query);
124 
125         Map<String, Recipient> delegators = new HashMap<String, Recipient>(results.getResults().size());
126 
127         for ( ActionItem actionItem : results.getResults() ) {
128             String recipientPrincipalId = actionItem.getPrincipalId();
129             if (recipientPrincipalId != null && !delegators.containsKey(recipientPrincipalId)) {
130                 delegators.put(recipientPrincipalId, new WebFriendlyRecipient(
131                         KimApiServiceLocator.getPersonService().getPerson(recipientPrincipalId)));
132             }
133         }
134 
135         return delegators.values();
136     }
137 
138     @Override
139     public Collection<ActionItem> getActionList(String principalId, ActionListFilter filter) {
140         List<String> filteredByItems = new ArrayList<String>();
141 
142         List<Predicate> crit = handleActionItemCriteria(principalId, filter, filteredByItems);
143 
144         if ( LOG.isDebugEnabled() ) {
145             LOG.debug("running query to get action list for criteria " + crit);
146         }
147         QueryByCriteria query = QueryByCriteria.Builder.fromPredicates(crit);
148         QueryResults<ActionItem> results = dataObjectService.findMatching(ActionItem.class, query);
149         if ( LOG.isDebugEnabled() ) {
150             LOG.debug("found " + results.getResults().size() + " action items for user " + principalId);
151         }
152 
153         if (filter != null) {
154             boolean filterOn = !filteredByItems.isEmpty();
155             filter.setFilterOn(filterOn);
156             filter.setFilterLegend(StringUtils.join(filteredByItems, ", "));
157         }
158 
159         return createActionListForUser(results.getResults());
160     }
161 
162     protected List<Predicate> handleActionItemCriteria( String principalId, ActionListFilter filter, List<String> filteredByItems ) {
163         LOG.debug("setting up Action List criteria");
164         ArrayList<Predicate> crit = new ArrayList<Predicate>();
165 
166         if ( filter != null ) {
167             handleActionRequestedCriteria(filter, crit, filteredByItems);
168             handleDocumentCreateDateCriteria(filter, crit, filteredByItems);
169             handleAssignedDateCriteria(filter, crit, filteredByItems);
170             handleRouteStatusCriteria(filter, crit, filteredByItems);
171             handleDocumentTitleCriteria(filter, crit, filteredByItems);
172             handleDocumentTypeCriteria(filter, crit, filteredByItems);
173             handleWorkgroupCriteria(filter, crit, filteredByItems);
174             handleRecipientCriteria(principalId, filter, crit, filteredByItems);
175         } else {
176             crit.add( equal("principalId", principalId) );
177         }
178         LOG.debug( "Completed setting up Action List criteria");
179         return crit;
180     }
181 
182     protected void handleActionRequestedCriteria( ActionListFilter filter, Collection<Predicate> crit, List<String> filteredByItems ) {
183         if ( StringUtils.isNotBlank(filter.getActionRequestCd())
184                 && !filter.getActionRequestCd().equals(KewApiConstants.ALL_CODE)) {
185             if (filter.isExcludeActionRequestCd()) {
186                 crit.add( notEqual("actionRequestCd", filter.getActionRequestCd()));
187             } else {
188                 crit.add( equal("actionRequestCd", filter.getActionRequestCd()));
189             }
190             filteredByItems.add( "Action Requested" );
191         }
192     }
193 
194     protected void handleDateCriteria( String propertyPath, String filterLabel, Date fromDate, Date toDate, boolean excludeDates, Collection<Predicate> crit, List<String> filteredByItems ) {
195         if (fromDate != null || toDate != null) {
196             Timestamp fromDateTimestamp = beginningOfDay(fromDate);
197             Timestamp toDateTimestamp = endOfDay(toDate);
198             if (excludeDates) {
199                 if (fromDate != null && toDate != null) {
200                     crit.add( notBetween(propertyPath, fromDateTimestamp, toDateTimestamp ) );
201                 } else if (fromDate != null && toDate == null) {
202                     crit.add( lessThanOrEqual(propertyPath, fromDateTimestamp ) );
203                 } else if (fromDate == null && toDate != null) {
204                     crit.add( greaterThanOrEqual(propertyPath, toDateTimestamp ) );
205                 }
206             } else {
207                 if (fromDate != null && toDate != null) {
208                     crit.add( between(propertyPath, fromDateTimestamp, toDateTimestamp ) );
209                 } else if (fromDate != null && toDate == null) {
210                     crit.add( greaterThanOrEqual(propertyPath, fromDateTimestamp ) );
211                 } else if (fromDate == null && toDate != null) {
212                     crit.add( lessThanOrEqual(propertyPath, toDateTimestamp ) );
213                 }
214             }
215             filteredByItems.add("Date Created");
216         }
217     }
218 
219     protected void handleDocumentCreateDateCriteria( ActionListFilter filter, Collection<Predicate> crit, List<String> filteredByItems ) {
220         handleDateCriteria("routeHeader.createDate", "Date Created", filter.getCreateDateFrom(), filter.getCreateDateTo(), filter.isExcludeCreateDate(), crit, filteredByItems);
221     }
222 
223     protected void handleAssignedDateCriteria( ActionListFilter filter, Collection<Predicate> crit, List<String> filteredByItems ) {
224         handleDateCriteria("dateAssigned", "Date Last Assigned", filter.getLastAssignedDateFrom(), filter.getLastAssignedDateTo(), filter.isExcludeLastAssignedDate(), crit, filteredByItems);
225     }
226 
227     protected void handleRouteStatusCriteria( ActionListFilter filter, Collection<Predicate> crit, List<String> filteredByItems ) {
228         if ( StringUtils.isNotBlank(filter.getDocRouteStatus())
229                 && !filter.getDocRouteStatus().equals(KewApiConstants.ALL_CODE)) {
230             if (filter.isExcludeRouteStatus()) {
231                 crit.add( notEqual("routeHeader.docRouteStatus", filter.getDocRouteStatus() ) );
232             } else {
233                 crit.add( equal("routeHeader.docRouteStatus", filter.getDocRouteStatus() ) );
234             }
235             filteredByItems.add( "Document Route Status" );
236         }
237     }
238 
239     protected void handleDocumentTitleCriteria( ActionListFilter filter, Collection<Predicate> crit, List<String> filteredByItems ) {
240         if ( StringUtils.isNotBlank(filter.getDocumentTitle()) ) {
241             String docTitle = filter.getDocumentTitle().trim();
242             if (docTitle.endsWith("*")) {
243                 docTitle = docTitle.substring(0, docTitle.length() - 1);
244             }
245             if (filter.isExcludeDocumentTitle()) {
246                 crit.add( notLike("docTitle", "%" + docTitle + "%" ) );
247             } else {
248                 crit.add( like("docTitle", "%" + docTitle + "%" ) );
249             }
250             filteredByItems.add( "Document Title" );
251         }
252     }
253 
254     protected void handleDocumentTypeCriteria( ActionListFilter filter, Collection<Predicate> crit, List<String> filteredByItems ) {
255         if ( StringUtils.isNotBlank(filter.getDocumentType()) ) {
256             String documentTypeName = filter.getDocumentType();
257             if (filter.isExcludeDocumentType()) {
258                 crit.add( notLike( "docName", "%" + documentTypeName + "%" ) );
259             } else {
260                 DocumentType documentType = documentTypeService.findByName(documentTypeName);
261                 // not an exact document type - just use it as is
262                 if (documentType == null) {
263                     crit.add( like( "docName", "%" + documentTypeName + "%" ) );
264                 } else {
265 
266                     Collection<DocumentType> docs = getAllChildDocumentTypes(documentType);
267                     Collection<String> docNames = new ArrayList<String>(docs.size()+1);
268                     docNames.add(documentType.getName());
269                     for ( DocumentType doc : docs ) {
270                         docNames.add(doc.getName());
271                     }
272                     crit.add( in("docName", docNames) );
273                 }
274             }
275             filteredByItems.add( "Document Type" );
276         }
277     }
278 
279     protected Collection<DocumentType> getAllChildDocumentTypes( DocumentType docType ) {
280         Collection<DocumentType> allChildren = new ArrayList<DocumentType>();
281 
282         List<DocumentType> immediateChildren = documentTypeService.getChildDocumentTypes(docType.getId());
283         if ( immediateChildren != null ) {
284             allChildren.addAll(immediateChildren);
285 
286             for ( DocumentType childDoc : immediateChildren ) {
287                 allChildren.addAll( getAllChildDocumentTypes(childDoc));
288             }
289         }
290 
291         return allChildren;
292     }
293 
294     protected void handleWorkgroupCriteria( ActionListFilter filter, Collection<Predicate> crit, List<String> filteredByItems ) {
295         filter.setGroupId(null);
296         if ( StringUtils.isNotBlank(filter.getGroupIdString())
297                 && !filter.getGroupIdString().trim().equals(KewApiConstants.NO_FILTERING)) {
298 
299             filter.setGroupId(filter.getGroupIdString().trim());
300 
301             if (filter.isExcludeGroupId()) {
302                 crit.add( or(
303                         notEqual("groupId", filter.getGroupId()),
304                         isNull("groupId") ) );
305             } else {
306                 crit.add( equal("groupId", filter.getGroupId()) );
307             }
308             filteredByItems.add( "Action Request Workgroup" );
309         }
310     }
311 
312     protected void applyPrimaryDelegationCriteria( String actionListUserPrincipalId, ActionListFilter filter, Collection<Predicate> crit, List<String> filteredByItems ) {
313         // get the groups the user is a part of
314         List<String> delegatorGroupIds = KimApiServiceLocator.getGroupService().getGroupIdsByPrincipalId(actionListUserPrincipalId);
315         // add filter for requests where the current user was the primary delegator
316         if (delegatorGroupIds != null && !delegatorGroupIds.isEmpty()) {
317             crit.add( or( equal("delegatorPrincipalId", actionListUserPrincipalId), in("delegatorGroupId", delegatorGroupIds) ) );
318         } else {
319             crit.add( equal("delegatorPrincipalId", actionListUserPrincipalId) );
320         }
321         crit.add( equal("delegationType", DelegationType.PRIMARY.getCode() ) );
322         filter.setDelegationType(DelegationType.PRIMARY.getCode());
323         filter.setExcludeDelegationType(false);
324         filteredByItems.add("Primary Delegator Id");
325     }
326 
327     /**
328      * Apply criteria related to primary delegations.
329      *
330      * Called only after detecting that the user is filtering on primary validations.
331      *
332      * @return <b>true</b> if any criteria were applied, <b>false</b> otherwise
333      */
334     protected boolean handlePrimaryDelegation( String actionListUserPrincipalId, ActionListFilter filter, Collection<Predicate> crit, List<String> filteredByItems ) {
335         if ( StringUtils.isBlank(filter.getPrimaryDelegateId())
336                 || filter.getPrimaryDelegateId().trim().equals(KewApiConstants.ALL_CODE) ) {
337             // user wishes to see all primary delegations
338             applyPrimaryDelegationCriteria(actionListUserPrincipalId, filter, crit, filteredByItems);
339 
340             return true;
341         } else if (!filter.getPrimaryDelegateId().trim().equals(KewApiConstants.PRIMARY_DELEGATION_DEFAULT)) {
342             // user wishes to see primary delegation for a single user
343             crit.add( equal("principalId", filter.getPrimaryDelegateId() ) );
344             applyPrimaryDelegationCriteria(actionListUserPrincipalId, filter, crit, filteredByItems);
345 
346             return true;
347         }
348 
349         return false;
350     }
351 
352     /**
353      * Apply criteria related to secondary delegations.
354      *
355      * Called only after detecting that the user is filtering on secondary validations.
356      *
357      * @return <b>true</b> if any criteria were applied, <b>false</b> otherwise
358      */
359     protected boolean handleSecondaryDelegation( String actionListUserPrincipalId, ActionListFilter filter, Collection<Predicate> crit, List<String> filteredByItems ) {
360         crit.add( equal("principalId", actionListUserPrincipalId) );
361         if (StringUtils.isBlank(filter.getDelegatorId())) {
362             filter.setDelegationType(DelegationType.SECONDARY.getCode());
363 
364             // if isExcludeDelegationType() we want to show the default action list which is set up later in this method
365             if (!filter.isExcludeDelegationType()) {
366                 crit.add( equal("delegationType", DelegationType.SECONDARY.getCode() ) );
367                 filteredByItems.add("Secondary Delegator Id");
368 
369                 return true;
370             }
371         } else if (filter.getDelegatorId().trim().equals(KewApiConstants.ALL_CODE)) {
372             // user wishes to see all secondary delegations
373             crit.add( equal("delegationType", DelegationType.SECONDARY.getCode() ) );
374             filter.setDelegationType(DelegationType.SECONDARY.getCode());
375             filter.setExcludeDelegationType(false);
376             filteredByItems.add("Secondary Delegator Id");
377 
378             return true;
379         } else if (!filter.getDelegatorId().trim().equals(KewApiConstants.DELEGATION_DEFAULT)) {
380             // user has specified an id to see for secondary delegation
381             filter.setDelegationType(DelegationType.SECONDARY.getCode());
382             filter.setExcludeDelegationType(false);
383 
384             if (filter.isExcludeDelegatorId()) {
385                 crit.add( or( notEqual("delegatorPrincipalId", filter.getDelegatorId()), isNull("delegatorPrincipalId") ) );
386                 crit.add( or( notEqual("delegatorGroupId", filter.getDelegatorId()), isNull("delegatorGroupId") ) );
387             } else {
388                 crit.add( or( equal("delegatorPrincipalId", filter.getDelegatorId()), equal("delegatorGroupId", filter.getDelegatorId()) ) );
389             }
390             filteredByItems.add("Secondary Delegator Id");
391 
392             return true;
393         }
394 
395         return false;
396     }
397 
398     /**
399      * Handle the general recipient criteria (user, delegate)
400      *
401      * @param actionListUserPrincipalId
402      * @param filter
403      * @param crit
404      * @param filteredByItems
405      */
406     protected void handleRecipientCriteria( String actionListUserPrincipalId, ActionListFilter filter, Collection<Predicate> crit, List<String> filteredByItems ) {
407         if (StringUtils.isBlank(filter.getDelegationType())
408                 && StringUtils.isBlank(filter.getPrimaryDelegateId())
409                 && StringUtils.isBlank(filter.getDelegatorId())) {
410             crit.add( equal("principalId", actionListUserPrincipalId) );
411             return;
412         }
413         if ( StringUtils.equals(filter.getDelegationType(), DelegationType.PRIMARY.getCode() )
414                 || StringUtils.isNotBlank(filter.getPrimaryDelegateId())) {
415             // using a primary delegation
416             if ( handlePrimaryDelegation(actionListUserPrincipalId, filter, crit, filteredByItems)) {
417                 return;
418             }
419         }
420 
421         if (StringUtils.equals(filter.getDelegationType(), DelegationType.SECONDARY.getCode())
422                 || StringUtils.isNotBlank(filter.getDelegatorId()) ) {
423             // using a secondary delegation
424             if ( handleSecondaryDelegation(actionListUserPrincipalId, filter, crit, filteredByItems) ) {
425                 return;
426             }
427         }
428 
429         // if we haven't added delegation criteria above then use the default criteria below
430         filter.setDelegationType(DelegationType.SECONDARY.getCode());
431         filter.setExcludeDelegationType(true);
432         crit.add( equal("principalId", actionListUserPrincipalId) );
433         crit.add( or( notEqual("delegationType", DelegationType.SECONDARY.getCode()), isNull("delegationType") ) );
434     }
435 
436     /**
437      * Creates an Action List from the given collection of Action Items.  The Action List should
438      * contain only one action item per document.  The action item chosen should be the most "critical"
439      * or "important" one on the document.
440      *
441      * @return the Action List as a Collection of ActionItems
442      */
443     private Collection<ActionItem> createActionListForUser(Collection<ActionItem> actionItems) {
444         Map<String, ActionItem> actionItemMap = new HashMap<String, ActionItem>();
445         ActionListPriorityComparator comparator = new ActionListPriorityComparator();
446         for (ActionItem potentialActionItem: actionItems) {
447             ActionItem existingActionItem = actionItemMap.get(potentialActionItem.getDocumentId());
448             if (existingActionItem == null || comparator.compare(potentialActionItem, existingActionItem) > 0) {
449                 actionItemMap.put(potentialActionItem.getDocumentId(), potentialActionItem);
450             }
451         }
452         return actionItemMap.values();
453     }
454 
455 
456     /**
457      * {@inheritDoc}
458      */
459     @Override
460     public Collection<ActionItem> getActionListForSingleDocument(String documentId) {
461         if ( LOG.isDebugEnabled() ) {
462             LOG.debug("getting action list for document id " + documentId);
463         }
464         Collection<ActionItem> collection = findByDocumentId(documentId);
465         if ( LOG.isDebugEnabled() ) {
466             LOG.debug("found " + collection.size() + " action items for document id " + documentId);
467         }
468         return createActionListForRouteHeader(collection);
469     }
470 
471     /**
472      * Creates an Action List from the given collection of Action Items.  The Action List should
473      * contain only one action item per user.  The action item chosen should be the most "critical"
474      * or "important" one on the document.
475      *
476      * @return the Action List as a Collection of ActionItems
477      */
478     protected Collection<ActionItem> createActionListForRouteHeader(Collection<ActionItem> actionItems) {
479         Map<String, ActionItem> actionItemMap = new HashMap<String, ActionItem>();
480         ActionListPriorityComparator comparator = new ActionListPriorityComparator();
481         for (ActionItem potentialActionItem: actionItems) {
482             ActionItem existingActionItem = actionItemMap.get(potentialActionItem.getPrincipalId());
483             if (existingActionItem == null || comparator.compare(potentialActionItem, existingActionItem) > 0) {
484                 actionItemMap.put(potentialActionItem.getPrincipalId(), potentialActionItem);
485             }
486         }
487         return actionItemMap.values();
488     }
489 
490     public void setActionListDAO(ActionListDAO actionListDAO) {
491         this.actionListDAO = actionListDAO;
492     }
493 
494     public ActionListDAO getActionListDAO() {
495         return actionListDAO;
496     }
497 
498     @Override
499     public void deleteActionItemNoOutbox(ActionItem actionItem) {
500         deleteActionItem(actionItem, false, false);
501     }
502 
503     @Override
504     public void deleteActionItem(ActionItem actionItem) {
505         deleteActionItem(actionItem, false);
506     }
507 
508     @Override
509     public void deleteActionItem(ActionItem actionItem, boolean forceIntoOutbox) {
510         deleteActionItem(actionItem, forceIntoOutbox, true);
511     }
512 
513     protected void deleteActionItem(ActionItem actionItem, boolean forceIntoOutbox, boolean putInOutbox) {
514         dataObjectService.delete(actionItem);
515         // remove notification from KCB
516         notificationService.removeNotification(Collections.singletonList(ActionItem.to(actionItem)));
517         if (putInOutbox) {
518             saveOutboxItem(actionItem, forceIntoOutbox);
519         }
520     }
521 
522     @Override
523     public void deleteByDocumentId(String documentId) {
524         dataObjectService.deleteMatching(ActionItem.class, QueryByCriteria.Builder.forAttribute("documentId", documentId).build());
525     }
526 
527     @Override
528     public Collection<ActionItem> findByDocumentId(String documentId) {
529         QueryResults<ActionItem> results = dataObjectService.findMatching(ActionItem.class,
530                 QueryByCriteria.Builder.forAttribute("documentId", documentId).build());
531 
532         return results.getResults();
533     }
534 
535     @Override
536     public Collection<ActionItem> findByActionRequestId(String actionRequestId) {
537         QueryResults<ActionItem> results = dataObjectService.findMatching(ActionItem.class,
538                 QueryByCriteria.Builder.forAttribute("actionRequestId", actionRequestId).build());
539 
540         return results.getResults();
541     }
542 
543     @Override
544     public Collection<ActionItem> findByWorkflowUserDocumentId(String workflowUserId, String documentId) {
545         Map<String,String> criteria = new HashMap<String, String>(2);
546         criteria.put( "principalId", workflowUserId );
547         criteria.put( "documentId", documentId );
548         QueryResults<ActionItem> results = dataObjectService.findMatching(ActionItem.class,
549                 QueryByCriteria.Builder.andAttributes(criteria).build());
550 
551         return results.getResults();
552     }
553 
554     @Override
555     public Collection<ActionItem> findByDocumentTypeName(String documentTypeName) {
556         QueryResults<ActionItem> results = dataObjectService.findMatching(ActionItem.class,
557                 QueryByCriteria.Builder.forAttribute("docName", documentTypeName).build());
558 
559         return results.getResults();
560     }
561 
562     @Override
563     public ActionItem createActionItemForActionRequest(ActionRequestValue actionRequest) {
564         ActionItem actionItem = new ActionItem();
565 
566         DocumentRouteHeaderValue routeHeader = actionRequest.getRouteHeader();
567         DocumentType docType = routeHeader.getDocumentType();
568 
569         actionItem.setActionRequestCd(actionRequest.getActionRequested());
570         actionItem.setActionRequestId(actionRequest.getActionRequestId());
571         actionItem.setDocName(docType.getName());
572         actionItem.setRoleName(actionRequest.getQualifiedRoleName());
573         actionItem.setPrincipalId(actionRequest.getPrincipalId());
574         actionItem.setDocumentId(actionRequest.getDocumentId());
575         actionItem.setDateAssigned(new Timestamp(new Date().getTime()));
576         actionItem.setDocHandlerURL(docType.getResolvedDocumentHandlerUrl());
577         actionItem.setDocLabel(docType.getLabel());
578         actionItem.setDocTitle(routeHeader.getDocTitle());
579         actionItem.setGroupId(actionRequest.getGroupId());
580         actionItem.setResponsibilityId(actionRequest.getResponsibilityId());
581         actionItem.setDelegationType(actionRequest.getDelegationType());
582         actionItem.setRequestLabel(actionRequest.getRequestLabel());
583 
584         ActionRequestValue delegatorActionRequest = actionRequestService.findDelegatorRequest(actionRequest);
585         if (delegatorActionRequest != null) {
586             actionItem.setDelegatorPrincipalId(delegatorActionRequest.getPrincipalId());
587             actionItem.setDelegatorGroupId(delegatorActionRequest.getGroupId());
588         }
589 
590         return actionItem;
591     }
592 
593 
594     @Override
595     public void updateActionItemsForTitleChange(String documentId, String newTitle) {
596         Collection<ActionItem> items = findByDocumentId(documentId);
597         for ( ActionItem item : items ) {
598             item.setDocTitle(newTitle);
599             saveActionItem(item);
600         }
601     }
602 
603     @Override
604     public ActionItem saveActionItem(ActionItem actionItem) {
605         return saveActionItemBase(actionItem);
606     }
607 
608     @Override
609     public OutboxItem saveOutboxItem(OutboxItem outboxItem) {
610         return saveActionItemBase(outboxItem);
611     }
612 
613     protected <T extends ActionItemBase> T saveActionItemBase(T actionItemBase) {
614         if (actionItemBase.getDateAssigned() == null) {
615             actionItemBase.setDateAssigned(dateTimeService.getCurrentTimestamp());
616         }
617         return dataObjectService.save(actionItemBase);
618     }
619 
620     public GroupService getGroupService(){
621         return KimApiServiceLocator.getGroupService();
622     }
623 
624     @Override
625     public ActionItem findByActionItemId(String actionItemId) {
626         return dataObjectService.find(ActionItem.class, actionItemId);
627     }
628 
629     @Override
630     public int getCount(String principalId) {
631         return actionListDAO.getCount(principalId);
632     }
633 
634     /**
635      * {@inheritDoc}
636      */
637     @Override
638     public List<Object> getMaxActionItemDateAssignedAndCountForUser(String principalId) {
639         // KULRICE-12318 IU contribution, not sure if this is still needed with the JPA implementation
640         // as no result should cause a no result exception, going to add it to make sure.
641         List<Object> verifiedList = new ArrayList<Object>();
642         List<Object> maxDateAndUserCount =  getActionListDAO().getMaxActionItemDateAssignedAndCountForUser(principalId);
643 
644         verifiedList.add(0, verifyMaxActionItemDateAssigned(maxDateAndUserCount));
645         verifiedList.add(1, verifyCountForUser(maxDateAndUserCount));
646 
647         return verifiedList;
648     }
649 
650     /**
651      * Ensures the max action item date assigned is a valid {@link Timestamp} otherwise the current time stamp is
652      *  returned
653      * @param maxDateAndUserCount the list containing the max action item date assigned
654      * @return the max action item date assigned, or the current time stamp if one is not found
655      */
656     private Object verifyMaxActionItemDateAssigned(List<Object> maxDateAndUserCount) {
657         if (maxDateAndUserCount != null && maxDateAndUserCount.size() > 0 &&
658                 maxDateAndUserCount.get(0) != null && (maxDateAndUserCount.get(0) instanceof Timestamp)) {
659 
660             return maxDateAndUserCount.get(0);
661         } else {
662             return new Timestamp(new Date().getTime());
663         }
664     }
665 
666     /**
667      * Ensures the user count is valid, otherwise zero is returned.
668      * @param maxDateAndUserCount the list containing the user count
669      * @return the user count, or zero if the user count is not found
670      */
671     private Object verifyCountForUser(List<Object> maxDateAndUserCount) {
672         if (maxDateAndUserCount != null && maxDateAndUserCount.size() > 1 && maxDateAndUserCount.get(1) != null) {
673             return maxDateAndUserCount.get(1);
674         } else {
675             return Long.valueOf(0);
676         }
677     }
678 
679     /**
680      * {@inheritDoc}
681      */
682     @Override
683     public Collection<OutboxItem> getOutbox(String principalId, ActionListFilter filter) {
684         List<String> filteredByItems = new ArrayList<String>();
685 
686         List<Predicate> crit = handleActionItemCriteria(principalId, filter, filteredByItems);
687 
688         if ( LOG.isDebugEnabled() ) {
689             LOG.debug("running query to get outbox list for criteria " + crit);
690         }
691         QueryByCriteria.Builder query = QueryByCriteria.Builder.create(QueryByCriteria.Builder.fromPredicates(crit));
692         query.setMaxResults(getOutboxItemLimit());
693         QueryResults<OutboxItem> results = dataObjectService.findMatching(OutboxItem.class, query.build());
694         if ( LOG.isDebugEnabled() ) {
695             LOG.debug("found " + results.getResults().size() + " outbox items for user " + principalId);
696         }
697 
698         if ( filteredByItems.isEmpty() ) {
699             filter.setFilterOn(false);
700         } else {
701             filter.setFilterOn(true);
702         }
703         filter.setFilterLegend(StringUtils.join(filteredByItems, ", "));
704 
705         return results.getResults();
706     }
707 
708     /**
709      * Retrieves the outbox item limit from the parameter service, if the parameter is not found a default is returned.
710      * @return the outbox item limit.
711      */
712     private Integer getOutboxItemLimit() {
713         String fetchSizeParam =
714             CoreFrameworkServiceLocator.getParameterService().getParameterValueAsString(KewApiConstants.KEW_NAMESPACE,
715                 KRADConstants.DetailTypes.ACTION_LIST_DETAIL_TYPE, KewApiConstants.OUTBOX_ITEM_LIMIT);
716 
717         if(StringUtils.isNotBlank(fetchSizeParam)) {
718             return Integer.parseInt(fetchSizeParam);
719         } else {
720             return DEFAULT_OUTBOX_ITEM_LIMIT;
721         }
722     }
723 
724     /**
725      * {@inheritDoc}
726      */
727     @Override
728     public Collection<OutboxItem> getOutboxItemsByDocumentType(String documentTypeName) {
729         QueryResults<OutboxItem> results = dataObjectService.findMatching(OutboxItem.class,
730                 QueryByCriteria.Builder.forAttribute("docName", documentTypeName).build());
731 
732         return results.getResults();
733     }
734 
735     /**
736      * {@inheritDoc}
737      */
738     @Override
739     public void removeOutboxItems(String principalId, List<String> outboxItems) {
740         QueryByCriteria query = QueryByCriteria.Builder.fromPredicates(
741                 in("id", outboxItems));
742 
743         dataObjectService.deleteMatching(OutboxItem.class, query);
744     }
745 
746     @Override
747     public OutboxItem saveOutboxItem(ActionItem actionItem) {
748         return saveOutboxItem(actionItem, false);
749     }
750 
751     /**
752      *
753      * save the ouboxitem unless the document is saved or the user already has the item in their outbox.
754      *
755      * @see org.kuali.rice.kew.actionlist.service.ActionListService#saveOutboxItem(org.kuali.rice.kew.actionitem.ActionItem, boolean)
756      */
757     @Override
758     public OutboxItem saveOutboxItem(ActionItem actionItem, boolean forceIntoOutbox) {
759         Boolean isUsingOutBox = true;
760         List<UserOptions> options = userOptionsService.findByUserQualified(actionItem.getPrincipalId(), KewApiConstants.USE_OUT_BOX);
761         if (options == null || options.isEmpty()){
762             isUsingOutBox = true;
763         } else {
764             for ( UserOptions u : options ) {
765                 if ( !StringUtils.equals(u.getOptionVal(), "yes") ) {
766                     isUsingOutBox = false;
767                     break;
768                 }
769             }
770         }
771 
772         if (isUsingOutBox
773                 && ConfigContext.getCurrentContextConfig().getOutBoxOn()
774                 && getOutboxItemByDocumentIdUserId(actionItem.getDocumentId(), actionItem.getPrincipalId()) == null
775                 && !routeHeaderService.getRouteHeader(actionItem.getDocumentId()).getDocRouteStatus().equals(
776                 KewApiConstants.ROUTE_HEADER_SAVED_CD)) {
777 
778             // only create an outbox item if this user has taken action on the document
779             ActionRequestValue actionRequest = actionRequestService.findByActionRequestId(
780                     actionItem.getActionRequestId());
781             ActionTakenValue actionTaken = actionRequest.getActionTaken();
782             // if an action was taken...
783             if (forceIntoOutbox || (actionTaken != null && actionTaken.getPrincipalId().equals(actionItem.getPrincipalId()))) {
784                 return dataObjectService.save(new OutboxItem(actionItem));
785             }
786 
787         }
788         return null;
789     }
790 
791     protected OutboxItem getOutboxItemByDocumentIdUserId(String documentId, String principalId) {
792         Map<String,String> criteria = new HashMap<String, String>(2);
793         criteria.put( "principalId", principalId );
794         criteria.put( "documentId", documentId );
795         QueryResults<OutboxItem> results = dataObjectService.findMatching(OutboxItem.class,
796                 QueryByCriteria.Builder.andAttributes(criteria).build());
797         if ( results.getResults().isEmpty() ) {
798             return null;
799         }
800         return results.getResults().get(0);
801     }
802 
803     @Override
804     public Collection<ActionItem> findByPrincipalId(String principalId) {
805         QueryResults<ActionItem> results = dataObjectService.findMatching(ActionItem.class,
806                 QueryByCriteria.Builder.forAttribute("principalId", principalId)
807                         .setOrderByAscending("documentId").build());
808 
809         return results.getResults();
810     }
811 
812     /**
813      * {@inheritDoc}
814      */
815     @Override
816     public DocumentRouteHeaderValue getMinimalRouteHeader(String documentId) {
817         return actionListDAO.getMinimalRouteHeader(documentId);
818     }
819 
820     protected Timestamp beginningOfDay(Date date) {
821         if ( date == null ) {
822             return null;
823         }
824         Calendar cal = Calendar.getInstance();
825         cal.setTime(date);
826         cal.set(Calendar.HOUR_OF_DAY, 0);
827         cal.set(Calendar.MINUTE, 0);
828         cal.set(Calendar.SECOND, 0);
829         return new Timestamp( cal.getTimeInMillis() );
830     }
831 
832     protected Timestamp endOfDay(Date date) {
833         if ( date == null ) {
834             return null;
835         }
836         Calendar cal = Calendar.getInstance();
837         cal.setTime(date);
838         cal.set(Calendar.HOUR_OF_DAY, 23);
839         cal.set(Calendar.MINUTE, 59);
840         cal.set(Calendar.SECOND, 59);
841         return new Timestamp( cal.getTimeInMillis() );
842     }
843 
844     public void setDataObjectService(DataObjectService dataObjectService) {
845         this.dataObjectService = dataObjectService;
846     }
847 
848     public void setNotificationService(NotificationService notificationService) {
849         this.notificationService = notificationService;
850     }
851 
852     public void setDateTimeService(DateTimeService dateTimeService) {
853         this.dateTimeService = dateTimeService;
854     }
855 
856     public void setActionRequestService(ActionRequestService actionRequestService) {
857         this.actionRequestService = actionRequestService;
858     }
859 
860     public void setDocumentTypeService(DocumentTypeService documentTypeService) {
861         this.documentTypeService = documentTypeService;
862     }
863 
864     public void setUserOptionsService(UserOptionsService userOptionsService) {
865         this.userOptionsService = userOptionsService;
866     }
867 
868     public void setRouteHeaderService(RouteHeaderService routeHeaderService) {
869         this.routeHeaderService = routeHeaderService;
870     }
871 }