mirror of
				https://github.com/Alfresco/alfresco-community-repo.git
				synced 2025-10-29 15:21:53 +00:00 
			
		
		
		
	Compare commits
	
		
			27 Commits
		
	
	
		
			23.3.5.2
			...
			feature/AC
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					cb863c646f | ||
| 
						 | 
					a2d1391aee | ||
| 
						 | 
					818bcb09f3 | ||
| 
						 | 
					72e85076ad | ||
| 
						 | 
					a69be867e8 | ||
| 
						 | 
					fa6e0ded45 | ||
| 
						 | 
					ed0bbb6699 | ||
| 
						 | 
					8009a6d6dd | ||
| 
						 | 
					785bdb72ea | ||
| 
						 | 
					66aef18862 | ||
| 
						 | 
					7b516f24b6 | ||
| 
						 | 
					0de1aca0f6 | ||
| 
						 | 
					592dc35b6d | ||
| 
						 | 
					73724a8205 | ||
| 
						 | 
					c8c1102431 | ||
| 
						 | 
					c31ae1fe33 | ||
| 
						 | 
					82e9f0452d | ||
| 
						 | 
					377f546a9b | ||
| 
						 | 
					07dcba972f | ||
| 
						 | 
					d7f9ed1cf0 | ||
| 
						 | 
					4eccb77fa8 | ||
| 
						 | 
					07352336c5 | ||
| 
						 | 
					b5ce847bb1 | ||
| 
						 | 
					34925b497b | ||
| 
						 | 
					8986d03a2f | ||
| 
						 | 
					502c996c9e | ||
| 
						 | 
					a22e7d23f0 | 
@@ -84,6 +84,12 @@
 | 
			
		||||
         <artifactId>okhttp</artifactId>
 | 
			
		||||
         <scope>test</scope>
 | 
			
		||||
      </dependency>
 | 
			
		||||
      <dependency>
 | 
			
		||||
         <groupId>org.awaitility</groupId>
 | 
			
		||||
         <artifactId>awaitility</artifactId>
 | 
			
		||||
         <version>${dependency.awaitility.version}</version>
 | 
			
		||||
         <scope>test</scope>
 | 
			
		||||
      </dependency>
 | 
			
		||||
      <dependency>
 | 
			
		||||
         <groupId>org.apache.commons</groupId>
 | 
			
		||||
         <artifactId>commons-collections4</artifactId>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,60 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.rest.rm.community.model.hold;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonProperty;
 | 
			
		||||
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Builder;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
import org.alfresco.rest.search.RestRequestQueryModel;
 | 
			
		||||
import org.alfresco.utility.model.TestModel;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * POJO for hold bulk request
 | 
			
		||||
 *
 | 
			
		||||
 * @author Damian Ujma
 | 
			
		||||
 */
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
@Builder
 | 
			
		||||
@Data
 | 
			
		||||
@NoArgsConstructor
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public class HoldBulkOperation extends TestModel
 | 
			
		||||
{
 | 
			
		||||
    public enum HoldBulkOperationType
 | 
			
		||||
    {
 | 
			
		||||
        ADD
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @JsonProperty(required = true)
 | 
			
		||||
    private RestRequestQueryModel query;
 | 
			
		||||
    @JsonProperty(required = true)
 | 
			
		||||
    private HoldBulkOperationType op;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,50 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.rest.rm.community.model.hold;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonProperty;
 | 
			
		||||
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Builder;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * POJO for hold bulk request entry
 | 
			
		||||
 *
 | 
			
		||||
 * @author Damian Ujma
 | 
			
		||||
 */
 | 
			
		||||
@Builder
 | 
			
		||||
@Data
 | 
			
		||||
@NoArgsConstructor
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public class HoldBulkOperationEntry
 | 
			
		||||
{
 | 
			
		||||
    private String bulkStatusId;
 | 
			
		||||
 | 
			
		||||
    private long totalItems;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.rest.rm.community.model.hold;
 | 
			
		||||
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Builder;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
import org.alfresco.utility.model.TestModel;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * POJO for hold bulk request
 | 
			
		||||
 *
 | 
			
		||||
 * @author Damian Ujma
 | 
			
		||||
 */
 | 
			
		||||
@Builder
 | 
			
		||||
@Data
 | 
			
		||||
@NoArgsConstructor
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public class HoldBulkStatus extends TestModel
 | 
			
		||||
{
 | 
			
		||||
    private String bulkStatusId;
 | 
			
		||||
 | 
			
		||||
    private String startTime;
 | 
			
		||||
 | 
			
		||||
    private String endTime;
 | 
			
		||||
 | 
			
		||||
    private long processedItems;
 | 
			
		||||
 | 
			
		||||
    private long errorsCount;
 | 
			
		||||
 | 
			
		||||
    private long totalItems;
 | 
			
		||||
 | 
			
		||||
    private String lastError;
 | 
			
		||||
 | 
			
		||||
    private Status status;
 | 
			
		||||
 | 
			
		||||
    public enum Status
 | 
			
		||||
    {
 | 
			
		||||
        PENDING,
 | 
			
		||||
        IN_PROGRESS,
 | 
			
		||||
        DONE
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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.rest.rm.community.model.hold;
 | 
			
		||||
 | 
			
		||||
import org.alfresco.rest.core.RestModels;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handle collection of {@link HoldBulkStatusEntry}
 | 
			
		||||
 *
 | 
			
		||||
 * @author Damian Ujma
 | 
			
		||||
 */
 | 
			
		||||
public class HoldBulkStatusCollection extends RestModels<HoldBulkStatusEntry, HoldBulkStatusCollection>
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.rest.rm.community.model.hold;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonProperty;
 | 
			
		||||
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Builder;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
import org.alfresco.rest.core.RestModels;
 | 
			
		||||
 | 
			
		||||
@Builder
 | 
			
		||||
@Data
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
@NoArgsConstructor
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public class HoldBulkStatusEntry extends RestModels<HoldBulkStatus, HoldBulkStatusEntry>
 | 
			
		||||
{
 | 
			
		||||
    private HoldBulkStatus entry;
 | 
			
		||||
}
 | 
			
		||||
@@ -48,5 +48,5 @@ import org.alfresco.rest.core.RestModels;
 | 
			
		||||
public class HoldChildEntry extends RestModels<Hold, HoldChildEntry>
 | 
			
		||||
{
 | 
			
		||||
    @JsonProperty
 | 
			
		||||
    private HoldChildEntry entry;
 | 
			
		||||
    private HoldChild entry;
 | 
			
		||||
}
 | 
			
		||||
@@ -39,6 +39,10 @@ import static org.springframework.http.HttpMethod.PUT;
 | 
			
		||||
 | 
			
		||||
import org.alfresco.rest.core.RMRestWrapper;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.Hold;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldBulkOperation;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldBulkOperationEntry;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatus;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatusCollection;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldChild;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldChildCollection;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldDeletionReason;
 | 
			
		||||
@@ -287,4 +291,113 @@ public class HoldsAPI extends RMModelRequest
 | 
			
		||||
    {
 | 
			
		||||
        deleteHoldChild(holdId, holdChildId, EMPTY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Starts a bulk process for a hold.
 | 
			
		||||
     *
 | 
			
		||||
     * @param holdBulkOperation The bulk operation details
 | 
			
		||||
     * @param hold The identifier of a hold
 | 
			
		||||
     * @param parameters The URL parameters to add
 | 
			
		||||
     * @return The {@link HoldBulkOperationEntry} for the started bulk process
 | 
			
		||||
     * @throws RuntimeException for the following cases:
 | 
			
		||||
     * <ul>
 | 
			
		||||
     *  <li>{@code hold} or {@code holdBulkOperation} is invalid</li>
 | 
			
		||||
     *  <li>authentication fails</li>
 | 
			
		||||
     *  <li>current user does not have permission to start a bulk process for {@code hold}</li>
 | 
			
		||||
     *  <li>{@code hold} does not exist</li>
 | 
			
		||||
     * </ul>
 | 
			
		||||
     */
 | 
			
		||||
    public HoldBulkOperationEntry startBulkProcess(HoldBulkOperation holdBulkOperation, String hold, String parameters)
 | 
			
		||||
    {
 | 
			
		||||
        mandatoryObject("holdBulkOperation", holdBulkOperation);
 | 
			
		||||
        mandatoryString("hold", hold);
 | 
			
		||||
 | 
			
		||||
        return getRmRestWrapper().processModel(HoldBulkOperationEntry.class, requestWithBody(
 | 
			
		||||
            POST,
 | 
			
		||||
            toJson(holdBulkOperation),
 | 
			
		||||
            "holds/{hold}/bulk",
 | 
			
		||||
            hold,
 | 
			
		||||
            parameters
 | 
			
		||||
                                                                                          ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * See {@link #startBulkProcess(HoldBulkOperation, String, String)}
 | 
			
		||||
     */
 | 
			
		||||
    public HoldBulkOperationEntry startBulkProcess(HoldBulkOperation holdBulkOperation, String hold)
 | 
			
		||||
    {
 | 
			
		||||
        return startBulkProcess(holdBulkOperation, hold, EMPTY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the status of a bulk process for a hold.
 | 
			
		||||
     *
 | 
			
		||||
     * @param holdId The identifier of a hold
 | 
			
		||||
     * @param holdBulkStatusId The identifier of a bulk status operation
 | 
			
		||||
     * @param parameters The URL parameters to add
 | 
			
		||||
     * @return The {@link HoldBulkStatus} for the given {@code holdId} and {@code holdBulkStatusId}
 | 
			
		||||
     * @throws RuntimeException for the following cases:
 | 
			
		||||
     * <ul>
 | 
			
		||||
     *  <li>{@code holdId} or {@code holdBulkStatusId} is invalid</li>
 | 
			
		||||
     *  <li>authentication fails</li>
 | 
			
		||||
     *  <li>current user does not have permission to get the bulk status for {@code holdId}</li>
 | 
			
		||||
     *  <li>{@code holdId} or {@code holdBulkStatusId} does not exist</li>
 | 
			
		||||
     * </ul>
 | 
			
		||||
     */
 | 
			
		||||
    public HoldBulkStatus getBulkStatus(String holdId, String holdBulkStatusId, String parameters)
 | 
			
		||||
    {
 | 
			
		||||
        mandatoryString("holdId", holdId);
 | 
			
		||||
        mandatoryString("holdBulkStatusId", holdBulkStatusId);
 | 
			
		||||
 | 
			
		||||
        return getRmRestWrapper().processModel(HoldBulkStatus.class, simpleRequest(
 | 
			
		||||
            GET,
 | 
			
		||||
            "holds/{holdId}/bulk-statuses/{holdBulkStatusId}",
 | 
			
		||||
            holdId,
 | 
			
		||||
            holdBulkStatusId,
 | 
			
		||||
            parameters
 | 
			
		||||
                                                                                   ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * See {@link #getBulkStatus(String, String, String)}
 | 
			
		||||
     */
 | 
			
		||||
    public HoldBulkStatus getBulkStatus(String holdId, String holdBulkStatusId)
 | 
			
		||||
    {
 | 
			
		||||
        return getBulkStatus(holdId, holdBulkStatusId, EMPTY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the statuses of all bulk processes for a hold.
 | 
			
		||||
     *
 | 
			
		||||
     * @param holdId The identifier of a hold
 | 
			
		||||
     * @param parameters The URL parameters to add
 | 
			
		||||
     * @return The {@link HoldBulkStatusCollection} for the given {@code holdId}
 | 
			
		||||
     * @throws RuntimeException for the following cases:
 | 
			
		||||
     * <ul>
 | 
			
		||||
     *     <li>{@code holdId} is invalid</li>
 | 
			
		||||
     *     <li>authentication fails</li>
 | 
			
		||||
     *     <li>current user does not have permission to get the bulk statuses for {@code holdId}</li>
 | 
			
		||||
     *     <li>{@code holdId} does not exist</li>
 | 
			
		||||
     * </ul>
 | 
			
		||||
     */
 | 
			
		||||
    public HoldBulkStatusCollection getBulkStatuses(String holdId, String parameters)
 | 
			
		||||
    {
 | 
			
		||||
        mandatoryString("holdId", holdId);
 | 
			
		||||
 | 
			
		||||
        return getRmRestWrapper().processModels(HoldBulkStatusCollection.class, simpleRequest(
 | 
			
		||||
            GET,
 | 
			
		||||
            "holds/{holdId}/bulk-statuses",
 | 
			
		||||
            holdId,
 | 
			
		||||
            parameters
 | 
			
		||||
                                                                                             ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * See {@link #getBulkStatuses(String, String)}
 | 
			
		||||
     */
 | 
			
		||||
    public HoldBulkStatusCollection getBulkStatuses(String holdId)
 | 
			
		||||
    {
 | 
			
		||||
        return getBulkStatuses(holdId, EMPTY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,532 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.rest.rm.community.hold;
 | 
			
		||||
 | 
			
		||||
import static org.alfresco.rest.rm.community.base.TestData.HOLD_DESCRIPTION;
 | 
			
		||||
import static org.alfresco.rest.rm.community.base.TestData.HOLD_REASON;
 | 
			
		||||
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS;
 | 
			
		||||
import static org.alfresco.rest.rm.community.model.user.UserPermissions.PERMISSION_FILING;
 | 
			
		||||
import static org.alfresco.rest.rm.community.model.user.UserPermissions.PERMISSION_READ_RECORDS;
 | 
			
		||||
import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
 | 
			
		||||
import static org.alfresco.utility.report.log.Step.STEP;
 | 
			
		||||
import static org.awaitility.Awaitility.await;
 | 
			
		||||
import static org.junit.Assert.assertEquals;
 | 
			
		||||
import static org.junit.Assert.assertNotNull;
 | 
			
		||||
import static org.junit.Assert.assertTrue;
 | 
			
		||||
import static org.springframework.http.HttpStatus.ACCEPTED;
 | 
			
		||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
 | 
			
		||||
import static org.springframework.http.HttpStatus.FORBIDDEN;
 | 
			
		||||
import static org.springframework.http.HttpStatus.NOT_FOUND;
 | 
			
		||||
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
 | 
			
		||||
import org.alfresco.dataprep.CMISUtil;
 | 
			
		||||
import org.alfresco.dataprep.ContentActions;
 | 
			
		||||
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.Hold;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldBulkOperation;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldBulkOperation.HoldBulkOperationType;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldBulkOperationEntry;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatus;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatus.Status;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatusCollection;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatusEntry;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldChild;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.hold.HoldChildEntry;
 | 
			
		||||
import org.alfresco.rest.rm.community.model.user.UserRoles;
 | 
			
		||||
import org.alfresco.rest.search.RestRequestQueryModel;
 | 
			
		||||
import org.alfresco.rest.search.SearchRequest;
 | 
			
		||||
import org.alfresco.rest.v0.service.RoleService;
 | 
			
		||||
import org.alfresco.utility.constants.UserRole;
 | 
			
		||||
import org.alfresco.utility.model.FileModel;
 | 
			
		||||
import org.alfresco.utility.model.FolderModel;
 | 
			
		||||
import org.alfresco.utility.model.UserModel;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.testng.annotations.AfterClass;
 | 
			
		||||
import org.testng.annotations.BeforeClass;
 | 
			
		||||
import org.testng.annotations.Test;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * API tests for adding items to holds via the bulk process
 | 
			
		||||
 */
 | 
			
		||||
public class AddToHoldsBulkV1Tests extends BaseRMRestTest
 | 
			
		||||
{
 | 
			
		||||
    private static final String ACCESS_DENIED_ERROR_MESSAGE = "Access Denied.  You do not have the appropriate " +
 | 
			
		||||
        "permissions to perform this operation.";
 | 
			
		||||
    private static final int NUMBER_OF_FILES = 5;
 | 
			
		||||
    private final List<FileModel> addedFiles = new ArrayList<>();
 | 
			
		||||
    private final List<UserModel> users = new ArrayList<>();
 | 
			
		||||
    private final List<Hold> holds = new ArrayList<>();
 | 
			
		||||
    private Hold hold;
 | 
			
		||||
    private Hold hold2;
 | 
			
		||||
    private Hold hold3;
 | 
			
		||||
    private FolderModel rootFolder;
 | 
			
		||||
    private HoldBulkOperation holdBulkOperation;
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private RoleService roleService;
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private ContentActions contentActions;
 | 
			
		||||
 | 
			
		||||
    @BeforeClass(alwaysRun = true)
 | 
			
		||||
    public void preconditionForAddContentToHold()
 | 
			
		||||
    {
 | 
			
		||||
        STEP("Create a hold.");
 | 
			
		||||
        hold = getRestAPIFactory().getFilePlansAPI(getAdminUser()).createHold(
 | 
			
		||||
            Hold.builder().name("HOLD" + generateTestPrefix(AddToHoldsV1Tests.class)).description(HOLD_DESCRIPTION)
 | 
			
		||||
                .reason(HOLD_REASON).build(), FILE_PLAN_ALIAS);
 | 
			
		||||
        holds.add(hold);
 | 
			
		||||
 | 
			
		||||
        STEP("Create test files.");
 | 
			
		||||
        testSite = dataSite.usingAdmin().createPublicRandomSite();
 | 
			
		||||
 | 
			
		||||
        rootFolder = dataContent.usingAdmin().usingSite(testSite).createFolder();
 | 
			
		||||
        FolderModel folder1 = dataContent.usingAdmin().usingResource(rootFolder).createFolder();
 | 
			
		||||
        FolderModel folder2 = dataContent.usingAdmin().usingResource(folder1).createFolder();
 | 
			
		||||
 | 
			
		||||
        // Add files to subfolders in the site
 | 
			
		||||
        for (int i = 0; i < NUMBER_OF_FILES; i++)
 | 
			
		||||
        {
 | 
			
		||||
            FileModel documentHeld = dataContent.usingAdmin()
 | 
			
		||||
                .usingResource(i % 2 == 0 ? folder1 : folder2)
 | 
			
		||||
                .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
 | 
			
		||||
            addedFiles.add(documentHeld);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        RestRequestQueryModel queryReq = getContentFromSiteQuery(testSite.getId());
 | 
			
		||||
        SearchRequest searchRequest = new SearchRequest();
 | 
			
		||||
        searchRequest.setQuery(queryReq);
 | 
			
		||||
 | 
			
		||||
        STEP("Wait until all files are searchable.");
 | 
			
		||||
        await().atMost(30, TimeUnit.SECONDS)
 | 
			
		||||
            .until(() -> getRestAPIFactory().getSearchAPI(null).search(searchRequest).getPagination()
 | 
			
		||||
                .getTotalItems() == NUMBER_OF_FILES);
 | 
			
		||||
 | 
			
		||||
        holdBulkOperation = HoldBulkOperation.builder()
 | 
			
		||||
            .query(queryReq)
 | 
			
		||||
            .op(HoldBulkOperationType.ADD).build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user with the add to hold capability and hold filing permission
 | 
			
		||||
     * When the user adds content from a site to a hold using the bulk API
 | 
			
		||||
     * Then the content is added to the hold and the status of the bulk operation is DONE
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void addContentFromTestSiteToHoldUsingBulkAPI()
 | 
			
		||||
    {
 | 
			
		||||
        UserModel userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
 | 
			
		||||
            UserRole.SiteCollaborator, hold.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
 | 
			
		||||
        users.add(userAddHoldPermission);
 | 
			
		||||
 | 
			
		||||
        STEP("Add content from the site to the hold using the bulk API.");
 | 
			
		||||
        HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .startBulkProcess(holdBulkOperation, hold.getId());
 | 
			
		||||
 | 
			
		||||
        // Verify the status code
 | 
			
		||||
        assertStatusCode(ACCEPTED);
 | 
			
		||||
        assertEquals(NUMBER_OF_FILES, bulkOperationEntry.getTotalItems());
 | 
			
		||||
 | 
			
		||||
        STEP("Wait until all files are added to the hold.");
 | 
			
		||||
        await().atMost(20, TimeUnit.SECONDS).until(
 | 
			
		||||
            () -> getRestAPIFactory().getHoldsAPI(getAdminUser()).getChildren(hold.getId()).getEntries().size()
 | 
			
		||||
                == NUMBER_OF_FILES);
 | 
			
		||||
        List<String> holdChildrenNodeRefs = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .getChildren(hold.getId()).getEntries().stream().map(HoldChildEntry::getEntry).map(
 | 
			
		||||
                HoldChild::getId).toList();
 | 
			
		||||
        assertEquals(addedFiles.stream().map(FileModel::getNodeRefWithoutVersion).sorted().toList(),
 | 
			
		||||
            holdChildrenNodeRefs.stream().sorted().toList());
 | 
			
		||||
 | 
			
		||||
        STEP("Check the bulk status.");
 | 
			
		||||
        HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .getBulkStatus(hold.getId(), bulkOperationEntry.getBulkStatusId());
 | 
			
		||||
        assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, 0, null);
 | 
			
		||||
 | 
			
		||||
        STEP("Check the bulk statuses.");
 | 
			
		||||
        HoldBulkStatusCollection holdBulkStatusCollection = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .getBulkStatuses(hold.getId());
 | 
			
		||||
        assertEquals(Arrays.asList(holdBulkStatus), holdBulkStatusCollection.getEntries().stream().map(HoldBulkStatusEntry::getEntry).toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user with the add to hold capability and hold filing permission
 | 
			
		||||
     * When the user adds content from a folder and all subfolders to a hold using the bulk API
 | 
			
		||||
     * Then the content is added to the hold and the status of the bulk operation is DONE
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void addContentFromFolderAndAllSubfoldersToHoldUsingBulkAPI()
 | 
			
		||||
    {
 | 
			
		||||
        hold3 = getRestAPIFactory().getFilePlansAPI(getAdminUser()).createHold(
 | 
			
		||||
            Hold.builder().name("HOLD" + generateTestPrefix(AddToHoldsV1Tests.class)).description(HOLD_DESCRIPTION)
 | 
			
		||||
                .reason(HOLD_REASON).build(), FILE_PLAN_ALIAS);
 | 
			
		||||
        holds.add(hold3);
 | 
			
		||||
 | 
			
		||||
        UserModel userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
 | 
			
		||||
            UserRole.SiteCollaborator, hold3.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
 | 
			
		||||
        users.add(userAddHoldPermission);
 | 
			
		||||
 | 
			
		||||
        STEP("Add content from the site to the hold using the bulk API.");
 | 
			
		||||
        // Get content from folder and all subfolders of the root folder
 | 
			
		||||
        HoldBulkOperation bulkOperation = HoldBulkOperation.builder()
 | 
			
		||||
            .query(getContentFromFolderAndAllSubfoldersQuery(rootFolder.getNodeRefWithoutVersion()))
 | 
			
		||||
            .op(HoldBulkOperationType.ADD).build();
 | 
			
		||||
        HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .startBulkProcess(bulkOperation, hold3.getId());
 | 
			
		||||
 | 
			
		||||
        // Verify the status code
 | 
			
		||||
        assertStatusCode(ACCEPTED);
 | 
			
		||||
        assertEquals(NUMBER_OF_FILES, bulkOperationEntry.getTotalItems());
 | 
			
		||||
 | 
			
		||||
        STEP("Wait until all files are added to the hold.");
 | 
			
		||||
        await().atMost(20, TimeUnit.SECONDS).until(
 | 
			
		||||
            () -> getRestAPIFactory().getHoldsAPI(getAdminUser()).getChildren(hold3.getId()).getEntries().size()
 | 
			
		||||
                == NUMBER_OF_FILES);
 | 
			
		||||
        List<String> holdChildrenNodeRefs = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .getChildren(hold3.getId()).getEntries().stream().map(HoldChildEntry::getEntry).map(
 | 
			
		||||
                HoldChild::getId).toList();
 | 
			
		||||
        assertEquals(addedFiles.stream().map(FileModel::getNodeRefWithoutVersion).sorted().toList(),
 | 
			
		||||
            holdChildrenNodeRefs.stream().sorted().toList());
 | 
			
		||||
 | 
			
		||||
        STEP("Check the bulk status.");
 | 
			
		||||
        HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .getBulkStatus(hold3.getId(), bulkOperationEntry.getBulkStatusId());
 | 
			
		||||
        assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, 0, null);
 | 
			
		||||
 | 
			
		||||
        STEP("Check the bulk statuses.");
 | 
			
		||||
        HoldBulkStatusCollection holdBulkStatusCollection = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .getBulkStatuses(hold3.getId());
 | 
			
		||||
        assertEquals(Arrays.asList(holdBulkStatus), holdBulkStatusCollection.getEntries().stream().map(HoldBulkStatusEntry::getEntry).toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user without the add to hold capability
 | 
			
		||||
     * When the user adds content from a site to a hold using the bulk API
 | 
			
		||||
     * Then the user receives access denied error
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testBulkProcessWithUserWithoutAddToHoldCapability()
 | 
			
		||||
    {
 | 
			
		||||
        UserModel userWithoutAddToHoldCapability = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
 | 
			
		||||
            UserRole
 | 
			
		||||
                .SiteCollaborator,
 | 
			
		||||
            hold.getId(), UserRoles.ROLE_RM_POWER_USER, PERMISSION_FILING);
 | 
			
		||||
        users.add(userWithoutAddToHoldCapability);
 | 
			
		||||
 | 
			
		||||
        STEP("Add content from the site to the hold using the bulk API.");
 | 
			
		||||
        getRestAPIFactory().getHoldsAPI(userWithoutAddToHoldCapability)
 | 
			
		||||
            .startBulkProcess(holdBulkOperation, hold.getId());
 | 
			
		||||
 | 
			
		||||
        STEP("Verify the response status code and the error message.");
 | 
			
		||||
        assertStatusCode(FORBIDDEN);
 | 
			
		||||
        getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(ACCESS_DENIED_ERROR_MESSAGE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user without the filing permission on a hold
 | 
			
		||||
     * When the user adds content from a site to a hold using the bulk API
 | 
			
		||||
     * Then the user receives access denied error
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testBulkProcessWithUserWithoutFilingPermissionOnAHold()
 | 
			
		||||
    {
 | 
			
		||||
        // User without filing permission on a hold
 | 
			
		||||
        UserModel userWithoutPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
 | 
			
		||||
            UserRole.SiteCollaborator, hold.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_READ_RECORDS);
 | 
			
		||||
        users.add(userWithoutPermission);
 | 
			
		||||
 | 
			
		||||
        STEP("Add content from the site to the hold using the bulk API.");
 | 
			
		||||
        getRestAPIFactory().getHoldsAPI(userWithoutPermission)
 | 
			
		||||
            .startBulkProcess(holdBulkOperation, hold.getId());
 | 
			
		||||
 | 
			
		||||
        STEP("Verify the response status code and the error message.");
 | 
			
		||||
        assertStatusCode(FORBIDDEN);
 | 
			
		||||
        getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(ACCESS_DENIED_ERROR_MESSAGE);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user without the write permission on all the content
 | 
			
		||||
     * When the user adds content from a site to a hold using the bulk API
 | 
			
		||||
     * Then all processed items are marked as errors and the last error message contains access denied error
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testBulkProcessWithUserWithoutWritePermissionOnTheContent()
 | 
			
		||||
    {
 | 
			
		||||
        // User without write permission on the content
 | 
			
		||||
        UserModel userWithoutPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(
 | 
			
		||||
            testSite, UserRole.SiteConsumer,
 | 
			
		||||
            hold.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
 | 
			
		||||
        users.add(userWithoutPermission);
 | 
			
		||||
 | 
			
		||||
        // Wait until permissions are reverted
 | 
			
		||||
        SearchRequest searchRequest = new SearchRequest();
 | 
			
		||||
        searchRequest.setQuery(holdBulkOperation.getQuery());
 | 
			
		||||
        await().atMost(30, TimeUnit.SECONDS)
 | 
			
		||||
            .until(() -> getRestAPIFactory().getSearchAPI(userWithoutPermission).search(searchRequest).getPagination()
 | 
			
		||||
                .getTotalItems() == NUMBER_OF_FILES);
 | 
			
		||||
 | 
			
		||||
        STEP("Add content from the site to the hold using the bulk API.");
 | 
			
		||||
        HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(
 | 
			
		||||
            userWithoutPermission).startBulkProcess(holdBulkOperation, hold.getId());
 | 
			
		||||
 | 
			
		||||
        STEP("Verify the response.");
 | 
			
		||||
        assertStatusCode(ACCEPTED);
 | 
			
		||||
 | 
			
		||||
        await().atMost(20, TimeUnit.SECONDS).until(() ->
 | 
			
		||||
            getRestAPIFactory().getHoldsAPI(userWithoutPermission)
 | 
			
		||||
                .getBulkStatus(hold.getId(), bulkOperationEntry.getBulkStatusId()).getStatus() == Status.DONE);
 | 
			
		||||
 | 
			
		||||
        HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userWithoutPermission)
 | 
			
		||||
            .getBulkStatus(hold.getId(), bulkOperationEntry.getBulkStatusId());
 | 
			
		||||
        assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, NUMBER_OF_FILES, ACCESS_DENIED_ERROR_MESSAGE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user without the write permission on one file
 | 
			
		||||
     * When the user adds content from a site to a hold using the bulk API
 | 
			
		||||
     * Then all processed items are added to the hold except the one that the user does not have write permission
 | 
			
		||||
     * And the status of the bulk operation is DONE, contains the error message and the number of errors is 1
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testBulkProcessWithUserWithoutWritePermissionOnOneFile()
 | 
			
		||||
    {
 | 
			
		||||
        hold2 = getRestAPIFactory().getFilePlansAPI(getAdminUser()).createHold(
 | 
			
		||||
            Hold.builder().name("HOLD" + generateTestPrefix(AddToHoldsV1Tests.class)).description(HOLD_DESCRIPTION)
 | 
			
		||||
                .reason(HOLD_REASON).build(), FILE_PLAN_ALIAS);
 | 
			
		||||
        holds.add(hold2);
 | 
			
		||||
 | 
			
		||||
        UserModel userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
 | 
			
		||||
            UserRole.SiteCollaborator, hold2.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
 | 
			
		||||
        users.add(userAddHoldPermission);
 | 
			
		||||
 | 
			
		||||
        contentActions.setPermissionForUser(getAdminUser().getUsername(), getAdminUser().getPassword(),
 | 
			
		||||
            testSite.getId(), addedFiles.get(0).getName(), userAddHoldPermission.getUsername(),
 | 
			
		||||
            UserRole.SiteConsumer.getRoleId(), false);
 | 
			
		||||
 | 
			
		||||
        STEP("Add content from the site to the hold using the bulk API.");
 | 
			
		||||
        HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .startBulkProcess(holdBulkOperation, hold2.getId());
 | 
			
		||||
 | 
			
		||||
        // Verify the status code
 | 
			
		||||
        assertStatusCode(ACCEPTED);
 | 
			
		||||
        assertEquals(NUMBER_OF_FILES, bulkOperationEntry.getTotalItems());
 | 
			
		||||
 | 
			
		||||
        STEP("Wait until all files are added to the hold.");
 | 
			
		||||
        await().atMost(30, TimeUnit.SECONDS).until(
 | 
			
		||||
            () -> getRestAPIFactory().getHoldsAPI(getAdminUser()).getChildren(hold2.getId()).getEntries().size()
 | 
			
		||||
                == NUMBER_OF_FILES - 1);
 | 
			
		||||
        await().atMost(30, TimeUnit.SECONDS).until(
 | 
			
		||||
            () -> getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
                .getBulkStatus(hold2.getId(), bulkOperationEntry.getBulkStatusId()).getProcessedItems() == NUMBER_OF_FILES);
 | 
			
		||||
        List<String> holdChildrenNodeRefs = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .getChildren(hold2.getId()).getEntries().stream().map(HoldChildEntry::getEntry).map(
 | 
			
		||||
                HoldChild::getId).toList();
 | 
			
		||||
        assertEquals(addedFiles.stream().skip(1).map(FileModel::getNodeRefWithoutVersion).sorted().toList(),
 | 
			
		||||
            holdChildrenNodeRefs.stream().sorted().toList());
 | 
			
		||||
 | 
			
		||||
        STEP("Check the bulk status.");
 | 
			
		||||
        HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .getBulkStatus(hold2.getId(), bulkOperationEntry.getBulkStatusId());
 | 
			
		||||
        assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, 1, ACCESS_DENIED_ERROR_MESSAGE);
 | 
			
		||||
 | 
			
		||||
        STEP("Check the bulk statuses.");
 | 
			
		||||
        HoldBulkStatusCollection holdBulkStatusCollection = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
 | 
			
		||||
            .getBulkStatuses(hold2.getId());
 | 
			
		||||
        assertEquals(Arrays.asList(holdBulkStatus), holdBulkStatusCollection.getEntries().stream().map(HoldBulkStatusEntry::getEntry).toList());
 | 
			
		||||
 | 
			
		||||
        // Revert the permissions
 | 
			
		||||
        contentActions.setPermissionForUser(getAdminUser().getUsername(), getAdminUser().getPassword(),
 | 
			
		||||
            testSite.getId(), addedFiles.get(0).getName(), userAddHoldPermission.getUsername(),
 | 
			
		||||
            UserRole.SiteCollaborator.getRoleId(), true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given an unauthenticated user
 | 
			
		||||
     * When the user adds content from a site to a hold using the bulk API
 | 
			
		||||
     * Then the user receives unauthorized error
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testBulkProcessAsUnauthenticatedUser()
 | 
			
		||||
    {
 | 
			
		||||
        STEP("Start bulk process as unauthenticated user");
 | 
			
		||||
        getRestAPIFactory().getHoldsAPI(new UserModel(getAdminUser().getUsername(), "wrongPassword"))
 | 
			
		||||
            .startBulkProcess(holdBulkOperation, hold.getId());
 | 
			
		||||
 | 
			
		||||
        STEP("Verify the response status code.");
 | 
			
		||||
        assertStatusCode(UNAUTHORIZED);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user with the add to hold capability and hold filing permission
 | 
			
		||||
     * When the user adds content from a site to a hold using the bulk API
 | 
			
		||||
     * And the hold does not exist
 | 
			
		||||
     * Then the user receives not found error
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testBulkProcessForNonExistentHold()
 | 
			
		||||
    {
 | 
			
		||||
        STEP("Start bulk process for non existent hold");
 | 
			
		||||
        getRestAPIFactory().getHoldsAPI(getAdminUser()).startBulkProcess(holdBulkOperation, "nonExistentHoldId");
 | 
			
		||||
 | 
			
		||||
        STEP("Verify the response status code.");
 | 
			
		||||
        assertStatusCode(NOT_FOUND);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user with the add to hold capability and hold filing permission
 | 
			
		||||
     * When the user adds content from a site to a hold using the bulk API
 | 
			
		||||
     * and the bulk operation is invalid
 | 
			
		||||
     * Then the user receives bad request error
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testGetBulkStatusesForInvalidOperation()
 | 
			
		||||
    {
 | 
			
		||||
        STEP("Start bulk process for non existent hold");
 | 
			
		||||
 | 
			
		||||
        HoldBulkOperation invalidHoldBulkOperation = HoldBulkOperation.builder().op(null)
 | 
			
		||||
            .query(holdBulkOperation.getQuery()).build();
 | 
			
		||||
        getRestAPIFactory().getHoldsAPI(getAdminUser()).startBulkProcess(invalidHoldBulkOperation, hold.getId());
 | 
			
		||||
 | 
			
		||||
        STEP("Verify the response status code.");
 | 
			
		||||
        assertStatusCode(BAD_REQUEST);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user with the add to hold capability and hold filing permission
 | 
			
		||||
     * When the user adds content from a site to a hold using the bulk API
 | 
			
		||||
     * And the hold does not exist
 | 
			
		||||
     * Then the user receives not found error
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testGetBulkStatusForNonExistentHold()
 | 
			
		||||
    {
 | 
			
		||||
        STEP("Start bulk process for non existent hold");
 | 
			
		||||
        getRestAPIFactory().getHoldsAPI(getAdminUser()).getBulkStatus("nonExistentHoldId", "nonExistenBulkStatusId");
 | 
			
		||||
 | 
			
		||||
        STEP("Verify the response status code.");
 | 
			
		||||
        assertStatusCode(NOT_FOUND);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user with the add to hold capability and hold filing permission
 | 
			
		||||
     * When the user adds content from a site to a hold using the bulk API
 | 
			
		||||
     * And the bulk status does not exist
 | 
			
		||||
     * Then the user receives not found error
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testGetBulkStatusForNonExistentBulkStatus()
 | 
			
		||||
    {
 | 
			
		||||
        STEP("Start bulk process for non bulk status");
 | 
			
		||||
        getRestAPIFactory().getHoldsAPI(getAdminUser()).getBulkStatus(hold.getId(), "nonExistenBulkStatusId");
 | 
			
		||||
 | 
			
		||||
        STEP("Verify the response status code.");
 | 
			
		||||
        assertStatusCode(NOT_FOUND);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user with the add to hold capability and hold filing permission
 | 
			
		||||
     * When the user adds content from a site to a hold using the bulk API
 | 
			
		||||
     * And the hold does not exist
 | 
			
		||||
     * Then the user receives not found error
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testGetBulkStatusesForNonExistentHold()
 | 
			
		||||
    {
 | 
			
		||||
        STEP("Start bulk process for non existent hold");
 | 
			
		||||
        getRestAPIFactory().getHoldsAPI(getAdminUser()).getBulkStatuses("nonExistentHoldId");
 | 
			
		||||
 | 
			
		||||
        STEP("Verify the response status code.");
 | 
			
		||||
        assertStatusCode(NOT_FOUND);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a user with the add to hold capability and hold filing permission
 | 
			
		||||
     * When the user adds content from all sites to a hold using the bulk API to exceed the limit (30 items)
 | 
			
		||||
     * Then the user receives bad request error
 | 
			
		||||
     */
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testExceedingBulkOperationLimit()
 | 
			
		||||
    {
 | 
			
		||||
        RestRequestQueryModel queryReq = new RestRequestQueryModel();
 | 
			
		||||
        queryReq.setQuery("TYPE:content");
 | 
			
		||||
        queryReq.setLanguage("afts");
 | 
			
		||||
 | 
			
		||||
        HoldBulkOperation exceedLimitOp = HoldBulkOperation.builder()
 | 
			
		||||
            .query(queryReq)
 | 
			
		||||
            .op(HoldBulkOperationType.ADD).build();
 | 
			
		||||
 | 
			
		||||
        STEP("Start bulk process to exceed the limit");
 | 
			
		||||
        getRestAPIFactory().getHoldsAPI(getAdminUser()).startBulkProcess(exceedLimitOp, hold.getId());
 | 
			
		||||
 | 
			
		||||
        STEP("Verify the response status code.");
 | 
			
		||||
        assertStatusCode(BAD_REQUEST);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private void assertBulkProcessStatus(HoldBulkStatus holdBulkStatus, long expectedProcessedItems,
 | 
			
		||||
        int expectedErrorsCount, String expectedErrorMessage)
 | 
			
		||||
    {
 | 
			
		||||
        assertEquals(Status.DONE, holdBulkStatus.getStatus());
 | 
			
		||||
        assertEquals(expectedProcessedItems, holdBulkStatus.getTotalItems());
 | 
			
		||||
        assertEquals(expectedProcessedItems, holdBulkStatus.getProcessedItems());
 | 
			
		||||
        assertEquals(expectedErrorsCount, holdBulkStatus.getErrorsCount());
 | 
			
		||||
        assertNotNull(holdBulkStatus.getStartTime());
 | 
			
		||||
        assertNotNull(holdBulkStatus.getEndTime());
 | 
			
		||||
 | 
			
		||||
        if (expectedErrorMessage != null)
 | 
			
		||||
        {
 | 
			
		||||
            assertTrue(holdBulkStatus.getLastError().contains(expectedErrorMessage));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private RestRequestQueryModel getContentFromSiteQuery(String siteId)
 | 
			
		||||
    {
 | 
			
		||||
        RestRequestQueryModel queryReq = new RestRequestQueryModel();
 | 
			
		||||
        queryReq.setQuery("SITE:\"" + siteId + "\" and TYPE:content");
 | 
			
		||||
        queryReq.setLanguage("afts");
 | 
			
		||||
        return queryReq;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private RestRequestQueryModel getContentFromFolderAndAllSubfoldersQuery(String folderId)
 | 
			
		||||
    {
 | 
			
		||||
        RestRequestQueryModel queryReq = new RestRequestQueryModel();
 | 
			
		||||
        queryReq.setQuery("ANCESTOR:\"workspace://SpacesStore/" + folderId + "\" and TYPE:content");
 | 
			
		||||
        queryReq.setLanguage("afts");
 | 
			
		||||
        return queryReq;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @AfterClass(alwaysRun = true)
 | 
			
		||||
    public void cleanupAddToHoldsBulkV1Tests()
 | 
			
		||||
    {
 | 
			
		||||
        dataSite.usingAdmin().deleteSite(testSite);
 | 
			
		||||
        users.forEach(user -> getDataUser().usingAdmin().deleteUser(user));
 | 
			
		||||
        holds.forEach(hold -> getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(hold.getId()));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -139,3 +139,21 @@ content.metadata.async.extract.6.enabled=false
 | 
			
		||||
 | 
			
		||||
# Max number of entries returned in Record search view
 | 
			
		||||
rm.recordSearch.maxItems=500
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Hold bulk
 | 
			
		||||
#
 | 
			
		||||
# The number of worker threads.
 | 
			
		||||
rm.hold.bulk.threadCount=2
 | 
			
		||||
# The maximum number of total items to process in a single bulk operation.
 | 
			
		||||
rm.hold.bulk.maxItems=1000
 | 
			
		||||
# The number of entries to be fetched from the Search Service as a next set of work object to process.
 | 
			
		||||
rm.hold.bulk.batchSize=100
 | 
			
		||||
# The number of entries to process before reporting progress.
 | 
			
		||||
rm.hold.bulk.logging.interval=100
 | 
			
		||||
# The number of entries we process at a time in a transaction.
 | 
			
		||||
rm.hold.bulk.itemsPerTransaction=1
 | 
			
		||||
 | 
			
		||||
cache.bulkHoldStatusCache.cluster.type=fully-distributed
 | 
			
		||||
cache.bulkHoldRegistryCache.cluster.type=fully-distributed
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -89,6 +89,9 @@
 | 
			
		||||
   <!-- Import RM Audit -->
 | 
			
		||||
   <import resource="classpath:alfresco/module/org_alfresco_module_rm/rm-audit-context.xml"/>
 | 
			
		||||
 | 
			
		||||
   <!-- Import RM Bulk -->
 | 
			
		||||
   <import resource="classpath:alfresco/module/org_alfresco_module_rm/rm-bulk-context.xml"/>
 | 
			
		||||
 | 
			
		||||
   <!--  Import RM query context -->
 | 
			
		||||
   <import resource="classpath:alfresco/module/org_alfresco_module_rm/query/rm-query-context.xml" />
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<beans xmlns="http://www.springframework.org/schema/beans"
 | 
			
		||||
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
       xmlns:context="http://www.springframework.org/schema/context"
 | 
			
		||||
       xsi:schemaLocation="http://www.springframework.org/schema/beans
 | 
			
		||||
                        http://www.springframework.org/schema/beans/spring-beans.xsd
 | 
			
		||||
                        http://www.springframework.org/schema/context
 | 
			
		||||
                        http://www.springframework.org/schema/context/spring-context.xsd">
 | 
			
		||||
 | 
			
		||||
   <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="loggingInterval">
 | 
			
		||||
         <value>${rm.hold.bulk.logging.interval}</value>
 | 
			
		||||
      </property>
 | 
			
		||||
      <property name="itemsPerTransaction">
 | 
			
		||||
         <value>${rm.hold.bulk.itemsPerTransaction}</value>
 | 
			
		||||
      </property>
 | 
			
		||||
   </bean>
 | 
			
		||||
 | 
			
		||||
   <bean id="holdBulkMonitor" class="org.alfresco.module.org_alfresco_module_rm.bulk.hold.DefaultHoldBulkMonitor">
 | 
			
		||||
      <property name="holdProgressCache" ref="holdProgressCache" />
 | 
			
		||||
      <property name="holdProcessRegistry" ref="holdProcessRegistry" />
 | 
			
		||||
   </bean>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
   <bean name="holdProgressCache" factory-bean="cacheFactory" factory-method="createCache">
 | 
			
		||||
      <constructor-arg value="cache.bulkHoldStatusCache" />
 | 
			
		||||
   </bean>
 | 
			
		||||
 | 
			
		||||
   <bean name="holdProcessRegistry" factory-bean="cacheFactory" factory-method="createCache">
 | 
			
		||||
      <constructor-arg value="cache.bulkHoldRegistryCache" />
 | 
			
		||||
   </bean>
 | 
			
		||||
 | 
			
		||||
</beans>
 | 
			
		||||
@@ -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">
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,8 @@ services:
 | 
			
		||||
                -Daos.baseUrlOverwrite=http://localhost:8080/alfresco/aos
 | 
			
		||||
                -Dmessaging.broker.url=\"failover:(tcp://activemq:61616)?timeout=3000&jms.useCompression=true\"
 | 
			
		||||
                -DlocalTransform.core-aio.url=http://transform-core-aio:8090/
 | 
			
		||||
                -Drm.hold.bulk.maxItems=5
 | 
			
		||||
                -Drm.hold.bulk.batchSize=2
 | 
			
		||||
                "
 | 
			
		||||
        ports:
 | 
			
		||||
            - 8080:8080
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,249 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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 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.rest.framework.core.exceptions.InvalidArgumentException;
 | 
			
		||||
import org.alfresco.service.ServiceRegistry;
 | 
			
		||||
import org.alfresco.service.cmr.repository.NodeRef;
 | 
			
		||||
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.transaction.TransactionService;
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.beans.factory.InitializingBean;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A base class for executing bulk operations on nodes based on search query results
 | 
			
		||||
 */
 | 
			
		||||
public abstract class BulkBaseService<T> implements InitializingBean
 | 
			
		||||
{
 | 
			
		||||
    private static final Log LOG = LogFactory.getLog(BulkBaseService.class);
 | 
			
		||||
 | 
			
		||||
    protected ServiceRegistry serviceRegistry;
 | 
			
		||||
    protected SearchService searchService;
 | 
			
		||||
    protected TransactionService transactionService;
 | 
			
		||||
    protected SearchMapper searchMapper;
 | 
			
		||||
    protected BulkMonitor<T> bulkMonitor;
 | 
			
		||||
 | 
			
		||||
    protected int threadCount;
 | 
			
		||||
    protected int batchSize;
 | 
			
		||||
    protected int itemsPerTransaction;
 | 
			
		||||
    protected int maxItems;
 | 
			
		||||
    protected int loggingInterval;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void afterPropertiesSet() throws Exception
 | 
			
		||||
    {
 | 
			
		||||
        this.searchService = serviceRegistry.getSearchService();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute bulk operation on node based on the search query results
 | 
			
		||||
     *
 | 
			
		||||
     * @param nodeRef       node reference
 | 
			
		||||
     * @param bulkOperation bulk operation
 | 
			
		||||
     * @return bulk status
 | 
			
		||||
     */
 | 
			
		||||
    public T execute(NodeRef nodeRef, BulkOperation bulkOperation)
 | 
			
		||||
    {
 | 
			
		||||
        checkPermissions(nodeRef, bulkOperation);
 | 
			
		||||
 | 
			
		||||
        ResultSet resultSet = getTotalItems(bulkOperation.searchQuery(), maxItems);
 | 
			
		||||
        if (maxItems < resultSet.getNumberFound() || resultSet.hasMore())
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidArgumentException("Too many items to process. Please refine your query.");
 | 
			
		||||
        }
 | 
			
		||||
        long totalItems = resultSet.getNumberFound();
 | 
			
		||||
        // Generate a random process id
 | 
			
		||||
        String processId = UUID.randomUUID().toString();
 | 
			
		||||
 | 
			
		||||
        T initBulkStatus = getInitBulkStatus(processId, totalItems);
 | 
			
		||||
        bulkMonitor.updateBulkStatus(initBulkStatus);
 | 
			
		||||
        bulkMonitor.registerProcess(nodeRef, processId);
 | 
			
		||||
 | 
			
		||||
        BatchProcessWorker<NodeRef> batchProcessWorker = getWorkerProvider(nodeRef, bulkOperation);
 | 
			
		||||
        BulkStatusUpdater bulkStatusUpdater = getBulkStatusUpdater();
 | 
			
		||||
 | 
			
		||||
        BatchProcessor<NodeRef> batchProcessor = new BatchProcessor<>(
 | 
			
		||||
            processId,
 | 
			
		||||
            transactionService.getRetryingTransactionHelper(),
 | 
			
		||||
            getWorkProvider(bulkOperation, totalItems, bulkStatusUpdater),
 | 
			
		||||
            threadCount,
 | 
			
		||||
            itemsPerTransaction,
 | 
			
		||||
            bulkStatusUpdater,
 | 
			
		||||
            LOG,
 | 
			
		||||
            loggingInterval);
 | 
			
		||||
 | 
			
		||||
        runAsyncBatchProcessor(batchProcessor, batchProcessWorker, bulkStatusUpdater);
 | 
			
		||||
        return initBulkStatus;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Run batch processor
 | 
			
		||||
     */
 | 
			
		||||
    protected void runAsyncBatchProcessor(BatchProcessor<NodeRef> batchProcessor,
 | 
			
		||||
        BatchProcessWorker<NodeRef> batchProcessWorker, BulkStatusUpdater bulkStatusUpdater)
 | 
			
		||||
    {
 | 
			
		||||
        Runnable backgroundLogic = () -> {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (LOG.isDebugEnabled())
 | 
			
		||||
                {
 | 
			
		||||
                    LOG.debug("Started processing batch with name: " + batchProcessor.getProcessName());
 | 
			
		||||
                }
 | 
			
		||||
                batchProcessor.processLong(batchProcessWorker, true);
 | 
			
		||||
                if (LOG.isDebugEnabled())
 | 
			
		||||
                {
 | 
			
		||||
                    LOG.debug("Processing batch with name: " + batchProcessor.getProcessName() + " completed");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception exception)
 | 
			
		||||
            {
 | 
			
		||||
                LOG.error("Error processing batch with name: " + batchProcessor.getProcessName(), exception);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                bulkStatusUpdater.update();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Thread backgroundThread = new Thread(backgroundLogic, "BulkBaseService-BackgroundThread");
 | 
			
		||||
        backgroundThread.start();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get initial bulk status
 | 
			
		||||
     *
 | 
			
		||||
     * @param processId  process id
 | 
			
		||||
     * @param totalItems total items
 | 
			
		||||
     * @return bulk status
 | 
			
		||||
     */
 | 
			
		||||
    protected abstract T getInitBulkStatus(String processId, long totalItems);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get bulk status updater
 | 
			
		||||
     *
 | 
			
		||||
     * @return bulk status updater
 | 
			
		||||
     */
 | 
			
		||||
    protected abstract BulkStatusUpdater getBulkStatusUpdater();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get work provider
 | 
			
		||||
     *
 | 
			
		||||
     * @param bulkOperation     bulk operation
 | 
			
		||||
     * @param totalItems        total items
 | 
			
		||||
     * @param bulkStatusUpdater bulk status updater
 | 
			
		||||
     * @return work provider
 | 
			
		||||
     */
 | 
			
		||||
    protected abstract BatchProcessWorkProvider<NodeRef> getWorkProvider(BulkOperation bulkOperation, long totalItems,
 | 
			
		||||
        BulkStatusUpdater bulkStatusUpdater);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get worker provider
 | 
			
		||||
     *
 | 
			
		||||
     * @param nodeRef       node reference
 | 
			
		||||
     * @param bulkOperation bulk operation
 | 
			
		||||
     * @return worker provider
 | 
			
		||||
     */
 | 
			
		||||
    protected abstract BatchProcessWorker<NodeRef> getWorkerProvider(NodeRef nodeRef, BulkOperation bulkOperation);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check permissions
 | 
			
		||||
     *
 | 
			
		||||
     * @param nodeRef       node reference
 | 
			
		||||
     * @param bulkOperation bulk operation
 | 
			
		||||
     */
 | 
			
		||||
    protected abstract void checkPermissions(NodeRef nodeRef, BulkOperation bulkOperation);
 | 
			
		||||
 | 
			
		||||
    protected ResultSet getTotalItems(Query searchQuery, int skipCount)
 | 
			
		||||
    {
 | 
			
		||||
        SearchParameters searchParams = new SearchParameters();
 | 
			
		||||
        searchMapper.setDefaults(searchParams);
 | 
			
		||||
        searchMapper.fromQuery(searchParams, searchQuery);
 | 
			
		||||
        searchParams.setSkipCount(skipCount);
 | 
			
		||||
        searchParams.setMaxItems(1);
 | 
			
		||||
        return searchService.query(searchParams);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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(int maxItems)
 | 
			
		||||
    {
 | 
			
		||||
        this.maxItems = maxItems;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setLoggingInterval(int loggingInterval)
 | 
			
		||||
    {
 | 
			
		||||
        this.loggingInterval = loggingInterval;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setItemsPerTransaction(int itemsPerTransaction)
 | 
			
		||||
    {
 | 
			
		||||
        this.itemsPerTransaction = itemsPerTransaction;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,58 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An interface for monitoring the progress of a bulk operation
 | 
			
		||||
 */
 | 
			
		||||
public interface BulkMonitor<T>
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the bulk status
 | 
			
		||||
     *
 | 
			
		||||
     * @param bulkStatus the bulk status
 | 
			
		||||
     */
 | 
			
		||||
    void updateBulkStatus(T bulkStatus);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register a process
 | 
			
		||||
     *
 | 
			
		||||
     * @param nodeRef   the node reference
 | 
			
		||||
     * @param processId the process id
 | 
			
		||||
     */
 | 
			
		||||
    void registerProcess(NodeRef nodeRef, String processId);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the bulk status
 | 
			
		||||
     *
 | 
			
		||||
     * @param bulkStatusId the bulk status id
 | 
			
		||||
     * @return the bulk status
 | 
			
		||||
     */
 | 
			
		||||
    T getBulkStatus(String bulkStatusId);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An immutable POJO to represent a bulk operation
 | 
			
		||||
 */
 | 
			
		||||
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");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.springframework.context.ApplicationEventPublisher;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An interface for updating the status of a bulk operation
 | 
			
		||||
 */
 | 
			
		||||
public interface BulkStatusUpdater extends ApplicationEventPublisher
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the bulk status
 | 
			
		||||
     */
 | 
			
		||||
    void update();
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,109 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.repo.cache.SimpleCache;
 | 
			
		||||
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
 | 
			
		||||
import org.alfresco.service.cmr.repository.NodeRef;
 | 
			
		||||
import org.springframework.context.ApplicationEvent;
 | 
			
		||||
import org.springframework.extensions.surf.util.AbstractLifecycleBean;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default hold bulk monitor implementation
 | 
			
		||||
 */
 | 
			
		||||
public class DefaultHoldBulkMonitor extends AbstractLifecycleBean implements HoldBulkMonitor
 | 
			
		||||
{
 | 
			
		||||
    protected SimpleCache<String, HoldBulkStatus> holdProgressCache;
 | 
			
		||||
    protected SimpleCache<String, List<HoldBulkProcessDetails>> holdProcessRegistry;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void updateBulkStatus(HoldBulkStatus holdBulkStatus)
 | 
			
		||||
    {
 | 
			
		||||
        holdProgressCache.put(holdBulkStatus.bulkStatusId(), holdBulkStatus);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void registerProcess(NodeRef holdRef, String processId)
 | 
			
		||||
    {
 | 
			
		||||
        List<HoldBulkProcessDetails> processIds = Optional.ofNullable(holdProcessRegistry.get(holdRef.getId()))
 | 
			
		||||
            .orElse(new ArrayList<>());
 | 
			
		||||
        processIds.add(new HoldBulkProcessDetails(processId, null));
 | 
			
		||||
        holdProcessRegistry.put(holdRef.getId(), processIds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public HoldBulkStatus getBulkStatus(String bulkStatusId)
 | 
			
		||||
    {
 | 
			
		||||
        return holdProgressCache.get(bulkStatusId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<HoldBulkStatus> getBulkStatusesForHold(String holdId)
 | 
			
		||||
    {
 | 
			
		||||
        return Optional.ofNullable(holdProcessRegistry.get(holdId))
 | 
			
		||||
            .map(bulkProcessDetailsList -> bulkProcessDetailsList.stream()
 | 
			
		||||
                .map(HoldBulkProcessDetails::bulkStatusId)
 | 
			
		||||
                .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<HoldBulkProcessDetails>> holdProcessRegistry)
 | 
			
		||||
    {
 | 
			
		||||
        this.holdProcessRegistry = holdProcessRegistry;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onBootstrap(ApplicationEvent applicationEvent)
 | 
			
		||||
    {
 | 
			
		||||
        // NOOP
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onShutdown(ApplicationEvent applicationEvent)
 | 
			
		||||
    {
 | 
			
		||||
        // NOOP
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.List;
 | 
			
		||||
 | 
			
		||||
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkMonitor;
 | 
			
		||||
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An interface for monitoring the progress of a bulk hold operation
 | 
			
		||||
 */
 | 
			
		||||
public interface HoldBulkMonitor extends BulkMonitor<HoldBulkStatus>
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the bulk statuses for a hold
 | 
			
		||||
     *
 | 
			
		||||
     * @param holdId the hold id
 | 
			
		||||
     * @return the bulk statuses
 | 
			
		||||
     */
 | 
			
		||||
    List<HoldBulkStatus> getBulkStatusesForHold(String holdId);
 | 
			
		||||
}
 | 
			
		||||
@@ -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 java.io.Serializable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A simple immutable POJO to hold the details of a bulk hold process
 | 
			
		||||
 */
 | 
			
		||||
public record HoldBulkProcessDetails(String bulkStatusId, String creatorInstance) implements Serializable
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Interface defining a hold bulk service.
 | 
			
		||||
 */
 | 
			
		||||
public interface HoldBulkService
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Initiates a bulk operation on a hold.
 | 
			
		||||
     *
 | 
			
		||||
     * @param holdRef       The hold reference
 | 
			
		||||
     * @param bulkOperation The bulk operation
 | 
			
		||||
     */
 | 
			
		||||
    HoldBulkStatus execute(NodeRef holdRef, BulkOperation bulkOperation);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,258 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.Locale;
 | 
			
		||||
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.BulkOperation;
 | 
			
		||||
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkStatusUpdater;
 | 
			
		||||
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.BatchProcessWorker;
 | 
			
		||||
import org.alfresco.repo.security.authentication.AuthenticationUtil;
 | 
			
		||||
import org.alfresco.repo.security.permissions.AccessDeniedException;
 | 
			
		||||
import org.alfresco.rest.api.search.model.Query;
 | 
			
		||||
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
 | 
			
		||||
import org.alfresco.rm.rest.api.model.HoldBulkOperationType;
 | 
			
		||||
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.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
import org.springframework.extensions.surf.util.I18NUtil;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Implementation of the {@link HoldBulkService} interface.
 | 
			
		||||
 */
 | 
			
		||||
@SuppressWarnings("PMD.PreserveStackTrace")
 | 
			
		||||
public class HoldBulkServiceImpl extends BulkBaseService<HoldBulkStatus> implements HoldBulkService
 | 
			
		||||
{
 | 
			
		||||
    private static final Logger LOGGER = LoggerFactory.getLogger(HoldBulkServiceImpl.class);
 | 
			
		||||
 | 
			
		||||
    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 BulkStatusUpdater getBulkStatusUpdater()
 | 
			
		||||
    {
 | 
			
		||||
        return new HoldBulkStatusUpdater((HoldBulkMonitor) bulkMonitor);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected BatchProcessWorkProvider<NodeRef> getWorkProvider(BulkOperation bulkOperation, long totalItems,
 | 
			
		||||
        BulkStatusUpdater bulkStatusUpdater)
 | 
			
		||||
    {
 | 
			
		||||
        return new AddToHoldWorkerProvider(new AtomicInteger(0), bulkOperation, totalItems, bulkStatusUpdater);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected BatchProcessWorker<NodeRef> getWorkerProvider(NodeRef nodeRef, BulkOperation bulkOperation)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            HoldBulkOperationType holdBulkOperationType = HoldBulkOperationType.valueOf(bulkOperation.operationType()
 | 
			
		||||
                .toUpperCase(Locale.ENGLISH));
 | 
			
		||||
            return switch (holdBulkOperationType)
 | 
			
		||||
            {
 | 
			
		||||
                case ADD -> new AddToHoldWorkerBatch(nodeRef);
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        catch (IllegalArgumentException e)
 | 
			
		||||
        {
 | 
			
		||||
            String errorMsg = "Unsupported action type when starting the bulk process: ";
 | 
			
		||||
            if (LOGGER.isDebugEnabled())
 | 
			
		||||
            {
 | 
			
		||||
                LOGGER.debug("{} {}", errorMsg, bulkOperation.operationType(), e);
 | 
			
		||||
            }
 | 
			
		||||
            throw new InvalidArgumentException(errorMsg + 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()) && (!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;
 | 
			
		||||
        private final BulkStatusUpdater bulkStatusUpdater;
 | 
			
		||||
 | 
			
		||||
        public AddToHoldWorkerProvider(AtomicInteger currentNodeNumber, BulkOperation bulkOperation, long totalItems,
 | 
			
		||||
            BulkStatusUpdater bulkStatusUpdater)
 | 
			
		||||
        {
 | 
			
		||||
            this.currentNodeNumber = currentNodeNumber;
 | 
			
		||||
            this.searchQuery = bulkOperation.searchQuery();
 | 
			
		||||
            this.totalItems = totalItems;
 | 
			
		||||
            this.bulkStatusUpdater = bulkStatusUpdater;
 | 
			
		||||
            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 = searchService.query(searchParams);
 | 
			
		||||
            if (result.getNodeRefs().isEmpty())
 | 
			
		||||
            {
 | 
			
		||||
                return Collections.emptyList();
 | 
			
		||||
            }
 | 
			
		||||
            AuthenticationUtil.popAuthentication();
 | 
			
		||||
            if (LOGGER.isDebugEnabled())
 | 
			
		||||
            {
 | 
			
		||||
                LOGGER.debug("Processing the next work for the batch processor, skipCount={}, size={}",
 | 
			
		||||
                    searchParams.getSkipCount(), result.getNumberFound());
 | 
			
		||||
            }
 | 
			
		||||
            currentNodeNumber.addAndGet(batchSize);
 | 
			
		||||
            bulkStatusUpdater.update();
 | 
			
		||||
            return result.getNodeRefs();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private SearchParameters getNextPageParameters()
 | 
			
		||||
        {
 | 
			
		||||
            SearchParameters searchParams = new SearchParameters();
 | 
			
		||||
            searchMapper.setDefaults(searchParams);
 | 
			
		||||
            searchMapper.fromQuery(searchParams, searchQuery);
 | 
			
		||||
            searchParams.setSkipCount(currentNodeNumber.get());
 | 
			
		||||
            searchParams.setMaxItems(batchSize);
 | 
			
		||||
            searchParams.setLimit(batchSize);
 | 
			
		||||
            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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.BulkStatusUpdater;
 | 
			
		||||
import org.alfresco.repo.batch.BatchMonitor;
 | 
			
		||||
import org.alfresco.repo.batch.BatchMonitorEvent;
 | 
			
		||||
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An implementation of {@link BulkStatusUpdater} for the hold bulk operation
 | 
			
		||||
 */
 | 
			
		||||
public class HoldBulkStatusUpdater implements BulkStatusUpdater
 | 
			
		||||
{
 | 
			
		||||
    private final Runnable task;
 | 
			
		||||
    private BatchMonitor batchMonitor;
 | 
			
		||||
 | 
			
		||||
    public HoldBulkStatusUpdater(HoldBulkMonitor holdBulkMonitor)
 | 
			
		||||
    {
 | 
			
		||||
        this.task = () -> holdBulkMonitor.updateBulkStatus(
 | 
			
		||||
            new HoldBulkStatus(batchMonitor.getProcessName(), batchMonitor.getStartTime(),
 | 
			
		||||
                batchMonitor.getEndTime(),
 | 
			
		||||
                batchMonitor.getSuccessfullyProcessedEntriesLong() + batchMonitor.getTotalErrorsLong(),
 | 
			
		||||
                batchMonitor.getTotalErrorsLong(), batchMonitor.getTotalResultsLong(),
 | 
			
		||||
                batchMonitor.getLastError()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void update()
 | 
			
		||||
    {
 | 
			
		||||
        if (task != null && batchMonitor != null)
 | 
			
		||||
        {
 | 
			
		||||
            task.run();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void publishEvent(Object event)
 | 
			
		||||
    {
 | 
			
		||||
        if (event instanceof BatchMonitorEvent batchMonitorEvent)
 | 
			
		||||
        {
 | 
			
		||||
            batchMonitor = batchMonitorEvent.getBatchMonitor();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,120 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.getBulkStatusesForHold(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 bulkStatusId, Parameters parameters)
 | 
			
		||||
        throws RelationshipResourceNotFoundException
 | 
			
		||||
    {
 | 
			
		||||
        checkNotBlank("holdId", holdId);
 | 
			
		||||
        checkNotBlank("bulkStatusId", bulkStatusId);
 | 
			
		||||
        mandatory("parameters", parameters);
 | 
			
		||||
 | 
			
		||||
        NodeRef holdRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
 | 
			
		||||
 | 
			
		||||
        checkReadPermissions(holdRef);
 | 
			
		||||
 | 
			
		||||
        return Optional.ofNullable(holdBulkMonitor.getBulkStatus(bulkStatusId)).orElseThrow(() -> new EntityNotFoundException(bulkStatusId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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,9 @@ 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.HoldBulkOperationEntry;
 | 
			
		||||
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 +73,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 +163,23 @@ public class HoldsEntityResource implements
 | 
			
		||||
        return reason;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Operation("bulk")
 | 
			
		||||
    @WebApiDescription(title = "Start the hold bulk operation",
 | 
			
		||||
        successStatus = HttpServletResponse.SC_ACCEPTED)
 | 
			
		||||
    public HoldBulkOperationEntry 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);
 | 
			
		||||
 | 
			
		||||
        HoldBulkStatus holdBulkStatus = holdBulkService.execute(parentNodeRef,
 | 
			
		||||
            new BulkOperation(holdBulkOperation.query(), holdBulkOperation.op().name()));
 | 
			
		||||
        return new HoldBulkOperationEntry(holdBulkStatus.bulkStatusId(), holdBulkStatus.totalItems());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
 | 
			
		||||
    {
 | 
			
		||||
        this.apiUtils = apiUtils;
 | 
			
		||||
@@ -181,4 +204,9 @@ public class HoldsEntityResource implements
 | 
			
		||||
    {
 | 
			
		||||
        this.transactionService = transactionService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setHoldBulkService(HoldBulkService holdBulkService)
 | 
			
		||||
    {
 | 
			
		||||
        this.holdBulkService = holdBulkService;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {}
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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 record HoldBulkOperationEntry(String bulkStatusId, long totalItems){}
 | 
			
		||||
@@ -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.rm.rest.api.model;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This enum represents the types of bulk operations that can be performed on holds
 | 
			
		||||
 */
 | 
			
		||||
public enum HoldBulkOperationType
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * The ADD operation represents adding items to a hold in bulk.
 | 
			
		||||
     */
 | 
			
		||||
    ADD
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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.util.Date;
 | 
			
		||||
 | 
			
		||||
public record HoldBulkStatus(String bulkStatusId, Date startTime, Date endTime, long processedItems, 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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,119 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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 static org.junit.Assert.assertEquals;
 | 
			
		||||
import static org.mockito.Mockito.when;
 | 
			
		||||
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import org.alfresco.module.org_alfresco_module_rm.bulk.hold.DefaultHoldBulkMonitor;
 | 
			
		||||
import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkProcessDetails;
 | 
			
		||||
import org.alfresco.repo.cache.SimpleCache;
 | 
			
		||||
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
 | 
			
		||||
import org.alfresco.service.cmr.repository.NodeRef;
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.mockito.Mock;
 | 
			
		||||
import org.mockito.Mockito;
 | 
			
		||||
import org.mockito.MockitoAnnotations;
 | 
			
		||||
 | 
			
		||||
public class DefaultHoldBulkMonitorUnitTest
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    @Mock
 | 
			
		||||
    private SimpleCache<String, HoldBulkStatus> holdProgressCache;
 | 
			
		||||
 | 
			
		||||
    @Mock
 | 
			
		||||
    private SimpleCache<String, List<HoldBulkProcessDetails>> holdProcessRegistry;
 | 
			
		||||
 | 
			
		||||
    private DefaultHoldBulkMonitor holdBulkMonitor;
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void setUp()
 | 
			
		||||
    {
 | 
			
		||||
        MockitoAnnotations.openMocks(this);
 | 
			
		||||
        holdBulkMonitor = new DefaultHoldBulkMonitor();
 | 
			
		||||
        holdBulkMonitor.setHoldProgressCache(holdProgressCache);
 | 
			
		||||
        holdBulkMonitor.setHoldProcessRegistry(holdProcessRegistry);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testUpdateBulkStatus()
 | 
			
		||||
    {
 | 
			
		||||
        HoldBulkStatus status = new HoldBulkStatus("bulkStatusId", null, null, 0L, 0L, 0L, null);
 | 
			
		||||
 | 
			
		||||
        holdBulkMonitor.updateBulkStatus(status);
 | 
			
		||||
 | 
			
		||||
        Mockito.verify(holdProgressCache).put("bulkStatusId", status);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testRegisterProcess()
 | 
			
		||||
    {
 | 
			
		||||
        NodeRef holdRef = new NodeRef("workspace://SpacesStore/holdId");
 | 
			
		||||
        String processId = "processId";
 | 
			
		||||
        when(holdProcessRegistry.get(holdRef.getId())).thenReturn(null);
 | 
			
		||||
 | 
			
		||||
        holdBulkMonitor.registerProcess(holdRef, processId);
 | 
			
		||||
 | 
			
		||||
        Mockito.verify(holdProcessRegistry)
 | 
			
		||||
            .put(holdRef.getId(), Arrays.asList(new HoldBulkProcessDetails(processId, null)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testGetBulkStatusesForHoldReturnsEmptyListWhenNoProcesses()
 | 
			
		||||
    {
 | 
			
		||||
        when(holdProcessRegistry.get("holdId")).thenReturn(null);
 | 
			
		||||
        assertEquals(Collections.emptyList(), holdBulkMonitor.getBulkStatusesForHold("holdId"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testGetBulkStatusesForHoldReturnsSortedStatuses()
 | 
			
		||||
    {
 | 
			
		||||
        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")
 | 
			
		||||
                .stream().map(bulkStatusId -> new HoldBulkProcessDetails(bulkStatusId, null)).toList());
 | 
			
		||||
        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.getBulkStatusesForHold("holdId"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2314,6 +2314,112 @@ paths:
 | 
			
		||||
          description: Unexpected error
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/Error'
 | 
			
		||||
  '/holds/{holdId}/bulk-statuses':
 | 
			
		||||
    get:
 | 
			
		||||
      tags:
 | 
			
		||||
        - holds
 | 
			
		||||
      operationId: listBulkStatuses
 | 
			
		||||
      summary: Get bulk statuses
 | 
			
		||||
      description: |
 | 
			
		||||
        Gets bulk statuses for hold with id **holdId**.
 | 
			
		||||
      parameters:
 | 
			
		||||
        - $ref: '#/parameters/holdIdParam'
 | 
			
		||||
        - $ref: '#/parameters/skipCountParam'
 | 
			
		||||
        - $ref: '#/parameters/maxItemsParam'
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          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/{bulkStatusId}':
 | 
			
		||||
    get:
 | 
			
		||||
      tags:
 | 
			
		||||
        - holds
 | 
			
		||||
      operationId: getBulkStatus
 | 
			
		||||
      summary: Get a bulk status
 | 
			
		||||
      description: |
 | 
			
		||||
        Gets a bulk status specified by **bulkStatusId** for **holdId**.
 | 
			
		||||
      parameters:
 | 
			
		||||
        - $ref: '#/parameters/holdIdParam'
 | 
			
		||||
        - $ref: '#/parameters/bulkStatusId'
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: Successful response
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/HoldBulkStatus'
 | 
			
		||||
        '400':
 | 
			
		||||
          description: |
 | 
			
		||||
            Invalid parameter: **holdId** or **bulkStatusId** is not a valid format
 | 
			
		||||
        '401':
 | 
			
		||||
          description: Authentication failed
 | 
			
		||||
        '403':
 | 
			
		||||
          description: Current user does not have permission to read **holdId**
 | 
			
		||||
        '404':
 | 
			
		||||
          description: "**holdId** or **bulkStatusId** does not exist"
 | 
			
		||||
        default:
 | 
			
		||||
          description: Unexpected error
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/Error'
 | 
			
		||||
  '/holds/{holdId}/bulk':
 | 
			
		||||
    post:
 | 
			
		||||
      tags:
 | 
			
		||||
        - holds
 | 
			
		||||
      operationId: startHoldBulkProcess
 | 
			
		||||
      summary: Start the hold bulk process
 | 
			
		||||
      description: |
 | 
			
		||||
        Start the asynchronous bulk process for a hold with id **holdId** based on search query results.
 | 
			
		||||
        
 | 
			
		||||
        ```JSON
 | 
			
		||||
        For example, the following JSON body starts 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/HoldBulkOperationEntry'
 | 
			
		||||
        '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 for **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
 | 
			
		||||
  bulkStatusId:
 | 
			
		||||
    name: bulkStatusId
 | 
			
		||||
    in: path
 | 
			
		||||
    description: The identifier of a bulk process.
 | 
			
		||||
    required: true
 | 
			
		||||
    type: string
 | 
			
		||||
  ## Record
 | 
			
		||||
  recordIdParam:
 | 
			
		||||
    name: recordId
 | 
			
		||||
@@ -4018,6 +4130,91 @@ definitions:
 | 
			
		||||
    properties:
 | 
			
		||||
      reason:
 | 
			
		||||
        type: string
 | 
			
		||||
  SearchRequestQuery:
 | 
			
		||||
    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 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/SearchRequestQuery'
 | 
			
		||||
      op:
 | 
			
		||||
        description: The operation type.
 | 
			
		||||
        type: string
 | 
			
		||||
        default: ADD
 | 
			
		||||
        enum:
 | 
			
		||||
          - ADD
 | 
			
		||||
  HoldBulkOperationEntry:
 | 
			
		||||
    type: object
 | 
			
		||||
    properties:
 | 
			
		||||
      bulkStatusId:
 | 
			
		||||
        type: string
 | 
			
		||||
      totalItems:
 | 
			
		||||
        type: integer
 | 
			
		||||
        format: int64
 | 
			
		||||
  HoldBulkStatus:
 | 
			
		||||
    type: object
 | 
			
		||||
    properties:
 | 
			
		||||
      bulkStatusId:
 | 
			
		||||
        type: string
 | 
			
		||||
      startTime:
 | 
			
		||||
        type: string
 | 
			
		||||
        format: date-time
 | 
			
		||||
      endTime:
 | 
			
		||||
        type: string
 | 
			
		||||
        format: date-time
 | 
			
		||||
      processedItems:
 | 
			
		||||
        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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user