diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuter.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuter.java index 6715fa8550..cebad424b0 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuter.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuter.java @@ -42,6 +42,7 @@ import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.cmr.security.PersonService; import org.apache.commons.logging.Log; @@ -122,7 +123,7 @@ public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecute { StringBuilder sb = new StringBuilder(); - sb.append("TYPE:\"rma:dispositionAction\" + "); + sb.append("TYPE:\"rma:dispositionAction\" AND "); sb.append("(@rma\\:dispositionAction:("); boolean bFirst = true; @@ -164,68 +165,32 @@ public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecute if (dispositionActions != null && !dispositionActions.isEmpty()) { - // execute search - ResultSet results = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, - SearchService.LANGUAGE_FTS_ALFRESCO, getQuery()); - List resultNodes = results.getNodeRefs(); - results.close(); - - if (logger.isDebugEnabled()) + boolean hasMore = true; + int skipCount = 0; + while(hasMore) { - logger.debug("Processing " + resultNodes.size() + " nodes"); - } + SearchParameters params = new SearchParameters(); + params.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + params.setLanguage(SearchService.LANGUAGE_FTS_ALFRESCO); + params.setQuery(getQuery()); + params.setSkipCount(skipCount); - // process search results - for (NodeRef node : resultNodes) - { - final NodeRef currentNode = node; + // execute search + ResultSet results = searchService.query(params); + List resultNodes = results.getNodeRefs(); + hasMore = results.hasMore(); + skipCount += resultNodes.size(); // increase by page size + results.close(); - RetryingTransactionCallback processTranCB = new RetryingTransactionCallback() + if (logger.isDebugEnabled()) { - public Boolean execute() - { - final String dispAction = (String) nodeService.getProperty(currentNode, - RecordsManagementModel.PROP_DISPOSITION_ACTION); + logger.debug("Processing " + resultNodes.size() + " nodes"); + } - // Run disposition action - if (dispAction != null && dispositionActions.contains(dispAction)) - { - ChildAssociationRef parent = nodeService.getPrimaryParent(currentNode); - if (parent.getTypeQName().equals(RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION)) - { - Map props = new HashMap(1); - props.put(RMDispositionActionExecuterAbstractBase.PARAM_NO_ERROR_CHECK, - Boolean.FALSE); - - try - { - // execute disposition action - recordsManagementActionService.executeRecordsManagementAction( - parent.getParentRef(), dispAction, props); - - if (logger.isDebugEnabled()) - { - logger.debug("Processed action: " + dispAction + "on" + parent); - } - } - catch (AlfrescoRuntimeException exception) - { - if (logger.isDebugEnabled()) - { - logger.debug(exception); - } - } - } - } - - return Boolean.TRUE; - } - }; - - // if exists - if (nodeService.exists(currentNode)) + // process search results + for (NodeRef node : resultNodes) { - retryingTransactionHelper.doInTransaction(processTranCB); + executeAction(node); } } } @@ -241,6 +206,62 @@ public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecute } } + /** + * Helper method that executes a disposition action + * + * @param actionNode - the disposition action to execute + */ + private void executeAction(final NodeRef actionNode) + { + RetryingTransactionCallback processTranCB = new RetryingTransactionCallback() + { + public Boolean execute() + { + final String dispAction = (String) nodeService.getProperty(actionNode, + RecordsManagementModel.PROP_DISPOSITION_ACTION); + + // Run disposition action + if (dispAction != null && dispositionActions.contains(dispAction)) + { + ChildAssociationRef parent = nodeService.getPrimaryParent(actionNode); + if (parent.getTypeQName().equals(RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION)) + { + Map props = new HashMap(1); + props.put(RMDispositionActionExecuterAbstractBase.PARAM_NO_ERROR_CHECK, + Boolean.FALSE); + + try + { + // execute disposition action + recordsManagementActionService.executeRecordsManagementAction( + parent.getParentRef(), dispAction, props); + + if (logger.isDebugEnabled()) + { + logger.debug("Processed action: " + dispAction + "on" + parent); + } + } + catch (AlfrescoRuntimeException exception) + { + if (logger.isDebugEnabled()) + { + logger.debug(exception); + } + } + } + } + + return Boolean.TRUE; + } + }; + + // if exists + if (nodeService.exists(actionNode)) + { + retryingTransactionHelper.doInTransaction(processTranCB); + } + } + public PersonService getPersonService() { return personService; diff --git a/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuterUnitTest.java b/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuterUnitTest.java index f93bb7b2d6..537c03e9e3 100644 --- a/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuterUnitTest.java +++ b/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuterUnitTest.java @@ -29,17 +29,19 @@ package org.alfresco.module.org_alfresco_module_rm.job; import static org.alfresco.module.org_alfresco_module_rm.test.util.AlfMock.generateQName; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyMap; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.contains; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -48,13 +50,15 @@ import org.alfresco.module.org_alfresco_module_rm.test.util.BaseUnitTest; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.search.ResultSet; -import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.search.SearchParameters; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; /** * Disposition lifecycle job execution unit test. @@ -92,7 +96,8 @@ public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest executer.setDispositionActions(dispositionActions); // setup interactions - doReturn(mockedResultSet).when(mockedSearchService).query(eq(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE), eq(SearchService.LANGUAGE_FTS_ALFRESCO), anyString()); + doReturn(mockedResultSet).when(mockedSearchService).query(any(SearchParameters.class)); + when(mockedResultSet.hasMore()).thenReturn(false); } /** @@ -100,7 +105,9 @@ public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest */ private void verifyQuery() { - verify(mockedSearchService, times(1)).query(eq(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE), eq(SearchService.LANGUAGE_FTS_ALFRESCO), contains(QUERY)); + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(SearchParameters.class); + verify(mockedSearchService, times(1)).query(paramsCaptor.capture()); + assertTrue(paramsCaptor.getValue().getQuery().contains(QUERY)); verify(mockedResultSet, times(1)).getNodeRefs(); verify(mockedResultSet, times(1)).close(); } @@ -249,7 +256,64 @@ public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest { String actual = executer.getQuery(); - String expected = "TYPE:\"rma:dispositionAction\" + (@rma\\:dispositionAction:(\"cutoff\" OR \"retain\")) AND ISUNSET:\"rma:dispositionActionCompletedAt\" AND ( @rma\\:dispositionEventsEligible:true OR @rma\\:dispositionAsOf:[MIN TO NOW] ) "; + String expected = "TYPE:\"rma:dispositionAction\" AND " + + "(@rma\\:dispositionAction:(\"cutoff\" OR \"retain\")) " + + "AND ISUNSET:\"rma:dispositionActionCompletedAt\" " + + "AND ( @rma\\:dispositionEventsEligible:true OR @rma\\:dispositionAsOf:[MIN TO NOW] ) "; + assertEquals(expected, actual); } + + /** + * Given the maximum page of elements for search service is 2 + * and search service finds more than one page of elements + * When the job executer runs + * Then the executer retrieves both pages and iterates all elements + */ + @Test + public void testPagination() + { + final NodeRef node1 = generateNodeRef(); + final NodeRef node2 = generateNodeRef(); + final NodeRef node3 = generateNodeRef(); + final NodeRef node4 = generateNodeRef(); + + // mock the search service to return the right page + when(mockedSearchService.query(any(SearchParameters.class))).thenAnswer( + new Answer() + { + @Override + public ResultSet answer(InvocationOnMock invocation) + { + SearchParameters params = invocation.getArgumentAt(0, SearchParameters.class); + if (params.getSkipCount() == 0) + { + // mock first page + ResultSet result1 = mock(ResultSet.class); + when(result1.getNodeRefs()).thenReturn(Arrays.asList(node1, node2)); + when(result1.hasMore()).thenReturn(true); + return result1; + } + else if (params.getSkipCount() == 2) + { + // mock second page + ResultSet result2 = mock(ResultSet.class); + when(result2.getNodeRefs()).thenReturn(Arrays.asList(node3, node4)); + when(result2.hasMore()).thenReturn(false); + return result2; + } + throw new IndexOutOfBoundsException("Pagination did not stop after the second page!"); + } + }); + + // call the service + executer.executeImpl(); + + // check the loop iterated trough all the elements + verify(mockedNodeService).exists(node1); + verify(mockedNodeService).exists(node2); + verify(mockedNodeService).exists(node3); + verify(mockedNodeService).exists(node4); + verify(mockedSearchService, times(2)).query(any(SearchParameters.class)); + } }