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.service.impl;
017
018import org.apache.commons.collections.CollectionUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.core.api.config.property.ConfigContext;
021import org.kuali.rice.core.api.criteria.Predicate;
022import org.kuali.rice.core.api.criteria.QueryByCriteria;
023import org.kuali.rice.core.api.criteria.QueryResults;
024import org.kuali.rice.core.api.datetime.DateTimeService;
025import org.kuali.rice.core.api.delegation.DelegationType;
026import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
027import org.kuali.rice.kew.actionitem.ActionItem;
028import org.kuali.rice.kew.actionitem.ActionItemBase;
029import org.kuali.rice.kew.actionitem.OutboxItem;
030import org.kuali.rice.kew.actionlist.ActionListFilter;
031import org.kuali.rice.kew.actionlist.dao.ActionListDAO;
032import org.kuali.rice.kew.actionlist.dao.impl.ActionListPriorityComparator;
033import org.kuali.rice.kew.actionlist.service.ActionListService;
034import org.kuali.rice.kew.actionrequest.ActionRequestValue;
035import org.kuali.rice.kew.actionrequest.KimGroupRecipient;
036import org.kuali.rice.kew.actionrequest.Recipient;
037import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
038import org.kuali.rice.kew.actiontaken.ActionTakenValue;
039import org.kuali.rice.kew.api.KewApiConstants;
040import org.kuali.rice.kew.doctype.bo.DocumentType;
041import org.kuali.rice.kew.doctype.service.DocumentTypeService;
042import org.kuali.rice.kew.notification.service.NotificationService;
043import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
044import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
045import org.kuali.rice.kew.useroptions.UserOptions;
046import org.kuali.rice.kew.useroptions.UserOptionsService;
047import org.kuali.rice.kew.util.WebFriendlyRecipient;
048import org.kuali.rice.kim.api.group.GroupService;
049import org.kuali.rice.kim.api.services.KimApiServiceLocator;
050import org.kuali.rice.krad.data.DataObjectService;
051import org.kuali.rice.krad.util.KRADConstants;
052
053import java.sql.Timestamp;
054import java.util.ArrayList;
055import java.util.Calendar;
056import java.util.Collection;
057import java.util.Collections;
058import java.util.Date;
059import java.util.HashMap;
060import java.util.List;
061import java.util.Map;
062
063import static org.kuali.rice.core.api.criteria.PredicateFactory.*;
064
065/**
066 * Default implementation of the {@link ActionListService}.
067 *
068 * @author Kuali Rice Team (rice.collab@kuali.org)
069 */
070public class ActionListServiceImpl implements ActionListService {
071
072    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ActionListServiceImpl.class);
073    private static final Integer DEFAULT_OUTBOX_ITEM_LIMIT = Integer.valueOf(10000);
074
075    protected DataObjectService dataObjectService;
076    protected NotificationService notificationService;
077    protected DateTimeService dateTimeService;
078    protected ActionRequestService actionRequestService;
079    protected DocumentTypeService documentTypeService;
080    protected UserOptionsService userOptionsService;
081    protected RouteHeaderService routeHeaderService;
082
083    protected ActionListDAO actionListDAO;
084
085    @Override
086    public Collection<Recipient> findUserSecondaryDelegators(String principalId) {
087
088        QueryByCriteria query = QueryByCriteria.Builder.fromPredicates(
089                equal("principalId", principalId),
090                equal("delegationType", DelegationType.SECONDARY.getCode()),
091                or(isNotNull("delegatorPrincipalId"), isNotNull("delegatorGroupId")));
092
093        QueryResults<ActionItem> results = dataObjectService.findMatching(ActionItem.class, query);
094
095        Map<Object, Recipient> delegators = new HashMap<Object, Recipient>(results.getResults().size());
096
097        for ( ActionItem actionItem : results.getResults() ) {
098            String delegatorPrincipalId = actionItem.getDelegatorPrincipalId();
099            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}