Compare commits

...

3 Commits

Author SHA1 Message Date
Damian.Ujma@hyland.com
8986d03a2f ACS-7557 Add permissions checks [ags] 2024-05-06 15:24:27 +02:00
Damian.Ujma@hyland.com
502c996c9e ACS-7557 Fix [ags] 2024-04-29 16:15:03 +02:00
Damian.Ujma@hyland.com
a22e7d23f0 ACS-7557 Add bulk API design 2024-04-29 16:00:40 +02:00
18 changed files with 1362 additions and 1 deletions

View File

@@ -139,3 +139,11 @@ content.metadata.async.extract.6.enabled=false
# Max number of entries returned in Record search view
rm.recordSearch.maxItems=500
#
# Hold bulk
#
rm.hold.bulk.threadCount=2
rm.hold.bulk.maxItems=1000
rm.hold.bulk.batchSize=100
rm.hold.bulk.logging.interval.ms=1000

View File

@@ -83,6 +83,13 @@
<property name="nodesModelFactory" ref="nodesModelFactory" />
<property name="fileFolderService" ref="FileFolderService" />
<property name="transactionService" ref="transactionService" />
<property name="holdBulkService" ref="holdBulkService" />
</bean>
<bean class="org.alfresco.rm.rest.api.holds.HoldsBulkStatusesRelation" >
<property name="holdBulkMonitor" ref="holdBulkMonitor" />
<property name="apiUtils" ref="apiUtils" />
<property name="permissionService" ref="PermissionService" />
</bean>
<bean class="org.alfresco.rm.rest.api.holds.HoldsChildrenRelation">
@@ -257,4 +264,42 @@
<property name="beanName" value="restJsonModule" />
<property name="extendingBeanName" value="rm.restJsonModule" />
</bean>
<bean id="holdBulkService"
class="org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkServiceImpl">
<property name="serviceRegistry" ref="ServiceRegistry" />
<property name="transactionService" ref="transactionService" />
<property name="searchMapper" ref="searchapiSearchMapper" />
<property name="bulkMonitor" ref="holdBulkMonitor" />
<property name="holdService" ref="HoldService" />
<property name="capabilityService" ref="CapabilityService" />
<property name="permissionService" ref="PermissionService" />
<property name="nodeService" ref="NodeService" />
<property name="threadCount">
<value>${rm.hold.bulk.threadCount}</value>
</property>
<property name="batchSize">
<value>${rm.hold.bulk.batchSize}</value>
</property>
<property name="maxItems">
<value>${rm.hold.bulk.maxItems}</value>
</property>
<property name="loggingIntervalMs">
<value>${rm.hold.bulk.logging.interval.ms}</value>
</property>
</bean>
<bean id="holdBulkMonitor" class="org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkMonitor">
<property name="holdProgressCache" ref="holdProgressCache" />
<property name="holdProcessRegistry" ref="holdProcessRegistry" />
</bean>
<bean name="holdProgressCache" factory-bean="cacheFactory" factory-method="createCache">
<constructor-arg value="cache.workerRegistryCache" />
</bean>
<bean name="holdProcessRegistry" factory-bean="cacheFactory" factory-method="createCache">
<constructor-arg value="cache.workerRegistryCache" />
</bean>
</beans>

View File

@@ -0,0 +1,207 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.alfresco.repo.batch.BatchProcessWorkProvider;
import org.alfresco.repo.batch.BatchProcessor;
import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker;
import org.alfresco.rest.api.search.impl.SearchMapper;
import org.alfresco.rest.api.search.model.Query;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
public abstract class BulkBaseService<T> implements InitializingBean
{
private static final Log logger = LogFactory.getLog(BulkBaseService.class);
private ServiceRegistry serviceRegistry;
private SearchService searchService;
private TransactionService transactionService;
private SearchMapper searchMapper;
private BulkMonitor<T> bulkMonitor;
private ApplicationEventPublisher applicationEventPublisher;
private int threadCount;
private int batchSize;
private long maxItems;
private int loggingIntervalMs;
@Override
public void afterPropertiesSet() throws Exception
{
this.searchService = serviceRegistry.getSearchService();
}
public T execute(NodeRef holdRef, BulkOperation bulkOperation)
{
checkPermissions(holdRef, bulkOperation);
long totalItems = getTotalItems(bulkOperation.searchQuery());
if (maxItems < totalItems)
{
throw new RuntimeException("Too many items to process. Please refine your query.");
}
String processId = UUID.randomUUID().toString();
T initBulkStatus = getInitBulkStatus(processId, totalItems);
bulkMonitor.updateBulkStatus(initBulkStatus);
bulkMonitor.registerProcess(holdRef, processId);
BatchProcessWorker<NodeRef> batchProcessWorker = getWorkerProvider(holdRef, bulkOperation);
BatchProcessor<NodeRef> batchProcessor = new BatchProcessor<NodeRef>(
processId,
transactionService.getRetryingTransactionHelper(),
getWorkProvider(bulkOperation, totalItems),
threadCount,
batchSize,
applicationEventPublisher,
logger,
loggingIntervalMs);
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(runBatchProcessor(batchProcessor, batchProcessWorker));
return initBulkStatus;
}
protected Callable<Void> runBatchProcessor(BatchProcessor<NodeRef> batchProcessor,
BatchProcessWorker<NodeRef> batchProcessWorker)
{
return () -> {
TaskScheduler taskScheduler = getTaskScheduler(batchProcessor, bulkMonitor);
taskScheduler.schedule(loggingIntervalMs);
try
{
batchProcessor.processLong(batchProcessWorker, true);
taskScheduler.runTask();
}
catch (Throwable t)
{
//TODO: handle exception
}
finally
{
taskScheduler.stopListening();
}
return null;
};
}
protected abstract T getInitBulkStatus(String processId, long totalItems);
protected abstract TaskScheduler getTaskScheduler(BatchProcessor<NodeRef> batchProcessor, BulkMonitor<T> monitor);
protected abstract BatchProcessWorkProvider<NodeRef> getWorkProvider(BulkOperation bulkOperation, long totalItems);
protected abstract BatchProcessWorker<NodeRef> getWorkerProvider(NodeRef nodeRef, BulkOperation bulkOperation);
protected abstract void checkPermissions(NodeRef holdRef, BulkOperation bulkOperation);
protected long getTotalItems(Query searchQuery)
{
SearchParameters searchParams = new SearchParameters();
searchMapper.setDefaults(searchParams);
searchMapper.fromQuery(searchParams, searchQuery);
searchParams.setSkipCount(0);
searchParams.setMaxItems(1);
return searchService.query(searchParams).getNumberFound();
}
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher)
{
this.applicationEventPublisher = applicationEventPublisher;
}
public void setServiceRegistry(ServiceRegistry serviceRegistry)
{
this.serviceRegistry = serviceRegistry;
}
public void setSearchService(SearchService searchService)
{
this.searchService = searchService;
}
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
public void setSearchMapper(SearchMapper searchMapper)
{
this.searchMapper = searchMapper;
}
public void setBulkMonitor(BulkMonitor<T> bulkMonitor)
{
this.bulkMonitor = bulkMonitor;
}
public void setThreadCount(int threadCount)
{
this.threadCount = threadCount;
}
public void setBatchSize(int batchSize)
{
this.batchSize = batchSize;
}
public void setMaxItems(long maxItems)
{
this.maxItems = maxItems;
}
public void setLoggingIntervalMs(int loggingIntervalMs)
{
this.loggingIntervalMs = loggingIntervalMs;
}
public SearchMapper getSearchMapper()
{
return searchMapper;
}
public int getBatchSize()
{
return batchSize;
}
public SearchService getSearchService()
{
return searchService;
}
}

View File

@@ -0,0 +1,38 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk;
import org.alfresco.service.cmr.repository.NodeRef;
public interface BulkMonitor<T>
{
void updateBulkStatus(T bulkStatus);
void registerProcess(NodeRef nodeRef, String processId);
T getBulkStatus(String processName);
}

View File

@@ -0,0 +1,41 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk;
import org.alfresco.rest.api.search.model.Query;
public record BulkOperation(Query searchQuery, String operationType)
{
public BulkOperation
{
if (operationType == null || searchQuery == null)
{
throw new IllegalArgumentException("Operation type and search query must not be null");
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk;
public interface TaskScheduler
{
void schedule(long msInterval);
void runTask();
void stopListening();
}

View File

@@ -0,0 +1,88 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkMonitor;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.service.cmr.repository.NodeRef;
public class HoldBulkMonitor implements BulkMonitor<HoldBulkStatus>
{
private SimpleCache<String, HoldBulkStatus> holdProgressCache;
private SimpleCache<String, List<String>> holdProcessRegistry;
public void updateBulkStatus(HoldBulkStatus holdBulkStatus)
{
holdProgressCache.put(holdBulkStatus.processId(), holdBulkStatus);
}
public void registerProcess(NodeRef holdRef, String processId)
{
List<String> processIds = Optional.ofNullable(holdProcessRegistry.get(holdRef.getId()))
.orElse(new ArrayList<>());
processIds.add(processId);
holdProcessRegistry.put(holdRef.getId(), processIds);
}
public HoldBulkStatus getBulkStatus(String processName)
{
return holdProgressCache.get(processName);
}
public List<HoldBulkStatus> getBatchStatusesForHold(String holdId)
{
return Optional.ofNullable(holdProcessRegistry.get(holdId))
.map(list -> list.stream()
.map(this::getBulkStatus)
.filter(Objects::nonNull)
.sorted(Comparator.comparing(HoldBulkStatus::endTime, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(HoldBulkStatus::startTime, Comparator.nullsLast(Comparator.naturalOrder()))
.reversed())
.toList())
.orElse(Collections.emptyList());
}
public void setHoldProgressCache(
SimpleCache<String, HoldBulkStatus> holdProgressCache)
{
this.holdProgressCache = holdProgressCache;
}
public void setHoldProcessRegistry(
SimpleCache<String, List<String>> holdProcessRegistry)
{
this.holdProcessRegistry = holdProcessRegistry;
}
}

View File

@@ -0,0 +1,36 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.service.cmr.repository.NodeRef;
public interface HoldBulkService
{
HoldBulkStatus execute(NodeRef holdRef, BulkOperation bulkOperation);
}

View File

@@ -0,0 +1,235 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
import static org.alfresco.model.ContentModel.PROP_NAME;
import static org.alfresco.rm.rest.api.model.HoldBulkOperationType.ADD;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
import org.alfresco.model.ContentModel;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkBaseService;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkMonitor;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation;
import org.alfresco.module.org_alfresco_module_rm.bulk.TaskScheduler;
import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService;
import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel;
import org.alfresco.module.org_alfresco_module_rm.hold.HoldService;
import org.alfresco.repo.batch.BatchProcessWorkProvider;
import org.alfresco.repo.batch.BatchProcessor;
import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.rest.api.search.impl.SearchMapper;
import org.alfresco.rest.api.search.model.Query;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
import org.springframework.extensions.surf.util.I18NUtil;
public class HoldBulkServiceImpl extends BulkBaseService<HoldBulkStatus> implements HoldBulkService
{
private HoldService holdService;
private static final String MSG_ERR_ACCESS_DENIED = "permissions.err_access_denied";
private CapabilityService capabilityService;
private PermissionService permissionService;
private NodeService nodeService;
@Override
protected HoldBulkStatus getInitBulkStatus(String processId, long totalItems)
{
return new HoldBulkStatus(processId, null, null, 0, 0, totalItems, null);
}
@Override
protected TaskScheduler getTaskScheduler(BatchProcessor<NodeRef> batchProcessor,
BulkMonitor<HoldBulkStatus> monitor)
{
return new HoldTaskScheduler(() -> monitor.updateBulkStatus(
new HoldBulkStatus(batchProcessor.getProcessName(), batchProcessor.getStartTime(),
batchProcessor.getEndTime(), batchProcessor.getSuccessfullyProcessedEntriesLong(),
batchProcessor.getTotalErrorsLong(), batchProcessor.getTotalResultsLong(),
batchProcessor.getLastError())));
}
@Override
protected BatchProcessWorkProvider<NodeRef> getWorkProvider(BulkOperation bulkOperation, long totalItems)
{
return new AddToHoldWorkerProvider(new AtomicInteger(0), bulkOperation, totalItems);
}
@Override
protected BatchProcessWorker<NodeRef> getWorkerProvider(NodeRef nodeRef, BulkOperation bulkOperation)
{
if (ADD.name().equals(bulkOperation.operationType()))
{
return new AddToHoldWorkerBatch(nodeRef);
}
throw new IllegalArgumentException("Unsupported action type when starting the bulk process: " + bulkOperation.operationType());
}
@Override
protected void checkPermissions(NodeRef holdRef, BulkOperation bulkOperation)
{
if (!holdService.isHold(holdRef))
{
final String holdName = (String) nodeService.getProperty(holdRef, PROP_NAME);
throw new InvalidArgumentException(I18NUtil.getMessage("rm.hold.not-hold", holdName), null);
}
if (ADD.name().equals(bulkOperation.operationType()))
{
if (!AccessStatus.ALLOWED.equals(
capabilityService.getCapabilityAccessState(holdRef, RMPermissionModel.ADD_TO_HOLD)) ||
permissionService.hasPermission(holdRef, RMPermissionModel.FILING) == AccessStatus.DENIED)
{
throw new AccessDeniedException(I18NUtil.getMessage(MSG_ERR_ACCESS_DENIED));
}
}
}
private class AddToHoldWorkerBatch implements BatchProcessWorker<NodeRef>
{
private final NodeRef holdRef;
private final String currentUser;
public AddToHoldWorkerBatch(NodeRef holdRef)
{
this.holdRef = holdRef;
currentUser = AuthenticationUtil.getFullyAuthenticatedUser();
}
@Override
public String getIdentifier(NodeRef entry)
{
return entry.getId();
}
@Override
public void beforeProcess()
{
AuthenticationUtil.pushAuthentication();
}
@Override
public void process(NodeRef entry) throws Throwable
{
AuthenticationUtil.setFullyAuthenticatedUser(currentUser);
holdService.addToHold(holdRef, entry);
}
@Override
public void afterProcess()
{
AuthenticationUtil.popAuthentication();
}
}
private class AddToHoldWorkerProvider implements BatchProcessWorkProvider<NodeRef>
{
private final AtomicInteger currentNodeNumber;
private final Query searchQuery;
private final String currentUser;
private final long totalItems;
public AddToHoldWorkerProvider(AtomicInteger currentNodeNumber, BulkOperation bulkOperation, long totalItems)
{
this.currentNodeNumber = currentNodeNumber;
this.searchQuery = bulkOperation.searchQuery();
this.totalItems = totalItems;
currentUser = AuthenticationUtil.getFullyAuthenticatedUser();
}
@Override
public int getTotalEstimatedWorkSize()
{
return (int) totalItems;
}
@Override
public long getTotalEstimatedWorkSizeLong()
{
return totalItems;
}
@Override
public Collection<NodeRef> getNextWork()
{
AuthenticationUtil.pushAuthentication();
AuthenticationUtil.setFullyAuthenticatedUser(currentUser);
SearchParameters searchParams = getNextPageParameters();
ResultSet result = getSearchService().query(searchParams);
if (result.getNodeRefs().isEmpty())
{
return Collections.emptyList();
}
AuthenticationUtil.popAuthentication();
currentNodeNumber.addAndGet(getBatchSize());
return result.getNodeRefs();
}
private SearchParameters getNextPageParameters()
{
SearchParameters searchParams = new SearchParameters();
SearchMapper searchMapper = getSearchMapper();
searchMapper.setDefaults(searchParams);
searchMapper.fromQuery(searchParams, searchQuery);
searchParams.setSkipCount(currentNodeNumber.get());
searchParams.setMaxItems(getBatchSize());
searchParams.addSort("@" + ContentModel.PROP_CREATED, true);
return searchParams;
}
}
public void setHoldService(HoldService holdService)
{
this.holdService = holdService;
}
public void setCapabilityService(CapabilityService capabilityService)
{
this.capabilityService = capabilityService;
}
public void setPermissionService(PermissionService permissionService)
{
this.permissionService = permissionService;
}
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
}

View File

@@ -0,0 +1,63 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.alfresco.module.org_alfresco_module_rm.bulk.TaskScheduler;
public class HoldTaskScheduler implements TaskScheduler
{
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private ScheduledFuture<?> scheduledTask;
private Runnable task;
public HoldTaskScheduler(Runnable task)
{
this.task = task;
}
public void schedule(long loggingInterval)
{
scheduledTask = executor.scheduleAtFixedRate(task, loggingInterval, loggingInterval, TimeUnit.MILLISECONDS);
}
public void runTask()
{
task.run();
}
public void stopListening()
{
scheduledTask.cancel(false);
executor.shutdown();
}
}

View File

@@ -0,0 +1,118 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rm.rest.api.holds;
import static org.alfresco.module.org_alfresco_module_rm.util.RMParameterCheck.checkNotBlank;
import static org.alfresco.util.ParameterCheck.mandatory;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkMonitor;
import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException;
import org.alfresco.rest.framework.resource.RelationshipResource;
import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
import org.springframework.extensions.surf.util.I18NUtil;
@RelationshipResource(name = "bulk-statuses", entityResource = HoldsEntityResource.class, title = "Bulk statuses of a hold")
public class HoldsBulkStatusesRelation
implements RelationshipResourceAction.Read<HoldBulkStatus>, RelationshipResourceAction.ReadById<HoldBulkStatus>
{
private HoldBulkMonitor holdBulkMonitor;
private FilePlanComponentsApiUtils apiUtils;
private PermissionService permissionService;
@Override
public CollectionWithPagingInfo<HoldBulkStatus> readAll(String holdId, Parameters parameters)
{
// validate parameters
checkNotBlank("holdId", holdId);
mandatory("parameters", parameters);
NodeRef holdRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
checkReadPermissions(holdRef);
List<HoldBulkStatus> statuses = holdBulkMonitor.getBatchStatusesForHold(holdId);
List<HoldBulkStatus> page = statuses.stream()
.skip(parameters.getPaging().getSkipCount())
.limit(parameters.getPaging().getMaxItems())
.collect(Collectors.toCollection(LinkedList::new));
int totalItems = statuses.size();
boolean hasMore = parameters.getPaging().getSkipCount() + parameters.getPaging().getMaxItems() < totalItems;
return CollectionWithPagingInfo.asPaged(parameters.getPaging(), page, hasMore, totalItems);
}
@Override
public HoldBulkStatus readById(String holdId, String processId, Parameters parameters)
throws RelationshipResourceNotFoundException
{
checkNotBlank("processId", processId);
mandatory("parameters", parameters);
NodeRef holdRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
checkReadPermissions(holdRef);
return Optional.ofNullable(holdBulkMonitor.getBulkStatus(processId)).orElseThrow(() -> new EntityNotFoundException(processId));
}
private void checkReadPermissions(NodeRef holdRef)
{
if (permissionService.hasReadPermission(holdRef) == AccessStatus.DENIED)
{
throw new PermissionDeniedException(I18NUtil.getMessage("permissions.err_access_denied"));
}
}
public void setHoldBulkMonitor(HoldBulkMonitor holdBulkMonitor)
{
this.holdBulkMonitor = holdBulkMonitor;
}
public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
{
this.apiUtils = apiUtils;
}
public void setPermissionService(PermissionService permissionService)
{
this.permissionService = permissionService;
}
}

View File

@@ -30,6 +30,8 @@ import static org.alfresco.module.org_alfresco_module_rm.util.RMParameterCheck.c
import static org.alfresco.util.ParameterCheck.mandatory;
import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation;
import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkService;
import org.alfresco.module.org_alfresco_module_rm.hold.HoldService;
import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
@@ -42,6 +44,8 @@ import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rest.framework.webscripts.WithResponse;
import org.alfresco.rm.rest.api.impl.ApiNodesModelFactory;
import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils;
import org.alfresco.rm.rest.api.model.HoldBulkOperation;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.rm.rest.api.model.HoldDeletionReason;
import org.alfresco.rm.rest.api.model.HoldModel;
import org.alfresco.service.cmr.model.FileFolderService;
@@ -68,6 +72,7 @@ public class HoldsEntityResource implements
private ApiNodesModelFactory nodesModelFactory;
private HoldService holdService;
private TransactionService transactionService;
private HoldBulkService holdBulkService;
@Override
public void afterPropertiesSet() throws Exception
@@ -157,6 +162,22 @@ public class HoldsEntityResource implements
return reason;
}
@Operation("bulk")
@WebApiDescription(title = "Start the hold bulk operation",
successStatus = HttpServletResponse.SC_ACCEPTED)
public HoldBulkStatus bulk(String holdId, HoldBulkOperation holdBulkOperation, Parameters parameters,
WithResponse withResponse)
{
// validate parameters
checkNotBlank("holdId", holdId);
mandatory("parameters", parameters);
NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
return holdBulkService.execute(parentNodeRef,
new BulkOperation(holdBulkOperation.query(), holdBulkOperation.op().name()));
}
public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
{
this.apiUtils = apiUtils;
@@ -181,4 +202,9 @@ public class HoldsEntityResource implements
{
this.transactionService = transactionService;
}
public void setHoldBulkService(HoldBulkService holdBulkService)
{
this.holdBulkService = holdBulkService;
}
}

View File

@@ -0,0 +1,33 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rm.rest.api.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.alfresco.rest.api.search.model.Query;
public record HoldBulkOperation(@JsonProperty(required = true) Query query, @JsonProperty(required = true) HoldBulkOperationType op) {}

View File

@@ -0,0 +1,32 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rm.rest.api.model;
public enum HoldBulkOperationType
{
ADD
}

View File

@@ -0,0 +1,76 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rm.rest.api.model;
import java.io.Serializable;
import java.text.NumberFormat;
import java.util.Date;
public record HoldBulkStatus(String processId, Date startTime, Date endTime, long itemsProcessed, long errorsCount,
long totalItems, String lastError) implements Serializable
{
public enum Status
{
PENDING("PENDING"),
IN_PROGRESS("IN PROGRESS"),
DONE("DONE");
private final String value;
Status(String value)
{
this.value = value;
}
public String getValue()
{
return value;
}
}
public String getStatus()
{
if (startTime == null && endTime == null)
{
return Status.PENDING.getValue();
}
else if (startTime != null && endTime == null)
{
return Status.IN_PROGRESS.getValue();
}
else
{
return Status.DONE.getValue();
}
}
public String getPercentageProcessed()
{
return itemsProcessed <= totalItems ? NumberFormat.getPercentInstance().format(
totalItems == 0 ? 1.0F : (float) itemsProcessed / totalItems) : "Unknown";
}
}

View File

@@ -0,0 +1,86 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
public class HoldBulkMonitorTest {
@Mock
private SimpleCache<String, HoldBulkStatus> holdProgressCache;
@Mock
private SimpleCache<String, List<String>> holdProcessRegistry;
private HoldBulkMonitor holdBulkMonitor;
@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
holdBulkMonitor = new HoldBulkMonitor();
holdBulkMonitor.setHoldProgressCache(holdProgressCache);
holdBulkMonitor.setHoldProcessRegistry(holdProcessRegistry);
}
@Test
public void getBatchStatusesForHoldReturnsEmptyListWhenNoProcesses() {
when(holdProcessRegistry.get("holdId")).thenReturn(null);
assertEquals(Collections.emptyList(), holdBulkMonitor.getBatchStatusesForHold("holdId"));
}
@Test
public void getBatchStatusesForHoldReturnsSortedStatuses() {
HoldBulkStatus status1 = new HoldBulkStatus(null, new Date(1000), new Date(2000), 0L, 0L, 0L, null);
HoldBulkStatus status2 = new HoldBulkStatus(null, new Date(3000), null, 0L, 0L, 0L, null);
HoldBulkStatus status3 = new HoldBulkStatus(null, new Date(4000), null, 0L, 0L, 0L, null);
HoldBulkStatus status4 = new HoldBulkStatus(null, new Date(500), new Date(800), 0L, 0L, 0L, null);
HoldBulkStatus status5 = new HoldBulkStatus(null, null, null, 0L, 0L, 0L, null);
when(holdProcessRegistry.get("holdId")).thenReturn(Arrays.asList("process1", "process2", "process3", "process4", "process5"));
when(holdProgressCache.get("process1")).thenReturn(status1);
when(holdProgressCache.get("process2")).thenReturn(status2);
when(holdProgressCache.get("process3")).thenReturn(status3);
when(holdProgressCache.get("process4")).thenReturn(status4);
when(holdProgressCache.get("process5")).thenReturn(status5);
assertEquals(Arrays.asList(status5, status3, status2, status1, status4), holdBulkMonitor.getBatchStatusesForHold("holdId"));
}
}

View File

@@ -2314,6 +2314,112 @@ paths:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
'/holds/{holdId}/bulk-statuses':
get:
tags:
- holds
operationId: bulkStatuses
summary: Get bulk statuses
description: |
Gets bulk statuses for hold with id **holdId**.
parameters:
- $ref: '#/parameters/holdIdParam'
- $ref: '#/parameters/skipCountParam'
- $ref: '#/parameters/maxItemsParam'
responses:
'202':
description: Successful response
schema:
$ref: '#/definitions/HoldBulkStatusPaging'
'400':
description: |
Invalid parameter: **holdId** is not a valid format
'401':
description: Authentication failed
'403':
description: Current user does not have permission to read **holdId**
'404':
description: "**holdId** does not exist"
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
'/holds/{holdId}/bulk-statuses/{processId}':
get:
tags:
- holds
operationId: bulkStatus
summary: Get the bulk status
description: |
Gets the bulk status specified by **processId** for **holdId**.
parameters:
- $ref: '#/parameters/holdIdParam'
- $ref: '#/parameters/processId'
responses:
'202':
description: Successful response
schema:
$ref: '#/definitions/HoldBulkStatus'
'400':
description: |
Invalid parameter: **holdId** or **processId** is not a valid format
'401':
description: Authentication failed
'403':
description: Current user does not have permission to read **holdId**
'404':
description: "**holdId** or **processId** does not exist"
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
'/holds/{holdId}/bulk':
post:
tags:
- holds
operationId: holdBulk
summary: Start the hold bulk process
description: |
Start the asynchronous bulk process of a hold with id **holdId** based on search query results.
```JSON
For example, the following JSON body start the bulk process to add search query results
as children of a hold.
{
"query": {
"query": "SITE:swsdp and TYPE:content",
"language": "afts"
},
"op": "ADD"
}
```
parameters:
- $ref: '#/parameters/holdIdParam'
- in: body
name: holdBulkOperation
description: Bulk operation.
required: true
schema:
$ref: '#/definitions/HoldBulkOperation'
responses:
'202':
description: Successful response
schema:
$ref: '#/definitions/HoldBulkStatus'
'400':
description: |
Invalid parameter: **holdId** is not a valid format or **HoldBulkOperation** is not valid
'401':
description: Authentication failed
'403':
description: Current user does not have permission to start the bulk process of **holdId**
'404':
description: "**holdId** does not exist"
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
'/holds/{holdId}/delete':
post:
tags:
@@ -2862,6 +2968,12 @@ parameters:
description: The identifier of a child of a hold.
required: true
type: string
processId:
name: processId
in: path
description: The identifier of a bulk process.
required: true
type: string
## Record
recordIdParam:
name: recordId
@@ -4018,6 +4130,84 @@ definitions:
properties:
reason:
type: string
RequestQuery:
description: Query.
type: object
required:
- query
properties:
language:
description: The query language in which the query is written.
type: string
default: afts
enum:
- afts
- lucene
- cmis
userQuery:
description: The exact search request typed in by the user
type: string
query:
description: The query which may have been generated in some way from the userQuery
type: string
HoldBulkOperation:
type: object
properties:
query:
$ref: '#/definitions/RequestQuery'
op:
description: The query language in which the query is written.
type: string
default: ADD
enum:
- ADD
HoldBulkStatus:
type: object
properties:
processId:
type: string
startTime:
type: string
format: date-time
endTime:
type: string
format: date-time
itemsProcessed:
type: integer
format: int64
errorsCount:
type: integer
format: int64
totalItems:
type: integer
format: int64
lastError:
type: string
status:
type: string
enum:
- PENDING
- IN PROGRESS
- DONE
HoldBulkStatusEntry:
type: object
required:
- entry
properties:
entry:
$ref: '#/definitions/HoldBulkStatus'
HoldBulkStatusPaging:
type: object
properties:
list:
type: object
properties:
pagination:
$ref: '#/definitions/Pagination'
entries:
type: array
items:
$ref: '#/definitions/HoldBulkStatusEntry'
##
RequestBodyFile:
type: object

View File

@@ -709,4 +709,6 @@ cache.ldapInitialDirContextCache.cluster.type=fully-distributed
cache.ldapInitialDirContextCache.backup-count=1
cache.ldapInitialDirContextCache.eviction-policy=NONE
cache.ldapInitialDirContextCache.merge-policy=com.hazelcast.spi.merge.LatestUpdateMergePolicy
cache.ldapInitialDirContextCache.readBackupData=false
cache.ldapInitialDirContextCache.readBackupData=false
cache.workerRegistryCache.type=fully-distributed