Revert "ACS 11286 optmize FTS query and adding CMIS query mode"

This reverts commit 72c9e3d3a7.
This commit is contained in:
bsayan2
2026-05-14 09:29:17 +00:00
parent 72c9e3d3a7
commit b72314a0df
5 changed files with 143 additions and 532 deletions
@@ -74,8 +74,6 @@ rm.dispositionlifecycletrigger.cronexpression=0 0/5 * * * ?
#
rm.dispositionlifecycletrigger.batchsize=500
rm.dispositionlifecycletrigger.queryMode=
#
# Global RM notify of records due for review cron job expression
#
@@ -82,7 +82,6 @@
<property name="recordsManagementActionService" ref="recordsManagementActionService" />
<property name="freezeService" ref="freezeService"/>
<property name="batchSize" value="${rm.dispositionlifecycletrigger.batchsize}"/>
<property name="queryMode" value="${rm.dispositionlifecycletrigger.queryMode}"/>
</bean>
<bean id="scheduledDispositionLifecyceleSchedulerAccessor" class="org.alfresco.schedule.AlfrescoSchedulerAccessorBean">
@@ -31,17 +31,11 @@ import static org.alfresco.module.org_alfresco_module_rm.action.RMDispositionAct
import java.io.Serializable;
import java.util.ArrayList;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService;
@@ -56,8 +50,10 @@ import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.PersonService;
/**
* The Disposition Lifecycle Job Finds all disposition action nodes which are for disposition actions specified Where asOf &gt; now OR dispositionEventsEligible = true; Runs the cut off or retain action for eligible records.
* The Disposition Lifecycle Job Finds all disposition action nodes which are for disposition actions specified Where
* asOf &gt; now OR dispositionEventsEligible = true; Runs the cut off or retain action for eligible records.
*
* @author mrogers
* @author Roy Wetherall
@@ -66,29 +62,9 @@ import org.alfresco.service.cmr.security.PersonService;
public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecuter
{
public enum QueryMode
{
FTS_DEFAULT, CMIS;
public static QueryMode getQueryMode(String queryMode)
{
if (StringUtils.isBlank(queryMode))
{
return FTS_DEFAULT;
}
String normalizedQueryMode = queryMode.trim();
return EnumSet.allOf(QueryMode.class).stream()
.filter(mode -> mode.name().equalsIgnoreCase(normalizedQueryMode))
.findFirst()
.orElse(FTS_DEFAULT);
}
}
/** batching properties */
private int batchSize;
public static final int DEFAULT_BATCH_SIZE = 500;
private static final int CMIS_QUERY_LIMIT = 100000;
/** list of disposition actions to automatically execute */
private List<String> dispositionActions;
@@ -111,8 +87,6 @@ public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecute
/** freeze service */
private FreezeService freezeService;
private QueryMode queryMode = QueryMode.FTS_DEFAULT;
/**
* @param freezeService freeze service
*/
@@ -124,8 +98,7 @@ public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecute
/**
* List of disposition actions to automatically execute when eligible.
*
* @param dispositionActions
* disposition actions
* @param dispositionActions disposition actions
*/
public void setDispositionActions(List<String> dispositionActions)
{
@@ -138,8 +111,7 @@ public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecute
}
/**
* @param recordsManagementActionService
* records management action service
* @param recordsManagementActionService records management action service
*/
public void setRecordsManagementActionService(RecordsManagementActionService recordsManagementActionService)
{
@@ -147,8 +119,7 @@ public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecute
}
/**
* @param nodeService
* node service
* @param nodeService node service
*/
public void setNodeService(NodeService nodeService)
{
@@ -156,66 +127,53 @@ public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecute
}
/**
* @param searchService
* search service
* @param searchService search service
*/
public void setSearchService(SearchService searchService)
{
this.searchService = searchService;
}
private String getActionFilterQuery()
{
String actionFilterQuery = null;
StringBuilder sb = new StringBuilder("@rma\\:dispositionAction:(");
boolean first = true;
for (String dispositionAction : dispositionActions)
{
if (!first)
{
sb.append(" OR ");
}
sb.append("\"").append(dispositionAction).append("\"");
first = false;
}
if (dispositionActions.size() > 1)
{
actionFilterQuery = sb.append(")").toString();
}
return actionFilterQuery;
}
/**
* Builds a transactional CMIS query for eligible disposition action nodes. The date cutoff is evaluated at call time so each job run uses the current date.
* Get the search query string.
*
* @return CMIS SQL string
* @return job query string
*/
public String getCmisQuery()
protected String getQuery()
{
// Use end-of-current-day in UTC so that records due today are always included
// regardless of what time within the day the job fires.
String cutoff = LocalDate.now(ZoneOffset.UTC) + "T23:59:59.999Z";
StringBuilder sb = new StringBuilder("SELECT * FROM rma:dispositionAction WHERE ");
sb.append("rma:dispositionAction IN (");
boolean first = true;
for (String action : dispositionActions)
if (query == null)
{
if (!first)
{
sb.append(",");
}
sb.append("'").append(action).append("'");
first = false;
}
sb.append(") ");
sb.append("AND rma:dispositionActionCompletedAt IS NULL ");
sb.append("AND (rma:dispositionEventsEligible = true ");
sb.append("OR rma:dispositionAsOf <= TIMESTAMP '").append(cutoff).append("')");
StringBuilder sb = new StringBuilder();
log.debug("Constructed CMIS query: {}", sb);
return sb.toString();
sb.append("TYPE:\"rma:dispositionAction\" AND ");
sb.append("(@rma\\:dispositionAction:(");
boolean bFirst = true;
for (String dispositionAction : dispositionActions)
{
if (bFirst)
{
bFirst = false;
}
else
{
sb.append(" OR ");
}
sb.append("\"").append(dispositionAction).append("\"");
}
sb.append("))");
sb.append(" AND ISUNSET:\"rma:dispositionActionCompletedAt\" ");
sb.append(" AND ( ");
sb.append("@rma\\:dispositionEventsEligible:true ");
sb.append("OR @rma\\:dispositionAsOf:[MIN TO NOW] ");
sb.append(") ");
query = sb.toString();
}
return query;
}
/**
@@ -223,25 +181,10 @@ public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecute
*/
@Override
public void executeImpl()
{
if (queryMode == QueryMode.CMIS)
{
executeImplCmis();
}
else
{
executeImplFts();
}
}
/**
* FTS/index-based execution path (default). Paginates through search results using skipCount, relying on the search index (Elasticsearch or Solr) to resolve eligible nodes.
*/
private void executeImplFts()
{
try
{
log.debug("Job Starting (FTS mode)");
log.debug("Job Starting");
if (dispositionActions == null || dispositionActions.isEmpty())
{
@@ -251,300 +194,103 @@ public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecute
boolean hasMore = true;
int skipCount = 0;
List<NodeRef> resultNodes = new ArrayList<>();
if (batchSize < 1)
{
log.debug("Invalid value for batch size: {} default value used instead.", batchSize);
log.debug("Invalid value for batch size: " + batchSize + " default value used instead.");
batchSize = DEFAULT_BATCH_SIZE;
}
log.trace("Using batch size of {}", batchSize);
int batchNumber = 0;
int totalReturned = 0;
int totalEligible = 0;
int totalProcessed = 0;
log.trace("Using batch size of " + batchSize);
while (hasMore)
{
batchNumber++;
SearchParameters params = new SearchParameters();
params.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
params.setLanguage(SearchService.LANGUAGE_FTS_ALFRESCO);
params.setQuery(getQuery());
params.setSkipCount(skipCount);
params.setMaxItems(batchSize);
// set query
params.setQuery("TYPE:\"rma:dispositionAction\"");
params.addFilterQuery(getActionFilterQuery());
params.addFilterQuery("ISUNSET:\"rma:dispositionActionCompletedAt\"");
params.addFilterQuery("(@rma\\:dispositionEventsEligible:true OR -@rma\\:dispositionAsOf:["
+ LocalDate.now().plusDays(1) + " TO MAX])");
params.setTrackScore(false);
// execute search
ResultSet results = executeSearch(params, batchNumber);
if (results == null)
ResultSet results = searchService.query(params);
if(results != null)
{
log.warn("Disposition lifecycle search returned null; stopping pagination.");
break;
// filtering out the hold/freezed cases from the result set
resultNodes =
results.getNodeRefs().stream().filter(node -> nodeService.getPrimaryParent(node) == null ?
!freezeService.isFrozenOrHasFrozenChildren(node) :
!freezeService.isFrozenOrHasFrozenChildren(nodeService.getPrimaryParent(node).getParentRef())).collect(Collectors.toList());
}
// Declared outside try so it remains in scope after results.close().
Map<NodeRef, ChildAssociationRef> eligibleNodes;
int rawPageLength;
hasMore = results.hasMore();
skipCount += resultNodes.size(); // increase by page size
results.close();
log.debug("Processing " + resultNodes.size() + " nodes");
// process search results
if (!resultNodes.isEmpty())
{
executeAction(resultNodes);
}
}
log.debug("Job Finished");
}
catch (AlfrescoRuntimeException exception)
{
log.debug(exception.getMessage());
}
}
/**
* Helper method that executes a disposition action
*
* @param actionNodes - the disposition actions to execute
*/
private void executeAction(final List<NodeRef> actionNodes)
{
RetryingTransactionCallback<Boolean> processTranCB = () -> {
for (NodeRef actionNode : actionNodes)
{
if (!nodeService.exists(actionNode))
{
continue;
}
final String dispAction = (String) nodeService.getProperty(actionNode, PROP_DISPOSITION_ACTION);
// Run disposition action
if (dispAction == null || !dispositionActions.contains(dispAction))
{
continue;
}
ChildAssociationRef parent = nodeService.getPrimaryParent(actionNode);
if (!parent.getTypeQName().equals(ASSOC_NEXT_DISPOSITION_ACTION))
{
continue;
}
Map<String, Serializable> props = Map.of(PARAM_NO_ERROR_CHECK, false);
try
{
List<NodeRef> rawPage = results.getNodeRefs();
// Advance skip by raw hit count so paging stays aligned with the index; post-search
// freeze filtering must not shrink the skip step (would duplicate or skip hits).
rawPageLength = rawPage.size();
hasMore = results.hasMore();
skipCount += rawPageLength;
totalReturned += rawPageLength;
// Single getPrimaryParent call per node: result is carried forward so
// executeAction can reuse it without a second DB round-trip.
eligibleNodes = new LinkedHashMap<>(rawPageLength);
for (NodeRef node : rawPage)
{
ChildAssociationRef parent = nodeService.getPrimaryParent(node);
NodeRef freezeTarget = parent != null ? parent.getParentRef() : node;
if (!freezeService.isFrozenOrHasFrozenChildren(freezeTarget))
{
eligibleNodes.put(node, parent);
}
}
}
finally
{
results.close();
}
int batchEligible = eligibleNodes.size();
totalEligible += batchEligible;
log.debug("Batch {} — returned: {}, eligible (not frozen): {}, hasMore: {}",
batchNumber, rawPageLength, batchEligible, hasMore);
int batchProcessed = 0;
if (!eligibleNodes.isEmpty())
{
batchProcessed = retryingTransactionHelper.doInTransaction(() -> executeAction(eligibleNodes), false, true);
}
totalProcessed += batchProcessed;
log.debug("Batch {} — actioned: {}", batchNumber, batchProcessed);
}
log.debug("Job Finished (FTS mode) — batches: {}, total returned by search: {}, total eligible: {}, total actioned: {}",
batchNumber, totalReturned, totalEligible, totalProcessed);
}
catch (AlfrescoRuntimeException exception)
{
log.debug(exception.getMessage());
}
}
/**
* CMIS/DB-based execution path (enabled by {@code queryMode=CMIS}).
*/
private void executeImplCmis()
{
try
{
log.debug("Job Starting (CMIS/DB mode)");
if (dispositionActions == null || dispositionActions.isEmpty())
{
log.debug("Job Finished as disposition action is empty");
return;
}
if (batchSize < 1)
{
log.debug("Invalid value for batch size: {} default value used instead.", batchSize);
batchSize = DEFAULT_BATCH_SIZE;
}
log.trace("Using batch size of {}", batchSize);
int skipCount = 0;
int batchNumber = 0;
int totalReturned = 0;
int totalEligible = 0;
int totalProcessed = 0;
while (totalReturned < CMIS_QUERY_LIMIT)
{
batchNumber++;
int[] batch = runCmisBatch(skipCount, batchNumber);
int rawPageSize = batch[0];
int eligible = batch[1];
int processed = batch[2];
if (rawPageSize == 0)
{
log.debug("No more eligible nodes found; job complete.");
break;
}
totalReturned += rawPageSize;
totalEligible += eligible;
totalProcessed += processed;
if (processed > 0)
{
// Actioned nodes have dispositionActionCompletedAt set and will not reappear.
// Reset skip so the next query starts from position 0 and picks up any nodes
// that shifted into earlier positions as processed ones were removed.
skipCount = 0;
}
else
{
// The entire batch was unprocessable (frozen, non-existent, or malformed data).
// Advance skip past these nodes to avoid fetching the same stuck batch forever.
log.warn("No nodes processed from a batch of {}; advancing skip by {} to bypass unprocessable records.", rawPageSize, rawPageSize);
skipCount += rawPageSize;
}
}
log.debug("Job Finished (CMIS/DB mode) — batches: {}, total returned by query: {}, total eligible: {}, total actioned: {}",
batchNumber, totalReturned, totalEligible, totalProcessed);
}
catch (AlfrescoRuntimeException exception)
{
log.debug(exception.getMessage());
}
}
/**
* Runs one CMIS batch — search, freeze filter, and action execution — inside a single transaction.
*
* @param skipCount
* current pagination offset
* @param batchNumber
* 1-based batch index used in log messages
* @return int[3] — {rawPageSize, eligible, processed}; all zeros when no results found
*/
private int[] runCmisBatch(int skipCount, int batchNumber)
{
return retryingTransactionHelper.doInTransaction(() -> {
SearchParameters params = new SearchParameters();
params.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
params.setLanguage(SearchService.LANGUAGE_CMIS_ALFRESCO);
params.setQuery(getCmisQuery());
params.setMaxItems(batchSize);
params.setSkipCount(skipCount);
// Force DB routing. CMIS goes to the DB by default but being explicit here
// guards against any future routing change that could silently reintroduce
// the async-indexing lag and the need for skip arithmetic.
params.setQueryConsistency(QueryConsistency.TRANSACTIONAL);
ResultSet results = executeSearch(params, batchNumber);
if (results == null)
{
log.warn("Disposition lifecycle CMIS query returned null; stopping.");
return new int[]{0, 0, 0};
}
try
{
List<NodeRef> rawPage = results.getNodeRefs();
int rawPageSize = rawPage.size();
if (rawPageSize == 0)
{
return new int[]{0, 0, 0};
}
Map<NodeRef, ChildAssociationRef> eligibleNodes = new LinkedHashMap<>(rawPageSize);
for (NodeRef node : rawPage)
{
ChildAssociationRef parent = nodeService.getPrimaryParent(node);
NodeRef freezeTarget = parent != null ? parent.getParentRef() : node;
if (!freezeService.isFrozenOrHasFrozenChildren(freezeTarget))
{
eligibleNodes.put(node, parent);
}
}
int eligible = eligibleNodes.size();
log.debug("Batch {} — returned: {}, eligible (not frozen): {}, skip: {}",
batchNumber, rawPageSize, eligible, skipCount);
int processed = eligibleNodes.isEmpty() ? 0 : executeAction(eligibleNodes);
log.debug("Batch {} — actioned: {}", batchNumber, processed);
return new int[]{rawPageSize, eligible, processed};
}
finally
{
results.close();
}
}, false, true); // readOnly=false, requiresNew=true
}
private ResultSet executeSearch(SearchParameters params, int batchNumber)
{
long start = System.currentTimeMillis();
ResultSet results = searchService.query(params);
long elapsed = System.currentTimeMillis() - start;
log.debug("Executed batch-{} search results in {} ms.", batchNumber, elapsed);
return results;
}
/**
* Executes the disposition action for each eligible node.
*
* <p>
* Returns the number of nodes for which the action was actually executed. Nodes discarded by the three internal guards (does not exist, wrong action, wrong parent association) are not counted. The caller uses this count to detect a stuck batch — a non-empty batch that yields zero processed nodes — and advance the skip offset accordingly.
* </p>
*
* @param eligibleNodes
* map of disposition action node to its pre-computed primary parent
* @return number of nodes for which the disposition action was successfully invoked
*/
private int executeAction(final Map<NodeRef, ChildAssociationRef> eligibleNodes)
{
int processedCount = 0;
for (Map.Entry<NodeRef, ChildAssociationRef> entry : eligibleNodes.entrySet())
{
NodeRef actionNode = entry.getKey();
// Reuse the parent computed during the freeze-filter pass — no extra DB call.
ChildAssociationRef parent = entry.getValue();
if (!nodeService.exists(actionNode))
{
continue;
}
final String dispAction = (String) nodeService.getProperty(actionNode, PROP_DISPOSITION_ACTION);
// Run disposition action
if (dispAction == null || !dispositionActions.contains(dispAction))
{
continue;
}
if (parent == null || !parent.getTypeQName().equals(ASSOC_NEXT_DISPOSITION_ACTION))
{
continue;
}
Map<String, Serializable> props = Map.of(PARAM_NO_ERROR_CHECK, false);
try
{
recordsManagementActionService
// execute disposition action
recordsManagementActionService
.executeRecordsManagementAction(parent.getParentRef(), dispAction, props);
processedCount++;
log.trace("Processed action: {} on {}", dispAction, parent);
log.debug("Processed action: " + dispAction + "on" + parent);
}
catch (AlfrescoRuntimeException exception)
{
log.debug(exception.getMessage());
}
}
catch (AlfrescoRuntimeException exception)
{
log.debug(exception.getMessage());
}
}
return processedCount;
return Boolean.TRUE;
};
retryingTransactionHelper.doInTransaction(processTranCB, false, true);
}
public PersonService getPersonService()
@@ -75,8 +75,8 @@ public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest
private static final int BATCH_SIZE = 1;
/** test query snippet */
private static final String QUERY= "TYPE:\"rma:dispositionAction\"";
private static final String FILTER_QUERY = "@rma\\:dispositionAction:(\"cutoff\" OR \"retain\")";
private static final String QUERY = "\"" + CUTOFF + "\" OR \"" + RETAIN + "\"";
/** mocked result set */
@Mock ResultSet mockedResultSet;
@@ -118,7 +118,6 @@ public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest
ArgumentCaptor<SearchParameters> paramsCaptor = ArgumentCaptor.forClass(SearchParameters.class);
verify(mockedSearchService, times(numberOfInvocation)).query(paramsCaptor.capture());
assertTrue(paramsCaptor.getValue().getQuery().contains(QUERY));
assertTrue(paramsCaptor.getValue().getFilterQueries().toString().contains(FILTER_QUERY));
verify(mockedResultSet, times(numberOfInvocation)).getNodeRefs();
verify(mockedResultSet, times(numberOfInvocation)).close();
}
@@ -147,6 +146,7 @@ public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest
/**
* When the disposition actions do not match those that can be processed automatically.
*/
@SuppressWarnings("unchecked")
@Test
public void dispositionActionDoesNotMatch()
{
@@ -174,10 +174,12 @@ public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest
// ensure the query is executed and closed
verifyQueryTimes(2);
// ensure node existence is checked for each result
// ensure work is executed in transaction for each node processed
verify(mockedNodeService, times(2)).exists(any(NodeRef.class));
verify(mockedRetryingTransactionHelper, times(2)).doInTransaction(any(RetryingTransactionCallback.class),
anyBoolean(), anyBoolean());
// ensure each node is processed correctly
// ensure each node is process correctly
verify(mockedNodeService, times(1)).getProperty(node1, RecordsManagementModel.PROP_DISPOSITION_ACTION);
verify(mockedNodeService, times(1)).getProperty(node2, RecordsManagementModel.PROP_DISPOSITION_ACTION);
@@ -219,6 +221,7 @@ public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest
/**
* When there are disposition actions eligible for processing
*/
@SuppressWarnings("unchecked")
@Test
public void dispositionActionProcessed()
{
@@ -251,17 +254,19 @@ public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest
// ensure the query is executed and closed
verifyQueryTimes(2);
// ensure work is executed for each node
// ensure work is executed in transaction for each node processed
verify(mockedNodeService, times(2)).exists(any(NodeRef.class));
verify(mockedRetryingTransactionHelper, times(2)).doInTransaction(any(RetryingTransactionCallback.class),
anyBoolean(), anyBoolean());
// ensure each node is processed correctly
// ensure each node is process correctly
// node1
verify(mockedNodeService, times(1)).getProperty(node1, RecordsManagementModel.PROP_DISPOSITION_ACTION);
verify(mockedNodeService, times(1)).getPrimaryParent(node1);
verify(mockedNodeService, times(3)).getPrimaryParent(node1);
verify(mockedRecordsManagementActionService, times(1)).executeRecordsManagementAction(eq(parent), eq(CUTOFF), anyMap());
// node2
verify(mockedNodeService, times(1)).getProperty(node2, RecordsManagementModel.PROP_DISPOSITION_ACTION);
verify(mockedNodeService, times(1)).getPrimaryParent(node2);
verify(mockedNodeService, times(3)).getPrimaryParent(node2);
verify(mockedRecordsManagementActionService, times(1)).executeRecordsManagementAction(eq(parent), eq(RETAIN), anyMap());
// ensure no more interactions
@@ -269,138 +274,20 @@ public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest
}
/**
* Verify that getCmisQuery() generates a valid CMIS query containing the required action filters and conditions.
* The query includes a dynamic UTC timestamp cutoff, so we verify key components rather than exact string match.
* Brittle unit test that simply checks the generated query is an exact string when the supplied disposition actions
* are "CUTOFF" and "RETAIN" (see {@link #before}).
*/
@Test
public void testGetCmisQuery()
public void testGetQuery()
{
String actual = executer.getCmisQuery();
String actual = executer.getQuery();
// Verify the CMIS query contains all required components
assertTrue("CMIS query should start with SELECT statement", actual.contains("SELECT * FROM rma:dispositionAction"));
assertTrue("CMIS query should filter by disposition actions", actual.contains("rma:dispositionAction IN ('cutoff','retain')"));
assertTrue("CMIS query should exclude completed actions", actual.contains("rma:dispositionActionCompletedAt IS NULL"));
assertTrue("CMIS query should check for eligible events", actual.contains("rma:dispositionEventsEligible = true"));
assertTrue("CMIS query should check asOf date with UTC timestamp", actual.contains("rma:dispositionAsOf <= TIMESTAMP"));
assertTrue("CMIS query should use UTC timezone (Z suffix)", actual.contains("T23:59:59.999Z"));
}
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] ) ";
/**
* CMIS mode: when there are no results the job finishes after a single query.
*/
@Test
public void cmisNoResultsInQuery()
{
executer.setQueryMode("CMIS");
doReturn(Collections.EMPTY_LIST).when(mockedResultSet).getNodeRefs();
executer.executeImpl();
verify(mockedSearchService, times(1)).query(any(SearchParameters.class));
verify(mockedResultSet, times(1)).getNodeRefs();
verify(mockedResultSet, times(1)).close();
verifyNoMoreInteractions(mockedNodeService, mockedRecordsManagementActionService);
}
/**
* CMIS mode: when a node no longer exists it is skipped, no action is executed,
* and the skip offset advances so the job does not loop on the same batch forever.
*/
@Test
public void cmisNodeDoesNotExist()
{
executer.setQueryMode("CMIS");
NodeRef node1 = generateNodeRef(null, false); // exists = false
ChildAssociationRef parentAssoc = new ChildAssociationRef(
ASSOC_NEXT_DISPOSITION_ACTION, generateNodeRef(), generateQName(), generateNodeRef());
doReturn(parentAssoc).when(mockedNodeService).getPrimaryParent(node1);
doReturn(false).when(mockedFreezeService).isFrozenOrHasFrozenChildren(any(NodeRef.class));
// first batch returns the non-existent node; second batch (after skip advance) is empty
when(mockedResultSet.getNodeRefs())
.thenReturn(Collections.singletonList(node1))
.thenReturn(Collections.emptyList());
executer.executeImpl();
// two queries: one for the real batch, one after skip advance that finds nothing
verify(mockedSearchService, times(2)).query(any(SearchParameters.class));
verify(mockedNodeService, times(1)).getPrimaryParent(node1);
verify(mockedNodeService, times(1)).exists(node1);
verifyNoMoreInteractions(mockedRecordsManagementActionService);
}
/**
* CMIS mode: when the disposition action on the node does not match any configured
* action the node is skipped and the skip offset advances.
*/
@Test
public void cmisDispositionActionDoesNotMatch()
{
executer.setQueryMode("CMIS");
NodeRef node1 = generateNodeRef();
ChildAssociationRef parentAssoc = new ChildAssociationRef(
ASSOC_NEXT_DISPOSITION_ACTION, generateNodeRef(), generateQName(), generateNodeRef());
doReturn(DESTROY).when(mockedNodeService).getProperty(node1, RecordsManagementModel.PROP_DISPOSITION_ACTION);
doReturn(parentAssoc).when(mockedNodeService).getPrimaryParent(node1);
doReturn(false).when(mockedFreezeService).isFrozenOrHasFrozenChildren(any(NodeRef.class));
when(mockedResultSet.getNodeRefs())
.thenReturn(Collections.singletonList(node1))
.thenReturn(Collections.emptyList());
executer.executeImpl();
verify(mockedSearchService, times(2)).query(any(SearchParameters.class));
verify(mockedNodeService, times(1)).getPrimaryParent(node1);
verify(mockedNodeService, times(1)).exists(node1);
verify(mockedNodeService, times(1)).getProperty(node1, RecordsManagementModel.PROP_DISPOSITION_ACTION);
verifyNoMoreInteractions(mockedRecordsManagementActionService);
}
/**
* CMIS mode: when disposition actions are eligible they are processed, the skip offset
* is reset to zero after a successful batch, and the job continues until no more results.
*/
@Test
public void cmisDispositionActionProcessed()
{
executer.setQueryMode("CMIS");
NodeRef node1 = generateNodeRef();
NodeRef node2 = generateNodeRef();
NodeRef parent = generateNodeRef();
ChildAssociationRef parentAssoc = new ChildAssociationRef(
ASSOC_NEXT_DISPOSITION_ACTION, parent, generateQName(), generateNodeRef());
doReturn(CUTOFF).when(mockedNodeService).getProperty(node1, RecordsManagementModel.PROP_DISPOSITION_ACTION);
doReturn(RETAIN).when(mockedNodeService).getProperty(node2, RecordsManagementModel.PROP_DISPOSITION_ACTION);
doReturn(parentAssoc).when(mockedNodeService).getPrimaryParent(any(NodeRef.class));
doReturn(false).when(mockedFreezeService).isFrozenOrHasFrozenChildren(any(NodeRef.class));
// batch 1 → node1 processed → skip resets to 0
// batch 2 → node2 processed → skip resets to 0
// batch 3 → empty → loop stops
when(mockedResultSet.getNodeRefs())
.thenReturn(Collections.singletonList(node1))
.thenReturn(Collections.singletonList(node2))
.thenReturn(Collections.emptyList());
executer.executeImpl();
verify(mockedSearchService, times(3)).query(any(SearchParameters.class));
verify(mockedNodeService, times(1)).exists(node1);
verify(mockedNodeService, times(1)).getPrimaryParent(node1);
verify(mockedNodeService, times(1)).getProperty(node1, RecordsManagementModel.PROP_DISPOSITION_ACTION);
verify(mockedRecordsManagementActionService, times(1))
.executeRecordsManagementAction(eq(parent), eq(CUTOFF), anyMap());
verify(mockedNodeService, times(1)).exists(node2);
verify(mockedNodeService, times(1)).getPrimaryParent(node2);
verify(mockedNodeService, times(1)).getProperty(node2, RecordsManagementModel.PROP_DISPOSITION_ACTION);
verify(mockedRecordsManagementActionService, times(1))
.executeRecordsManagementAction(eq(parent), eq(RETAIN), anyMap());
assertEquals(expected, actual);
}
/**
@@ -461,8 +348,8 @@ public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest
executer.executeImpl();
ArgumentCaptor<SearchParameters> paramsCaptor = ArgumentCaptor.forClass(SearchParameters.class);
verify(mockedSearchService, times(1)).query(paramsCaptor.capture());
assertEquals(DispositionLifecycleJobExecuter.DEFAULT_BATCH_SIZE, paramsCaptor.getValue().getMaxItems());
verify(mockedSearchService, times(1)).query(paramsCaptor.capture());;
assertEquals(executer.DEFAULT_BATCH_SIZE, paramsCaptor.getValue().getMaxItems());
verify(mockedResultSet, times(1)).close();
executer.setBatchSize(BATCH_SIZE);
@@ -2,7 +2,7 @@
* #%L
* Alfresco Data model classes
* %%
* Copyright (C) 2005 - 2026 Alfresco Software Limited
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -206,8 +206,6 @@ public class SearchParameters implements BasicSearchParameters
*/
private int trackTotalHits;
private boolean trackScore = true;
/**
* Default constructor
*/
@@ -259,7 +257,6 @@ public class SearchParameters implements BasicSearchParameters
sp.ranges = this.ranges;
sp.timezone = this.timezone;
sp.trackTotalHits = this.trackTotalHits;
sp.trackScore = this.trackScore;
return sp;
}
@@ -1076,22 +1073,6 @@ public class SearchParameters implements BasicSearchParameters
}
}
public boolean isTrackScore()
{
return trackScore;
}
/**
* Controls whether relevance scores are computed for search hits. This hint is only acted upon by the Elasticsearch search subsystem; Solr and DB query paths ignore it.
*
* @param trackScore
* true to compute scores (default), false to skip scoring for better performance
*/
public void setTrackScore(boolean trackScore)
{
this.trackScore = trackScore;
}
/**
* @param searchTerm the searchTerm to set
*/