ACS-6917 Implement the Legal Holds v1 API (#2566)

---------

Co-authored-by: Tom Page <tpage-alfresco@users.noreply.github.com>
Co-authored-by: Domenico Sibilio <domenicosibilio@gmail.com>
This commit is contained in:
Damian Ujma
2024-04-04 11:46:56 +02:00
committed by GitHub
parent e8e747347a
commit 53d77f8d71
45 changed files with 3731 additions and 114 deletions

View File

@@ -39,6 +39,7 @@ import org.alfresco.rest.rm.community.requests.gscore.GSCoreAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.ActionsExecutionAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.FilePlanAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.FilesAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.HoldsAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.RMSiteAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.RMUserAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.RecordCategoryAPI;
@@ -243,4 +244,14 @@ public class RestAPIFactory
{
return getGSCoreAPI(null).usingActionsExecutionsAPI();
}
public HoldsAPI getHoldsAPI()
{
return getGSCoreAPI(null).usingHoldsAPI();
}
public HoldsAPI getHoldsAPI(UserModel userModel)
{
return getGSCoreAPI(userModel).usingHoldsAPI();
}
}

View File

@@ -61,7 +61,6 @@ public class FilePlanComponentFields
public static final String PROPERTIES_RECORD_SEARCH_DISPOSITION_EVENTS = "rma:recordSearchDispositionEvents";
public static final String PROPERTIES_DECLASSIFICATION_REVIEW_COMPLETED_BY = "rma:declassificationReviewCompletedBy";
public static final String PROPERTIES_DECLASSIFICATION_REVIEW_COMPLETED_AT = "rma:declassificationReviewCompletedAt";
/** File plan properties */
public static final String PROPERTIES_COMPONENT_ID = "st:componentId";

View File

@@ -0,0 +1,83 @@
/*-
* #%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 java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.alfresco.utility.model.TestModel;
/**
* POJO for hold
*
* @author Damian Ujma
*/
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Hold extends TestModel
{
@JsonProperty(required = true)
private String id;
@JsonProperty(required = true)
private String name;
@JsonProperty(required = true)
private String description;
@JsonProperty(required = true)
private String reason;
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o == null || getClass() != o.getClass())
{
return false;
}
Hold hold = (Hold) o;
return Objects.equals(id, hold.id) && Objects.equals(name, hold.name)
&& Objects.equals(description, hold.description) && Objects.equals(reason, hold.reason);
}
@Override
public int hashCode()
{
return Objects.hash(id, name, description, reason);
}
}

View File

@@ -0,0 +1,52 @@
/*-
* #%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.utility.model.TestModel;
/**
* POJO for hold child
*
* @author Damian Ujma
*/
@Builder
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HoldChild extends TestModel
{
@JsonProperty(required = true)
private String id;
}

View File

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

View File

@@ -0,0 +1,52 @@
/*-
* #%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;
/**
* POJO for hold child entry
*
* @author Damian Ujma
*/
@Builder
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class HoldChildEntry extends RestModels<Hold, HoldChildEntry>
{
@JsonProperty
private HoldChildEntry entry;
}

View File

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

View File

@@ -0,0 +1,52 @@
/*-
* #%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.utility.model.TestModel;
/**
* POJO for hold deletion reason
*
* @author Damian Ujma
*/
@Builder
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class HoldDeletionReason extends TestModel
{
@JsonProperty
private String reason;
}

View File

@@ -0,0 +1,52 @@
/*-
* #%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;
/**
* POJO for hold child entry
*
* @author Damian Ujma
*/
@Builder
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class HoldDeletionReasonEntry extends RestModels<HoldDeletionReason, HoldDeletionReasonEntry>
{
@JsonProperty
private HoldDeletionReason entry;
}

View File

@@ -26,31 +26,27 @@
*/
package org.alfresco.rest.rm.community.model.hold;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.alfresco.utility.model.TestModel;
import org.alfresco.rest.core.RestModels;
/**
* POJO for hold entry
*
* @author Rodica Sutu
* @since 3.2
* @author Damian Ujma
*/
@Builder
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties (ignoreUnknown = true)
public class HoldEntry extends TestModel
public class HoldEntry extends RestModels<Hold, HoldEntry>
{
@JsonProperty (required = true)
private String name;
@JsonProperty (required = true)
private String nodeRef;
@JsonProperty
private Hold entry;
}

View File

@@ -0,0 +1,56 @@
/*-
* #%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.v0;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.alfresco.utility.model.TestModel;
/**
* POJO for hold entry
*
* @author Rodica Sutu
* @since 3.2
*/
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties (ignoreUnknown = true)
public class HoldEntry extends TestModel
{
@JsonProperty (required = true)
private String name;
@JsonProperty (required = true)
private String nodeRef;
}

View File

@@ -37,6 +37,7 @@ import org.alfresco.rest.rm.community.requests.gscore.api.ActionsExecutionAPI;
import org.alfresco.rest.rm.community.requests.RMModelRequest;
import org.alfresco.rest.rm.community.requests.gscore.api.FilePlanAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.FilesAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.HoldsAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.RMSiteAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.RMUserAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.RecordCategoryAPI;
@@ -190,4 +191,6 @@ public class GSCoreAPI extends RMModelRequest
{
return new ActionsExecutionAPI(getRmRestWrapper());
}
public HoldsAPI usingHoldsAPI() { return new HoldsAPI(getRmRestWrapper()); }
}

View File

@@ -38,6 +38,8 @@ import static org.springframework.http.HttpMethod.PUT;
import org.alfresco.rest.core.RMRestWrapper;
import org.alfresco.rest.rm.community.model.fileplan.FilePlan;
import org.alfresco.rest.rm.community.model.hold.Hold;
import org.alfresco.rest.rm.community.model.hold.HoldCollection;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryCollection;
import org.alfresco.rest.rm.community.requests.RMModelRequest;
@@ -213,4 +215,74 @@ public class FilePlanAPI extends RMModelRequest
parameters));
}
/**
* Creates a hold.
*
* @param holdModel The hold model
* @param filePlanId The identifier of a file plan
* @param parameters The URL parameters to add
* @return The created {@link Hold}
* @throws RuntimeException for the following cases:
* <ul>
* <li>{@code filePlanId} is not a valid format or {@code filePlanId} is invalid</li>
* <li>authentication fails</li>
* <li>current user does not have permission to add children to {@code filePlanId}</li>
* <li>{@code filePlanIds} does not exist</li>
* <li>new name clashes with an existing node in the current parent container</li>
* </ul>
*/
public Hold createHold(Hold holdModel, String filePlanId, String parameters)
{
mandatoryString("filePlanId", filePlanId);
mandatoryObject("holdModel", holdModel);
return getRmRestWrapper().processModel(Hold.class, requestWithBody(
POST,
toJson(holdModel),
"file-plans/{filePlanId}/holds",
filePlanId,
parameters
));
}
/**
* See {@link #createHold(Hold, String, String)}
*/
public Hold createHold(Hold holdModel, String filePlanId)
{
return createHold(holdModel, filePlanId, EMPTY);
}
/**
* Gets the holds of a file plan.
*
* @param filePlanId The identifier of a file plan
* @param parameters The URL parameters to add
* @return The {@link HoldCollection} for the given {@code filePlanId}
* @throws RuntimeException for the following cases:
* <ul>
* <li>authentication fails</li>
* <li>current user does not have permission to read {@code filePlanId}</li>
* <li>{@code filePlanId} does not exist</li>
*</ul>
*/
public HoldCollection getHolds(String filePlanId, String parameters)
{
mandatoryString("filePlanId", filePlanId);
return getRmRestWrapper().processModels(HoldCollection.class, simpleRequest(
GET,
"file-plans/{filePlanId}/holds?{parameters}",
filePlanId,
parameters
));
}
/**
* See {@link #getHolds(String, String)}
*/
public HoldCollection getHolds(String filePlanId)
{
return getHolds(filePlanId, EMPTY);
}
}

View File

@@ -0,0 +1,290 @@
/*
* #%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.requests.gscore.api;
import static org.alfresco.rest.core.RestRequest.requestWithBody;
import static org.alfresco.rest.core.RestRequest.simpleRequest;
import static org.alfresco.rest.rm.community.util.ParameterCheck.mandatoryObject;
import static org.alfresco.rest.rm.community.util.ParameterCheck.mandatoryString;
import static org.alfresco.rest.rm.community.util.PojoUtility.toJson;
import static org.apache.commons.lang3.StringUtils.EMPTY;
import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
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.HoldChild;
import org.alfresco.rest.rm.community.model.hold.HoldChildCollection;
import org.alfresco.rest.rm.community.model.hold.HoldDeletionReason;
import org.alfresco.rest.rm.community.requests.RMModelRequest;
/**
* Holds REST API Wrapper
*
* @author Damian Ujma
*/
public class HoldsAPI extends RMModelRequest
{
/**
* @param rmRestWrapper
*/
public HoldsAPI(RMRestWrapper rmRestWrapper)
{
super(rmRestWrapper);
}
/**
* Gets a hold.
*
* @param holdId The identifier of a hold
* @param parameters The URL parameters to add
* @return The {@link Hold} for the given {@code holdId}
* @throws RuntimeException for the following cases:
* <ul>
* <li>{@code holdId} is not a valid format</li>
* <li>authentication fails</li>
* <li>current user does not have permission to read {@code holdId}</li>
* <li>{@code holdId} does not exist</li>
* </ul>
*/
public Hold getHold(String holdId, String parameters)
{
mandatoryString("holdId", holdId);
return getRmRestWrapper().processModel(Hold.class, simpleRequest(
GET,
"holds/{holdId}?{parameters}",
holdId,
parameters
));
}
/**
* See {@link #getHold(String, String)}
*/
public Hold getHold(String holdId)
{
mandatoryString("holdId", holdId);
return getHold(holdId, EMPTY);
}
/**
* Updates a hold.
*
* @param holdModel The hold model which holds the information
* @param holdId The identifier of the hold
* @param parameters The URL parameters to add
* @throws RuntimeException for the following cases:
* <ul>
* <li>the update request is invalid or {@code holdId} is not a valid format or {@code holdModel} is invalid</li>
* <li>authentication fails</li>
* <li>current user does not have permission to update {@code holdId}</li>
* <li>{@code holdId} does not exist</li>
* </ul>
*/
public Hold updateHold(Hold holdModel, String holdId, String parameters)
{
mandatoryObject("holdModel", holdModel);
mandatoryString("holdId", holdId);
return getRmRestWrapper().processModel(Hold.class, requestWithBody(
PUT,
toJson(holdModel),
"holds/{holdId}?{parameters}",
holdId,
parameters
));
}
/**
* See {@link #updateHold(Hold, String, String)}
*/
public Hold updateHold(Hold holdModel, String holdId)
{
mandatoryObject("holdModel", holdModel);
mandatoryString("holdId", holdId);
return updateHold(holdModel, holdId, EMPTY);
}
/**
* Deletes a hold.
*
* @param holdId The identifier of a hold
* @throws RuntimeException for the following cases:
* <ul>
* <li>{@code holdId} is not a valid format</li>
* <li>authentication fails</li>
* <li>current user does not have permission to delete {@code holdId}</li>
* <li>{@code holdId} does not exist</li>
* </ul>
*/
public void deleteHold(String holdId)
{
mandatoryString("holdId", holdId);
getRmRestWrapper().processEmptyModel(simpleRequest(
DELETE,
"holds/{holdId}",
holdId
));
}
/**
* Deletes a hold and stores a reason for deletion in the audit log.
*
* @param reason The reason for hold deletion
* @param holdId The identifier of a hold
* @throws RuntimeException for the following cases:
* <ul>
* <li>{@code holdId} is not a valid format or {@code reason} is invalid</li>
* <li>authentication fails</li>
* <li>current user does not have permission to delete {@code holdId}</li>
* <li>{@code holdId} does not exist</li>
* </ul>
*/
public HoldDeletionReason deleteHoldWithReason(HoldDeletionReason reason, String holdId)
{
mandatoryObject("reason", reason);
mandatoryString("holdId", holdId);
return getRmRestWrapper().processModel(HoldDeletionReason.class, requestWithBody(
POST,
toJson(reason),
"holds/{holdId}/delete",
holdId
));
}
/**
* Adds the relationship between a child and a parent hold.
*
* @param holdChild The hold child model
* @param holdId The identifier of a hold
* @param parameters The URL parameters to add
* @return The created {@link Hold}
* @throws RuntimeException for the following cases:
* <ul>
* <li>{@code holdId} is not a valid format or {@code holdId} is invalid</li>
* <li>authentication fails</li>
* <li>current user does not have permission to add children to {@code holdId}</li>
* <li>{@code holdId} does not exist</li>
* </ul>
*/
public HoldChild addChildToHold(HoldChild holdChild, String holdId, String parameters)
{
mandatoryObject("holdId", holdId);
return getRmRestWrapper().processModel(HoldChild.class, requestWithBody(
POST,
toJson(holdChild),
"holds/{holdId}/children",
holdId,
parameters));
}
/**
* See {@link #addChildToHold(HoldChild, String, String)}
*/
public HoldChild addChildToHold(HoldChild holdChild, String holdId)
{
return addChildToHold(holdChild, holdId, EMPTY);
}
/**
* Gets the children of a hold.
*
* @param holdId The identifier of a hold
* @param parameters The URL parameters to add
* @return The {@link HoldChildCollection} for the given {@code holdId}
* @throws RuntimeException for the following cases:
* <ul>
* <li>authentication fails</li>
* <li>current user does not have permission to read {@code holdId}</li>
* <li>{@code holdId} does not exist</li>
*</ul>
*/
public HoldChildCollection getChildren(String holdId, String parameters)
{
mandatoryString("holdId", holdId);
return getRmRestWrapper().processModels(HoldChildCollection.class, simpleRequest(
GET,
"holds/{holdId}/children",
holdId,
parameters
));
}
/**
* See {@link #getChildren(String, String)}
*/
public HoldChildCollection getChildren(String holdId)
{
return getChildren(holdId, EMPTY);
}
/**
* Deletes the relationship between a child and a parent hold.
*
* @param holdChildId The identifier of hold child
* @param holdId The identifier of a hold
* @param parameters The URL parameters to add
* @throws RuntimeException for the following cases:
* <ul>
* <li>{@code holdId} or {@code holdChildId} is invalid</li>
* <li>authentication fails</li>
* <li>current user does not have permission to delete children from {@code holdId}</li>
* <li>{@code holdId} does not exist</li>
* </ul>
*/
public void deleteHoldChild(String holdId, String holdChildId, String parameters)
{
mandatoryString("holdId", holdId);
mandatoryString("holdChildId", holdChildId);
getRmRestWrapper().processEmptyModel(simpleRequest(
DELETE,
"holds/{holdId}/children/{holdChildId}",
holdId,
holdChildId,
parameters
));
}
/**
* See {@link #deleteHoldChild(String, String, String)}
*/
public void deleteHoldChild(String holdId, String holdChildId)
{
deleteHoldChild(holdId, holdChildId, EMPTY);
}
}

View File

@@ -36,7 +36,7 @@ import java.util.stream.Collectors;
import org.alfresco.rest.core.v0.APIUtils;
import org.alfresco.rest.core.v0.BaseAPI;
import org.alfresco.rest.rm.community.model.hold.HoldEntry;
import org.alfresco.rest.rm.community.model.hold.v0.HoldEntry;
import org.alfresco.rest.rm.community.util.PojoUtility;
import org.alfresco.utility.model.UserModel;
import org.apache.http.HttpResponse;

View File

@@ -31,18 +31,18 @@ import static java.util.Arrays.asList;
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.audit.AuditEvents.ADD_TO_HOLD;
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS;
import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
import static org.alfresco.rest.rm.community.utils.RMSiteUtil.FILE_PLAN_PATH;
import static org.alfresco.utility.Utility.buildPath;
import static org.alfresco.utility.Utility.removeLastSlash;
import static org.alfresco.utility.data.RandomData.getRandomName;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.apache.commons.httpclient.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertTrue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.google.common.collect.ImmutableMap;
@@ -50,12 +50,13 @@ import com.google.common.collect.ImmutableMap;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
import org.alfresco.rest.rm.community.model.audit.AuditEntry;
import org.alfresco.rest.rm.community.model.hold.Hold;
import org.alfresco.rest.rm.community.model.hold.HoldChild;
import org.alfresco.rest.rm.community.model.record.Record;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
import org.alfresco.rest.rm.community.model.user.UserPermissions;
import org.alfresco.rest.rm.community.model.user.UserRoles;
import org.alfresco.rest.v0.HoldsAPI;
import org.alfresco.rest.v0.service.RMAuditService;
import org.alfresco.rest.v0.service.RoleService;
import org.alfresco.test.AlfrescoTest;
@@ -85,8 +86,6 @@ public class AuditAddToHoldTests extends BaseRMRestTest
@Autowired
private RMAuditService rmAuditService;
@Autowired
private HoldsAPI holdsAPI;
@Autowired
private RoleService roleService;
private UserModel rmAdmin, rmManagerNoReadOnHold, rmManagerNoReadOnNode;
@@ -94,17 +93,22 @@ public class AuditAddToHoldTests extends BaseRMRestTest
private RecordCategory recordCategory;
private RecordCategoryChild recordFolder;
private List<AuditEntry> auditEntries;
private final List<String> holdsList = asList(HOLD1, HOLD2);
private List<String> holdsListRef = new ArrayList<>();
private String hold1NodeRef;
private String hold2NodeRef;
@BeforeClass (alwaysRun = true)
public void preconditionForAuditAddToHoldTests()
{
STEP("Create 2 holds.");
hold1NodeRef = holdsAPI.createHoldAndGetNodeRef(getAdminUser().getUsername(),
getAdminUser().getPassword(), HOLD1, HOLD_REASON, HOLD_DESCRIPTION);
String hold2NodeRef = holdsAPI.createHoldAndGetNodeRef(getAdminUser().getUsername(), getAdminUser().getPassword(), HOLD2, HOLD_REASON, HOLD_DESCRIPTION);
hold1NodeRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(HOLD1).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
.getId();
hold2NodeRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(HOLD2).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
.getId();
holdsListRef = asList(hold1NodeRef, hold2NodeRef);
STEP("Create a new record category with a record folder.");
@@ -169,7 +173,8 @@ public class AuditAddToHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Add node to hold.");
holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), nodeId, HOLD1);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(nodeId).build(), hold1NodeRef);
STEP("Check the audit log contains the entry for the add to hold event.");
rmAuditService.checkAuditLogForEvent(getAdminUser(), ADD_TO_HOLD, rmAdmin, nodeName, nodePath,
@@ -191,9 +196,8 @@ public class AuditAddToHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Try to add the record to a hold by an user with no rights.");
holdsAPI.addItemsToHolds(rmManagerNoReadOnHold.getUsername(), rmManagerNoReadOnHold.getPassword(),
SC_INTERNAL_SERVER_ERROR, Collections.singletonList(recordToBeAdded.getId()),
Collections.singletonList(hold1NodeRef));
getRestAPIFactory().getHoldsAPI(rmManagerNoReadOnHold).addChildToHold(HoldChild.builder().id(recordToBeAdded.getId()).build(), hold1NodeRef);
assertStatusCode(FORBIDDEN);
STEP("Check the audit log doesn't contain the entry for the unsuccessful add to hold.");
assertTrue("The list of events should not contain Add to Hold entry ",
@@ -215,7 +219,7 @@ public class AuditAddToHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Add record folder to hold.");
holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), notEmptyRecFolder.getId(), HOLD1);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(notEmptyRecFolder.getId()).build(), hold1NodeRef);
auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(getAdminUser(), ADD_TO_HOLD);
@@ -239,8 +243,9 @@ public class AuditAddToHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Add record to multiple holds.");
holdsAPI.addItemsToHolds(rmAdmin.getUsername(), rmAdmin.getPassword(),
Collections.singletonList(recordToBeAdded.getId()), holdsList);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(recordToBeAdded.getId()).build(), hold1NodeRef);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(recordToBeAdded.getId()).build(), hold2NodeRef);
auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(getAdminUser(), ADD_TO_HOLD);
@@ -268,7 +273,7 @@ public class AuditAddToHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Add file to hold.");
holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), contentToBeAdded.getNodeRefWithoutVersion(), HOLD1);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(contentToBeAdded.getNodeRefWithoutVersion()).build(), hold1NodeRef);
STEP("Check that an user with no Read permissions can't see the entry for the add to hold event.");
assertTrue("The list of events should not contain Add to Hold entry ",
@@ -289,7 +294,7 @@ public class AuditAddToHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Add file to hold.");
holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), contentToBeAdded.getNodeRefWithoutVersion(), HOLD1);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(contentToBeAdded.getNodeRefWithoutVersion()).build(), hold1NodeRef);
auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(rmManagerNoReadOnHold, ADD_TO_HOLD);
@@ -304,7 +309,8 @@ public class AuditAddToHoldTests extends BaseRMRestTest
@AfterClass (alwaysRun = true)
public void cleanUpAuditAddToHoldTests()
{
holdsListRef.forEach(holdRef -> holdsAPI.deleteHold(getAdminUser(), holdRef));
holdsListRef.forEach(holdRef -> getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(holdRef));
dataSite.usingAdmin().deleteSite(privateSite);
asList(rmAdmin, rmManagerNoReadOnHold, rmManagerNoReadOnNode).forEach(user -> getDataUser().usingAdmin().deleteUser(user));
deleteRecordCategory(recordCategory.getId());

View File

@@ -31,9 +31,10 @@ import static java.util.Arrays.asList;
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.audit.AuditEvents.CREATE_HOLD;
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS;
import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.apache.commons.httpclient.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.CONFLICT;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertTrue;
@@ -44,8 +45,8 @@ import com.google.common.collect.ImmutableMap;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
import org.alfresco.rest.rm.community.model.audit.AuditEntry;
import org.alfresco.rest.rm.community.model.hold.Hold;
import org.alfresco.rest.rm.community.model.user.UserRoles;
import org.alfresco.rest.v0.HoldsAPI;
import org.alfresco.rest.v0.service.RMAuditService;
import org.alfresco.rest.v0.service.RoleService;
import org.alfresco.test.AlfrescoTest;
@@ -73,8 +74,6 @@ public class AuditCreateHoldTests extends BaseRMRestTest
@Autowired
private RMAuditService rmAuditService;
@Autowired
private HoldsAPI holdsAPI;
@Autowired
private RoleService roleService;
private UserModel rmAdmin, rmManager;
@@ -102,8 +101,10 @@ public class AuditCreateHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Create a new hold.");
String hold1NodeRef = holdsAPI.createHoldAndGetNodeRef(rmAdmin.getUsername(), rmAdmin.getPassword(), HOLD1,
HOLD_REASON, HOLD_DESCRIPTION);
String hold1NodeRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(HOLD1).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
.getId();
holdsListRef.add(hold1NodeRef);
STEP("Check the audit log contains the entry for the created hold with the hold details.");
rmAuditService.checkAuditLogForEvent(getAdminUser(), CREATE_HOLD, rmAdmin, HOLD1,
@@ -120,13 +121,18 @@ public class AuditCreateHoldTests extends BaseRMRestTest
public void createHoldEventIsNotAuditedForExistingHold()
{
STEP("Create a new hold.");
String hold2NodeRef = holdsAPI.createHoldAndGetNodeRef(rmAdmin.getUsername(), rmAdmin.getPassword(), HOLD2, HOLD_REASON, HOLD_DESCRIPTION);
String hold2NodeRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(HOLD2).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
.getId();
holdsListRef.add(hold2NodeRef);
rmAuditService.clearAuditLog();
STEP("Try to create again the same hold and expect action to fail.");
holdsAPI.createHold(rmAdmin.getUsername(), rmAdmin.getPassword(), HOLD2, HOLD_REASON, HOLD_DESCRIPTION,
SC_INTERNAL_SERVER_ERROR);
getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(HOLD2).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS);
assertStatusCode(CONFLICT);
STEP("Check the audit log doesn't contain the entry for the second create hold event.");
assertTrue("The list of events should not contain Create Hold entry ",
@@ -145,13 +151,17 @@ public class AuditCreateHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Create a new hold.");
holdsAPI.createHold(rmAdmin.getUsername(), rmAdmin.getPassword(), holdName, HOLD_REASON, HOLD_DESCRIPTION);
String nodeRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(holdName).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS).getId();
STEP("Get the list of audit entries for the create hold event.");
List<AuditEntry> auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(getAdminUser(), CREATE_HOLD);
STEP("Delete the created hold.");
holdsAPI.deleteHold(rmAdmin.getUsername(), rmAdmin.getPassword(), holdName);
getRestAPIFactory()
.getHoldsAPI(rmAdmin)
.deleteHold(nodeRef);
STEP("Get again the list of audit entries for the create hold event.");
List<AuditEntry> auditEntriesAfterDelete = rmAuditService.getAuditEntriesFilteredByEvent(getAdminUser(), CREATE_HOLD);
@@ -171,8 +181,10 @@ public class AuditCreateHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Create a new hold.");
String hold3NodeRef = holdsAPI.createHoldAndGetNodeRef(rmAdmin.getUsername(), rmAdmin.getPassword(), HOLD3,
HOLD_REASON, HOLD_DESCRIPTION);
String hold3NodeRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(HOLD3).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS).getId();
holdsListRef.add(hold3NodeRef);
STEP("Check that an user with no Read permissions over the hold can't see the entry for the create hold event");
@@ -183,7 +195,7 @@ public class AuditCreateHoldTests extends BaseRMRestTest
@AfterClass (alwaysRun = true)
public void cleanUpAuditCreateHoldTests()
{
holdsListRef.forEach(holdRef -> holdsAPI.deleteHold(getAdminUser(), holdRef));
holdsListRef.forEach(holdRef -> getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHold(holdRef));
asList(rmAdmin, rmManager).forEach(user -> getDataUser().usingAdmin().deleteUser(user));
}
}

View File

@@ -31,18 +31,20 @@ import static java.util.Arrays.asList;
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.audit.AuditEvents.DELETE_HOLD;
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS;
import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.apache.commons.httpclient.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.testng.AssertJUnit.assertTrue;
import java.util.Collections;
import java.util.List;
import com.google.common.collect.ImmutableMap;
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.HoldDeletionReason;
import org.alfresco.rest.rm.community.model.user.UserRoles;
import org.alfresco.rest.v0.HoldsAPI;
import org.alfresco.rest.v0.service.RMAuditService;
import org.alfresco.rest.v0.service.RoleService;
import org.alfresco.test.AlfrescoTest;
@@ -62,14 +64,13 @@ import org.testng.annotations.Test;
public class AuditDeleteHoldTests extends BaseRMRestTest
{
private final String PREFIX = generateTestPrefix(AuditDeleteHoldTests.class);
private final String HOLD = PREFIX + "holdToBeDeleted";
private final String HOLD2 = PREFIX + "deleteHold";
private final String hold = PREFIX + "holdToBeDeleted";
private final String hold2 = PREFIX + "deleteHold";
private final String hold3 = PREFIX + "deleteHoldWithReason";
@Autowired
private RMAuditService rmAuditService;
@Autowired
private HoldsAPI holdsAPI;
@Autowired
private RoleService roleService;
private UserModel rmAdmin, rmManager;
@@ -79,8 +80,10 @@ public class AuditDeleteHoldTests extends BaseRMRestTest
public void preconditionForAuditDeleteHoldTests()
{
STEP("Create a new hold.");
holdNodeRef = holdsAPI.createHoldAndGetNodeRef(getAdminUser().getUsername(), getAdminUser().getPassword(), HOLD,
HOLD_REASON, HOLD_DESCRIPTION);
holdNodeRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(hold).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
.getId();
STEP("Create 2 users with different permissions for the created hold.");
rmAdmin = roleService.createUserWithRMRole(UserRoles.ROLE_RM_ADMIN.roleId);
@@ -99,17 +102,51 @@ public class AuditDeleteHoldTests extends BaseRMRestTest
public void deleteHoldEventIsAudited()
{
STEP("Create a new hold.");
String holdRef = holdsAPI.createHoldAndGetNodeRef(rmAdmin.getUsername(), rmAdmin.getPassword(), HOLD2,
HOLD_REASON, HOLD_DESCRIPTION);
String holdRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(hold2).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
.getId();
rmAuditService.clearAuditLog();
STEP("Delete the created hold.");
holdsAPI.deleteHold(rmAdmin, holdRef);
getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHold(holdRef);
STEP("Check the audit log contains the entry for the deleted hold with the hold details.");
rmAuditService.checkAuditLogForEvent(getAdminUser(), DELETE_HOLD, rmAdmin, HOLD2,
Collections.singletonList(ImmutableMap.of("new", "", "previous", HOLD2, "name", "Hold Name")));
rmAuditService.checkAuditLogForEvent(getAdminUser(), DELETE_HOLD, rmAdmin, hold2,
List.of(ImmutableMap.of("new", "", "previous", hold2, "name", "Hold Name"),
ImmutableMap.of("new", "", "previous", "", "name", "Hold deletion reason")));
}
/**
* Given a hold is deleted with a reason
* When I view the audit log
* Then an entry has been created in the audit log which contains the following:
* name of the hold
* hold deletion reason
* user who deleted the hold
* date the delete occurred
*/
@Test
public void deleteHoldWithReasonEventIsAudited()
{
STEP("Create a new hold.");
String holdRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(hold3).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
.getId();
String deletionReason = "Test reason";
rmAuditService.clearAuditLog();
STEP("Delete the created hold with a reason.");
getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldWithReason(HoldDeletionReason.builder().reason(deletionReason).build(), holdRef);
STEP("Check the audit log contains the entry for the deleted hold with the hold details.");
rmAuditService.checkAuditLogForEvent(getAdminUser(), DELETE_HOLD, rmAdmin, hold3,
List.of(ImmutableMap.of("new", "", "previous", hold3, "name", "Hold Name"),
ImmutableMap.of("new", "", "previous", deletionReason, "name", "Hold deletion reason")));
}
/**
@@ -123,7 +160,8 @@ public class AuditDeleteHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Try to delete a hold by an user with no Read permissions over the hold.");
holdsAPI.deleteHold(rmManager.getUsername(), rmManager.getPassword(), holdNodeRef, SC_INTERNAL_SERVER_ERROR);
getRestAPIFactory().getHoldsAPI(rmManager).deleteHold(holdNodeRef);
assertStatusCode(FORBIDDEN);
STEP("Check the audit log doesn't contain the entry for the unsuccessful delete hold.");
assertTrue("The list of events should not contain Delete Hold entry ",
@@ -133,7 +171,7 @@ public class AuditDeleteHoldTests extends BaseRMRestTest
@AfterClass (alwaysRun = true)
public void cleanUpAuditDeleteHoldTests()
{
holdsAPI.deleteHold(getAdminUser(), holdNodeRef);
getRestAPIFactory().getHoldsAPI(rmManager).deleteHold(holdNodeRef);
asList(rmAdmin, rmManager).forEach(user -> getDataUser().usingAdmin().deleteUser(user));
}
}

View File

@@ -25,10 +25,14 @@
* #L%
*/
package org.alfresco.rest.rm.community.audit;
import static java.util.Arrays.asList;
import static org.alfresco.rest.rm.community.base.TestData.*;
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.audit.AuditEvents.ADD_TO_HOLD;
import static org.alfresco.rest.rm.community.model.audit.AuditEvents.REMOVE_FROM_HOLD;
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS;
import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
import static org.alfresco.utility.data.RandomData.getRandomName;
import static org.alfresco.utility.report.log.Step.STEP;
@@ -37,20 +41,22 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.core.IsNot.not;
import static org.springframework.http.HttpStatus.CREATED;
import static org.testng.AssertJUnit.*;
import static org.testng.AssertJUnit.assertFalse;
import static org.testng.AssertJUnit.assertTrue;
import java.util.Collections;
import java.util.List;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
import org.alfresco.rest.rm.community.model.audit.AuditEntry;
import org.alfresco.rest.rm.community.model.audit.AuditEvents;
import org.alfresco.rest.rm.community.model.hold.Hold;
import org.alfresco.rest.rm.community.model.hold.HoldChild;
import org.alfresco.rest.rm.community.model.record.Record;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
import org.alfresco.rest.rm.community.model.recordfolder.RecordFolder;
import org.alfresco.rest.rm.community.model.user.UserRoles;
import org.alfresco.rest.v0.HoldsAPI;
import org.alfresco.rest.v0.service.RMAuditService;
import org.alfresco.rest.v0.service.RoleService;
import org.alfresco.utility.model.FileModel;
@@ -69,8 +75,6 @@ public class AuditHoldsTest extends BaseRMRestTest {
@Autowired
private RMAuditService rmAuditService;
@Autowired
private HoldsAPI holdsAPI;
@Autowired
private RoleService roleService;
private UserModel rmAdmin;
private RecordCategory recordCategory;
@@ -85,8 +89,11 @@ public class AuditHoldsTest extends BaseRMRestTest {
rmAdmin = roleService.createUserWithRMRole(UserRoles.ROLE_RM_ADMIN.roleId);
STEP("Create a hold");
hold1NodeRef = holdsAPI.createHoldAndGetNodeRef(rmAdmin.getUsername(), rmAdmin.getPassword(), HOLD1, HOLD_REASON,
HOLD_DESCRIPTION);
hold1NodeRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(HOLD1).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
.getId();
STEP("Create a collaboration site with a test file.");
publicSite = dataSite.usingAdmin().createPublicRandomSite();
@@ -101,9 +108,11 @@ public class AuditHoldsTest extends BaseRMRestTest {
STEP("Add some items to the hold, then remove them from the hold");
final List<String> itemsList = asList(testFile.getNodeRefWithoutVersion(), recordToBeAdded.getId(), recordFolder2.getId());
final List<String> holdsList = Collections.singletonList(HOLD1);
holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), recordToBeAdded.getId(), HOLD1);
holdsAPI.removeItemsFromHolds(rmAdmin.getUsername(), rmAdmin.getPassword(), itemsList, holdsList);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(recordToBeAdded.getId()).build(), hold1NodeRef);
for(String childId : itemsList)
{
getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold1NodeRef, childId);
}
STEP("Delete the record folder that was held");
getRestAPIFactory().getRecordFolderAPI().deleteRecordFolder(recordFolder2.getId());

View File

@@ -31,18 +31,18 @@ import static java.util.Arrays.asList;
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.audit.AuditEvents.REMOVE_FROM_HOLD;
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS;
import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
import static org.alfresco.rest.rm.community.utils.RMSiteUtil.FILE_PLAN_PATH;
import static org.alfresco.utility.Utility.buildPath;
import static org.alfresco.utility.Utility.removeLastSlash;
import static org.alfresco.utility.data.RandomData.getRandomName;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.apache.commons.httpclient.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertTrue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.google.common.collect.ImmutableMap;
@@ -50,12 +50,13 @@ import com.google.common.collect.ImmutableMap;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
import org.alfresco.rest.rm.community.model.audit.AuditEntry;
import org.alfresco.rest.rm.community.model.hold.Hold;
import org.alfresco.rest.rm.community.model.hold.HoldChild;
import org.alfresco.rest.rm.community.model.record.Record;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
import org.alfresco.rest.rm.community.model.user.UserPermissions;
import org.alfresco.rest.rm.community.model.user.UserRoles;
import org.alfresco.rest.v0.HoldsAPI;
import org.alfresco.rest.v0.service.RMAuditService;
import org.alfresco.rest.v0.service.RoleService;
import org.alfresco.test.AlfrescoTest;
@@ -86,8 +87,6 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
@Autowired
private RMAuditService rmAuditService;
@Autowired
private HoldsAPI holdsAPI;
@Autowired
private RoleService roleService;
private UserModel rmAdmin, rmManagerNoReadOnHold, rmManagerNoReadOnNode;
@@ -96,10 +95,11 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
private RecordCategoryChild recordFolder, heldRecordFolder;
private Record heldRecord;
private List<AuditEntry> auditEntries;
private final List<String> holdsList = asList(HOLD1, HOLD2, HOLD3);
private List<String> holdsListRef = new ArrayList<>();
private FileModel heldContent;
private String hold1NodeRef;
private String hold2NodeRef;
private String hold3NodeRef;
@BeforeClass (alwaysRun = true)
public void preconditionForAuditRemoveFromHoldTests()
@@ -111,10 +111,18 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
privateSite = dataSite.usingUser(rmAdmin).createPrivateRandomSite();
STEP("Create new holds.");
hold1NodeRef = holdsAPI.createHoldAndGetNodeRef(getAdminUser().getUsername(), getAdminUser().getPassword(),
HOLD1, HOLD_REASON, HOLD_DESCRIPTION);
String hold2NodeRef = holdsAPI.createHoldAndGetNodeRef(getAdminUser().getUsername(), getAdminUser().getPassword(), HOLD2, HOLD_REASON, HOLD_DESCRIPTION);
String hold3NodeRef = holdsAPI.createHoldAndGetNodeRef(getAdminUser().getUsername(), getAdminUser().getPassword(), HOLD3, HOLD_REASON, HOLD_DESCRIPTION);
hold1NodeRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(HOLD1).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
.getId();
hold2NodeRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(HOLD2).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
.getId();
hold3NodeRef = getRestAPIFactory()
.getFilePlansAPI(rmAdmin)
.createHold(Hold.builder().name(HOLD3).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
.getId();
holdsListRef = asList(hold1NodeRef, hold2NodeRef, hold3NodeRef);
STEP("Create a new record category with a record folder.");
@@ -127,9 +135,12 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
heldRecordFolder = createRecordFolder(recordCategory.getId(), PREFIX + "heldRecFolder");
heldRecord = createElectronicRecord(recordFolder.getId(), PREFIX + "record");
holdsAPI.addItemsToHolds(getAdminUser().getUsername(), getAdminUser().getPassword(),
asList(heldContent.getNodeRefWithoutVersion(), heldRecordFolder.getId(), heldRecord.getId()),
holdsList);
holdsListRef.forEach(holdRef ->
{
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(heldContent.getNodeRefWithoutVersion()).build(), holdRef);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(heldRecordFolder.getId()).build(), holdRef);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(heldRecord.getId()).build(), holdRef);
});
STEP("Create users without rights to remove content from a hold.");
rmManagerNoReadOnHold = roleService.createUserWithSiteRoleRMRoleAndPermission(privateSite,
@@ -179,7 +190,7 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Remove node from hold.");
holdsAPI.removeItemFromHold(rmAdmin.getUsername(), rmAdmin.getPassword(), nodeId, HOLD3);
getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold3NodeRef, nodeId);
STEP("Check the audit log contains the entry for the remove from hold event.");
rmAuditService.checkAuditLogForEvent(getAdminUser(), REMOVE_FROM_HOLD, rmAdmin, nodeName, nodePath,
@@ -198,9 +209,8 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Try to remove the record from a hold by an user with no rights.");
holdsAPI.removeItemsFromHolds(rmManagerNoReadOnHold.getUsername(), rmManagerNoReadOnHold.getPassword(),
SC_INTERNAL_SERVER_ERROR, Collections.singletonList(heldRecord.getId()),
Collections.singletonList(hold1NodeRef));
getRestAPIFactory().getHoldsAPI(rmManagerNoReadOnHold).deleteHoldChild(hold1NodeRef, heldRecord.getId());
assertStatusCode(FORBIDDEN);
STEP("Check the audit log doesn't contain the entry for the unsuccessful remove from hold.");
assertTrue("The list of events should not contain remove from hold entry ",
@@ -220,12 +230,12 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
Record record = createElectronicRecord(notEmptyRecFolder.getId(), PREFIX + "record");
STEP("Add the record folder to a hold.");
holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), notEmptyRecFolder.getId(), HOLD1);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(notEmptyRecFolder.getId()).build(), hold1NodeRef);
rmAuditService.clearAuditLog();
STEP("Remove record folder from hold.");
holdsAPI.removeItemFromHold(rmAdmin.getUsername(), rmAdmin.getPassword(), notEmptyRecFolder.getId(), HOLD1);
getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold1NodeRef, notEmptyRecFolder.getId());
STEP("Get the list of audit entries for the remove from hold event.");
auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(getAdminUser(), REMOVE_FROM_HOLD);
@@ -247,8 +257,8 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Remove record folder from multiple holds.");
holdsAPI.removeItemsFromHolds(rmAdmin.getUsername(), rmAdmin.getPassword(),
Collections.singletonList(heldRecordFolder.getId()), asList(HOLD1, HOLD2));
getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold1NodeRef, heldRecordFolder.getId());
getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold2NodeRef, heldRecordFolder.getId());
STEP("Get the list of audit entries for the remove from hold event.");
auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(getAdminUser(), REMOVE_FROM_HOLD);
@@ -275,12 +285,12 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
STEP("Add content to a hold.");
FileModel heldFile = dataContent.usingAdmin().usingSite(privateSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), heldFile.getNodeRefWithoutVersion(), HOLD1);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(heldFile.getNodeRefWithoutVersion()).build(), hold1NodeRef);
rmAuditService.clearAuditLog();
STEP("Remove held content from the hold.");
holdsAPI.removeItemFromHold(rmAdmin.getUsername(), rmAdmin.getPassword(), heldFile.getNodeRefWithoutVersion(), HOLD1);
getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold1NodeRef, heldFile.getNodeRefWithoutVersion());
STEP("Check that an user with no Read permissions can't see the entry for the remove from hold event.");
assertTrue("The list of events should not contain Remove from Hold entry ",
@@ -298,12 +308,12 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
STEP("Add content to a hold.");
FileModel heldFile = dataContent.usingAdmin().usingSite(privateSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), heldFile.getNodeRefWithoutVersion(), HOLD1);
getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(heldFile.getNodeRefWithoutVersion()).build(), hold1NodeRef);
rmAuditService.clearAuditLog();
STEP("Remove held content from the hold.");
holdsAPI.removeItemFromHold(rmAdmin.getUsername(), rmAdmin.getPassword(), heldFile.getNodeRefWithoutVersion(), HOLD1);
getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold1NodeRef, heldFile.getNodeRefWithoutVersion());
auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(rmManagerNoReadOnHold, REMOVE_FROM_HOLD);
@@ -318,7 +328,7 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
@AfterClass (alwaysRun = true)
public void cleanUpAuditRemoveFromHoldTests()
{
holdsListRef.forEach(holdRef -> holdsAPI.deleteHold(getAdminUser(), holdRef));
holdsListRef.forEach(holdRef -> getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHold(holdRef));
dataSite.usingAdmin().deleteSite(privateSite);
asList(rmAdmin, rmManagerNoReadOnHold, rmManagerNoReadOnNode).forEach(user -> getDataUser().usingAdmin().deleteUser(user));
deleteRecordCategory(recordCategory.getId());

View File

@@ -60,12 +60,15 @@ import static org.testng.Assert.fail;
import static org.testng.AssertJUnit.assertEquals;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
import org.alfresco.rest.rm.community.base.DataProviderClass;
import org.alfresco.rest.rm.community.model.fileplan.FilePlan;
import org.alfresco.rest.rm.community.model.fileplan.FilePlanProperties;
import org.alfresco.rest.rm.community.model.hold.Hold;
import org.alfresco.rest.rm.community.model.hold.HoldCollection;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryCollection;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryProperties;
@@ -514,5 +517,97 @@ public class FilePlanTests extends BaseRMRestTest
);
}
/**
* <pre>
* Given that a file plan exists
* When I ask the API to create a hold
* Then it is created
* </pre>
*/
@Test
public void createHolds()
{
String holdName = "Hold" + getRandomAlphanumeric();
String holdDescription = "Description" + getRandomAlphanumeric();
String holdReason = "Reason" + getRandomAlphanumeric();
// Create the hold
Hold hold = Hold.builder()
.name(holdName)
.description(holdDescription)
.reason(holdReason)
.build();
Hold createdHold = getRestAPIFactory().getFilePlansAPI()
.createHold(hold, FILE_PLAN_ALIAS);
// Verify the status code
assertStatusCode(CREATED);
assertEquals(createdHold.getName(), holdName);
assertEquals(createdHold.getDescription(), holdDescription);
assertEquals(createdHold.getReason(), holdReason);
assertNotNull(createdHold.getId());
}
@Test
public void listHolds()
{
// Delete all holds
getRestAPIFactory().getFilePlansAPI().getHolds(FILE_PLAN_ALIAS).getEntries().forEach(holdEntry ->
getRestAPIFactory().getHoldsAPI().deleteHold(holdEntry.getEntry().getId()));
// Add holds
List<Hold> filePlanHolds = new ArrayList<>();
for (int i = 0; i < NUMBER_OF_CHILDREN; i++)
{
String holdName = "Hold name " + getRandomAlphanumeric();
String holdDescription = "Hold Description " + getRandomAlphanumeric();
String holdReason = "Reason " + getRandomAlphanumeric();
// Create a hold
Hold hold = Hold.builder()
.name(holdName)
.description(holdDescription)
.reason(holdReason)
.build();
Hold createdHold = getRestAPIFactory().getFilePlansAPI()
.createHold(hold, FILE_PLAN_ALIAS);
assertNotNull(createdHold.getId());
filePlanHolds.add(createdHold);
}
// Get holds of a file plan
HoldCollection holdCollection = getRestAPIFactory().getFilePlansAPI()
.getHolds(FILE_PLAN_ALIAS);
// Check status code
assertStatusCode(OK);
// Check holds against created list
holdCollection.getEntries().forEach(c ->
{
Hold hold = c.getEntry();
String holdId = hold.getId();
assertNotNull(holdId);
logger.info("Checking hold " + holdId);
try
{
// Find this hold in created holds list
Hold createdHold = filePlanHolds.stream()
.filter(child -> child.getId().equals(holdId))
.findFirst()
.orElseThrow();
assertEquals(createdHold.getName(), hold.getName());
assertEquals(createdHold.getDescription(), hold.getDescription());
assertEquals(createdHold.getReason(), hold.getReason());
}
catch (NoSuchElementException e)
{
fail("No child element for " + hold);
}
}
);
}
}

View File

@@ -60,7 +60,7 @@ import org.alfresco.dataprep.CMISUtil;
import org.alfresco.dataprep.ContentActions;
import org.alfresco.rest.model.RestNodeModel;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
import org.alfresco.rest.rm.community.model.hold.HoldEntry;
import org.alfresco.rest.rm.community.model.hold.v0.HoldEntry;
import org.alfresco.rest.rm.community.model.record.Record;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;

View File

@@ -0,0 +1,386 @@
/*-
* #%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.fileplancomponents.FilePlanComponentAlias.TRANSFERS_ALIAS;
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.UNFILED_RECORDS_CONTAINER_ALIAS;
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAspects.FROZEN_ASPECT;
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentType.UNFILED_RECORD_FOLDER_TYPE;
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.model.user.UserRoles.ROLE_RM_MANAGER;
import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
import static org.alfresco.rest.rm.community.utils.CoreUtil.toContentModel;
import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.IMAGE_FILE;
import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.createElectronicRecordModel;
import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.createNonElectronicRecordModel;
import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.getFile;
import static org.alfresco.utility.data.RandomData.getRandomAlphanumeric;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.apache.commons.httpclient.HttpStatus.SC_BAD_REQUEST;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.AssertJUnit.assertFalse;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.dataprep.ContentActions;
import org.alfresco.rest.model.RestNodeAssociationModelCollection;
import org.alfresco.rest.model.RestNodeModel;
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.HoldChild;
import org.alfresco.rest.rm.community.model.record.Record;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
import org.alfresco.rest.rm.community.model.user.UserRoles;
import org.alfresco.rest.rm.community.requests.gscore.api.FilePlanAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.RecordFolderAPI;
import org.alfresco.rest.v0.service.RoleService;
import org.alfresco.utility.constants.UserRole;
import org.alfresco.utility.model.FileModel;
import org.alfresco.utility.model.SiteModel;
import org.alfresco.utility.model.UserModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
* V1 API tests for adding content/record folder/records to holds
*
* @author Damian Ujma
*/
public class AddToHoldsV1Tests 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 String INVALID_TYPE_ERROR_MESSAGE = "Only records, record folders or content can be added to a hold.";
private static final String LOCKED_FILE_ERROR_MESSAGE = "Locked content can't be added to a hold.";
private static final String HOLD = "HOLD" + generateTestPrefix(AddToHoldsV1Tests.class);
private String holdNodeRef;
private SiteModel testSite;
private FileModel documentHeld;
private FileModel contentToAddToHold;
private FileModel contentAddToHoldNoPermission;
private Hold hold;
private UserModel userAddHoldPermission;
private final List<UserModel> users = new ArrayList<>();
private final List<String> nodesToBeClean = new ArrayList<>();
@Autowired
private RoleService roleService;
@Autowired
private ContentActions contentActions;
@BeforeClass(alwaysRun = true)
public void preconditionForAddContentToHold()
{
STEP("Create a hold.");
hold = createHold(FILE_PLAN_ALIAS,
Hold.builder().name(HOLD).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), getAdminUser());
holdNodeRef = hold.getId();
STEP("Create test files.");
testSite = dataSite.usingAdmin().createPublicRandomSite();
documentHeld = dataContent.usingAdmin().usingSite(testSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
contentToAddToHold = dataContent.usingAdmin().usingSite(testSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
contentAddToHoldNoPermission = dataContent.usingAdmin().usingSite(testSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
STEP("Add the content to the hold.");
getRestAPIFactory()
.getHoldsAPI(getAdminUser())
.addChildToHold(HoldChild.builder().id(documentHeld.getNodeRefWithoutVersion()).build(), hold.getId());
STEP("Create users");
userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
UserRole.SiteCollaborator, holdNodeRef, UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
users.add(userAddHoldPermission);
}
/**
* Given a hold that contains at least one active content
* When I use the existing REST API to retrieve the contents of the hold
* Then I should see all the active content on hold
*/
@Test
public void retrieveTheContentOfTheHoldUsingV1API()
{
STEP("Retrieve the list of children from the hold and collect the entries that have the name of the active " +
"content held");
List<String> documentNames = restClient.authenticateUser(getAdminUser()).withCoreAPI()
.usingNode(toContentModel(holdNodeRef))
.listChildren().getEntries().stream()
.map(RestNodeModel::onModel)
.map(RestNodeModel::getName)
.filter(documentName -> documentName.equals(documentHeld.getName()))
.toList();
STEP("Check the list of active content");
assertEquals(documentNames, Set.of(documentHeld.getName()));
}
/**
* Given a hold that contains at least one active content
* When I use the existing REST API to retrieve the holds the content is added
* Then the hold where the content held is returned
*/
@Test
public void retrieveTheHoldWhereTheContentIsAdded()
{
RestNodeAssociationModelCollection holdsEntries = getRestAPIFactory()
.getNodeAPI(documentHeld).usingParams("where=(assocType='rma:frozenContent')").getParents();
Hold retrievedHold = getRestAPIFactory().getHoldsAPI(getAdminUser())
.getHold(holdsEntries.getEntries().get(0).getModel().getId());
assertEquals(retrievedHold, hold, "Holds are not equal");
}
/**
* Valid nodes to be added to hold
*/
@DataProvider(name = "validNodesForAddToHold")
public Object[][] getValidNodesForAddToHold()
{
//create electronic and nonElectronic record in record folder
RecordCategoryChild recordFolder = createCategoryFolderInFilePlan();
RecordFolderAPI recordFolderAPI = getRestAPIFactory().getRecordFolderAPI();
nodesToBeClean.add(recordFolder.getParentId());
Record electronicRecord = recordFolderAPI.createRecord(createElectronicRecordModel(), recordFolder.getId(),
getFile
(IMAGE_FILE));
assertStatusCode(CREATED);
Record nonElectronicRecord = recordFolderAPI.createRecord(createNonElectronicRecordModel(),
recordFolder.getId());
assertStatusCode(CREATED);
getRestAPIFactory().getRMUserAPI().addUserPermission(recordFolder.getId(), userAddHoldPermission,
PERMISSION_FILING);
RecordCategoryChild folderToHold = createCategoryFolderInFilePlan();
getRestAPIFactory().getRMUserAPI().addUserPermission(folderToHold.getId(), userAddHoldPermission,
PERMISSION_FILING);
nodesToBeClean.add(folderToHold.getParentId());
return new String[][]
{ // record folder
{ folderToHold.getId() },
//electronic record
{ electronicRecord.getId() },
// non electronic record
{ nonElectronicRecord.getId() },
// document from collaboration site
{ contentToAddToHold.getNodeRefWithoutVersion() },
};
}
/**
* Given record folder/record/document not on hold
* And a hold
* And file permission on the hold
* And the appropriate capability to add to hold
* When I use the existing REST API to add the node to the hold
* Then the record folder/record/document is added to the hold
* And the item is frozen
*
* @throws Exception
*/
@Test(dataProvider = "validNodesForAddToHold")
public void addValidNodesToHoldWithAllowedUser(String nodeId) throws Exception
{
STEP("Add node to hold with user with permission.");
getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.addChildToHold(HoldChild.builder().id(nodeId).build(), hold.getId());
STEP("Check the node is frozen.");
assertTrue(hasAspect(nodeId, FROZEN_ASPECT));
}
/**
* Data provider with user without correct permission to add to hold and the node ref to be added to hold
*
* @return object with user model and the node ref to be added to hold
*/
@DataProvider(name = "userWithoutPermissionForAddToHold")
public Object[][] getUserWithoutPermissionForAddToHold()
{
//create record folder
RecordCategoryChild recordFolder = createCategoryFolderInFilePlan();
//create a rm manager and grant read permission over the record folder created
UserModel user = roleService.createUserWithRMRoleAndRMNodePermission(ROLE_RM_MANAGER.roleId,
recordFolder.getId(),
PERMISSION_READ_RECORDS);
getRestAPIFactory().getRMUserAPI().addUserPermission(holdNodeRef, user, PERMISSION_FILING);
nodesToBeClean.add(recordFolder.getParentId());
return new Object[][]
{ // user without write permission on the content
{
roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole.SiteConsumer,
holdNodeRef, UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING),
contentAddToHoldNoPermission.getNodeRefWithoutVersion()
},
// user with write permission on the content and without filling permission on a hold
{
roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole
.SiteCollaborator,
holdNodeRef, UserRoles.ROLE_RM_MANAGER, PERMISSION_READ_RECORDS),
contentAddToHoldNoPermission.getNodeRefWithoutVersion()
},
// user with write permission on the content, filling permission on a hold without add to
// hold capability
{
roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole
.SiteCollaborator,
holdNodeRef, UserRoles.ROLE_RM_POWER_USER, PERMISSION_READ_RECORDS),
contentAddToHoldNoPermission.getNodeRefWithoutVersion()
},
//user without write permission on RM record folder
{
user, recordFolder.getId()
},
};
}
/**
* Given a node not on hold
* And a hold
* And user without right permission to add to hold
* When I use the existing REST API to add the node to the hold
* Then the node is not added to the hold
* And the node is not frozen
*
* @throws Exception
*/
@Test(dataProvider = "userWithoutPermissionForAddToHold")
public void addContentToHoldWithUserWithoutHoldPermission(UserModel userModel, String nodeToBeAddedToHold)
throws Exception
{
users.add(userModel);
STEP("Add the node to the hold with user without permission.");
getRestAPIFactory()
.getHoldsAPI(userModel)
.addChildToHold(HoldChild.builder().id(nodeToBeAddedToHold).build(), holdNodeRef);
assertStatusCode(FORBIDDEN);
getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(ACCESS_DENIED_ERROR_MESSAGE);
STEP("Check the node is not frozen.");
assertFalse(hasAspect(nodeToBeAddedToHold, FROZEN_ASPECT));
}
/**
* Data provider with invalid node types that can be added to a hold
*/
@DataProvider(name = "invalidNodesForAddToHold")
public Object[][] getInvalidNodesForAddToHold()
{
//create locked file
FileModel contentLocked = dataContent.usingAdmin().usingSite(testSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
contentActions.checkOut(getAdminUser().getUsername(), getAdminUser().getPassword(),
testSite.getId(), contentLocked.getName());
RecordCategory category = createRootCategory(getRandomAlphanumeric());
nodesToBeClean.add(category.getId());
return new Object[][]
{ // file plan node id
{ getFilePlan(FILE_PLAN_ALIAS).getId(), SC_BAD_REQUEST, INVALID_TYPE_ERROR_MESSAGE },
//transfer container
{ getTransferContainer(TRANSFERS_ALIAS).getId(), SC_BAD_REQUEST, INVALID_TYPE_ERROR_MESSAGE },
// a record category
{ category.getId(), SC_BAD_REQUEST, INVALID_TYPE_ERROR_MESSAGE },
// unfiled records root
{ getUnfiledContainer(UNFILED_RECORDS_CONTAINER_ALIAS).getId(), SC_BAD_REQUEST,
INVALID_TYPE_ERROR_MESSAGE },
// an arbitrary unfiled records folder
{ createUnfiledContainerChild(UNFILED_RECORDS_CONTAINER_ALIAS, "Unfiled Folder " +
getRandomAlphanumeric(), UNFILED_RECORD_FOLDER_TYPE).getId(), SC_BAD_REQUEST,
INVALID_TYPE_ERROR_MESSAGE },
//folder,
{ dataContent.usingAdmin().usingSite(testSite).createFolder().getNodeRef(), SC_BAD_REQUEST,
INVALID_TYPE_ERROR_MESSAGE },
//document locked
{ contentLocked.getNodeRefWithoutVersion(), SC_BAD_REQUEST, LOCKED_FILE_ERROR_MESSAGE }
};
}
/**
* Given a node that is not a document/record/ record folder ( a valid node type to be added to hold)
* And a hold
* And user without right permission to add to hold
* When I use the existing REST API to add the node to the hold
* Then the node is not added to the hold
* And the node is not frozen
*
* @throws Exception
*/
@Test(dataProvider = "invalidNodesForAddToHold")
public void addInvalidNodesToHold(String itemNodeRef, int responseCode, String errorMessage) throws Exception
{
STEP("Add the node to the hold ");
getRestAPIFactory()
.getHoldsAPI(getAdminUser())
.addChildToHold(HoldChild.builder().id(itemNodeRef).build(), holdNodeRef);
assertStatusCode(HttpStatus.valueOf(responseCode));
getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(errorMessage);
STEP("Check node is not frozen.");
assertFalse(hasAspect(itemNodeRef, FROZEN_ASPECT));
}
private Hold createHold(String parentId, Hold hold, UserModel user)
{
FilePlanAPI filePlanAPI = getRestAPIFactory().getFilePlansAPI(user);
return filePlanAPI.createHold(hold, parentId);
}
@AfterClass(alwaysRun = true)
public void cleanUpAddContentToHold()
{
getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(holdNodeRef);
dataSite.usingAdmin().deleteSite(testSite);
users.forEach(user -> getDataUser().usingAdmin().deleteUser(user));
nodesToBeClean.forEach(this::deleteRecordCategory);
}
}

View File

@@ -0,0 +1,186 @@
/*-
* #%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.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS;
import static org.alfresco.utility.data.RandomData.getRandomAlphanumeric;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.NO_CONTENT;
import static org.springframework.http.HttpStatus.OK;
import java.util.ArrayList;
import java.util.List;
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.HoldDeletionReason;
import org.testng.annotations.AfterClass;
import org.testng.annotations.Test;
/**
* This class contains the tests for the Holds CRUD V1 API
*
* @author Damian Ujma
*/
public class HoldsTests extends BaseRMRestTest
{
private final List<String> nodeRefs = new ArrayList<>();
@Test
public void testGetHold()
{
String holdName = "Hold" + getRandomAlphanumeric();
String holdDescription = "Description" + getRandomAlphanumeric();
String holdReason = "Reason" + getRandomAlphanumeric();
// Create the hold
Hold hold = Hold.builder()
.name(holdName)
.description(holdDescription)
.reason(holdReason)
.build();
Hold createdHold = getRestAPIFactory().getFilePlansAPI()
.createHold(hold, FILE_PLAN_ALIAS);
// Get the hold
Hold receivedHold = getRestAPIFactory().getHoldsAPI().getHold(createdHold.getId());
nodeRefs.add(receivedHold.getId());
// Verify the status code
assertStatusCode(OK);
assertEquals(receivedHold.getName(), holdName);
assertEquals(receivedHold.getDescription(), holdDescription);
assertEquals(receivedHold.getReason(), holdReason);
assertNotNull(receivedHold.getId());
}
@Test
public void testUpdateHold()
{
String holdName = "Hold" + getRandomAlphanumeric();
String holdDescription = "Description" + getRandomAlphanumeric();
String holdReason = "Reason" + getRandomAlphanumeric();
// Create the hold
Hold hold = Hold.builder()
.name(holdName)
.description(holdDescription)
.reason(holdReason)
.build();
Hold createdHold = getRestAPIFactory().getFilePlansAPI()
.createHold(hold, FILE_PLAN_ALIAS);
nodeRefs.add(createdHold.getId());
Hold holdModel = Hold.builder()
.name("Updated" + holdName)
.description("Updated" + holdDescription)
.reason("Updated" + holdReason)
.build();
// Update the hold
Hold updatedHold = getRestAPIFactory().getHoldsAPI().updateHold(holdModel, createdHold.getId());
// Verify the status code
assertStatusCode(OK);
assertEquals(updatedHold.getName(), "Updated" + holdName);
assertEquals(updatedHold.getDescription(), "Updated" + holdDescription);
assertEquals(updatedHold.getReason(), "Updated" + holdReason);
assertNotNull(updatedHold.getId());
}
@Test
public void testDeleteHold()
{
String holdName = "Hold" + getRandomAlphanumeric();
String holdDescription = "Description" + getRandomAlphanumeric();
String holdReason = "Reason" + getRandomAlphanumeric();
// Create the hold
Hold hold = Hold.builder()
.name(holdName)
.description(holdDescription)
.reason(holdReason)
.build();
Hold createdHold = getRestAPIFactory().getFilePlansAPI()
.createHold(hold, FILE_PLAN_ALIAS);
nodeRefs.add(createdHold.getId());
// Delete the hold
getRestAPIFactory().getHoldsAPI().deleteHold(createdHold.getId());
// Verify the status code
assertStatusCode(NO_CONTENT);
// Try to get the hold
getRestAPIFactory().getHoldsAPI().getHold(createdHold.getId());
// Verify the status code
assertStatusCode(NOT_FOUND);
}
@Test
public void testDeleteHoldWithReason()
{
String holdName = "Hold" + getRandomAlphanumeric();
String holdDescription = "Description" + getRandomAlphanumeric();
String holdReason = "Reason" + getRandomAlphanumeric();
// Create the hold
Hold hold = Hold.builder()
.name(holdName)
.description(holdDescription)
.reason(holdReason)
.build();
Hold createdHold = getRestAPIFactory().getFilePlansAPI()
.createHold(hold, FILE_PLAN_ALIAS);
nodeRefs.add(createdHold.getId());
// Delete the hold with the reason
getRestAPIFactory().getHoldsAPI()
.deleteHoldWithReason(HoldDeletionReason.builder().reason("Example reason").build(), createdHold.getId());
// Verify the status code
assertStatusCode(OK);
// Try to get the hold
getRestAPIFactory().getHoldsAPI().getHold(createdHold.getId());
// Verify the status code
assertStatusCode(NOT_FOUND);
}
@AfterClass(alwaysRun = true)
public void cleanUpHoldsTests()
{
nodeRefs.forEach(nodeRef -> getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(nodeRef));
}
}

View File

@@ -0,0 +1,337 @@
/*-
* #%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.fileplancomponents.FilePlanComponentAspects.ASPECTS_VITAL_RECORD;
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAspects.ASPECTS_VITAL_RECORD_DEFINITION;
import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
import static org.alfresco.rest.rm.community.utils.CoreUtil.createBodyForMoveCopy;
import static org.alfresco.utility.data.RandomData.getRandomName;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.apache.commons.httpclient.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.OK;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import static org.testng.AssertJUnit.assertFalse;
import java.io.File;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.rest.core.JsonBodyGenerator;
import org.alfresco.rest.core.v0.BaseAPI.RM_ACTIONS;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
import org.alfresco.rest.rm.community.model.common.ReviewPeriod;
import org.alfresco.rest.rm.community.model.hold.Hold;
import org.alfresco.rest.rm.community.model.hold.HoldChild;
import org.alfresco.rest.rm.community.model.record.Record;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
import org.alfresco.rest.rm.community.model.recordfolder.RecordFolder;
import org.alfresco.rest.rm.community.model.recordfolder.RecordFolderProperties;
import org.alfresco.rest.rm.community.requests.gscore.api.FilePlanAPI;
import org.alfresco.rest.v0.RMRolesAndActionsAPI;
import org.alfresco.rest.v0.service.DispositionScheduleService;
import org.alfresco.utility.Utility;
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;
/**
* V1 API tests to check actions on frozen content
*
* @author Damian Ujma
*/
public class PreventActionsOnFrozenContentV1Tests extends BaseRMRestTest
{
private static String holdNodeRef;
private static FileModel contentHeld;
private static File updatedFile;
private static FolderModel folderModel;
private static RecordCategoryChild recordFolder;
private static Record recordFrozen;
private static Record recordNotHeld;
private static RecordCategory categoryWithRS;
private Hold hold;
@Autowired
private DispositionScheduleService dispositionScheduleService;
@Autowired
private RMRolesAndActionsAPI rmRolesAndActionsAPI;
@BeforeClass(alwaysRun = true)
public void preconditionForPreventActionsOnFrozenContent()
{
String holdOne = "HOLD" + generateTestPrefix(PreventActionsOnFrozenContentV1Tests.class);
STEP("Create a hold.");
hold = createHold(FILE_PLAN_ALIAS,
Hold.builder().name(holdOne).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), getAdminUser());
holdNodeRef = hold.getId();
STEP("Create a test file.");
testSite = dataSite.usingAdmin().createPublicRandomSite();
contentHeld = dataContent.usingAdmin().usingSite(testSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
STEP("Add the file to the hold.");
getRestAPIFactory()
.getHoldsAPI(getAdminUser())
.addChildToHold(HoldChild.builder().id(contentHeld.getNodeRefWithoutVersion()).build(), hold.getId());
STEP("Get a file resource.");
updatedFile = Utility.getResourceTestDataFile("SampleTextFile_10kb.txt");
STEP("Create a folder withing the test site .");
folderModel = dataContent.usingAdmin().usingSite(testSite)
.createFolder();
STEP("Create a record folder with some records");
recordFolder = createCategoryFolderInFilePlan();
recordFrozen = createElectronicRecord(recordFolder.getId(), getRandomName("elRecordFrozen"));
recordNotHeld = createElectronicRecord(recordFolder.getId(), getRandomName("elRecordNotHeld"));
assertStatusCode(CREATED);
STEP("Add the record to the hold.");
getRestAPIFactory()
.getHoldsAPI(getAdminUser())
.addChildToHold(HoldChild.builder().id(recordFrozen.getId()).build(), hold.getId());
}
/**
* Given active content on hold
* When I try to edit the properties
* Or perform an action that edits the properties
* Then I am not successful
*/
@Test
public void editPropertiesForContentHeld() throws Exception
{
STEP("Update name property of the held content");
JsonObject nameUpdated = Json.createObjectBuilder().add("name", "HeldNameUpdated").build();
restClient.authenticateUser(getAdminUser()).withCoreAPI().usingNode(contentHeld)
.updateNode(nameUpdated.toString());
STEP("Check the request failed.");
restClient.assertStatusCodeIs(FORBIDDEN);
restClient.assertLastError().containsSummary("Frozen content can't be updated.");
}
/*
* Given active content on hold
* When I try to update the content
* Then I am not successful
*/
@Test
public void updateContentForFrozenFile() throws Exception
{
STEP("Update content of the held file");
restClient.authenticateUser(getAdminUser()).withCoreAPI().usingNode(contentHeld).updateNodeContent(updatedFile);
STEP("Check the request failed.");
restClient.assertStatusCodeIs(INTERNAL_SERVER_ERROR);
restClient.assertLastError().containsSummary("Frozen content can't be updated.");
}
/*
* Given active content on hold
* When I try to delete the content
* Then I am not successful
*/
@Test
public void deleteFrozenFile() throws Exception
{
STEP("Delete frozen file");
restClient.authenticateUser(getAdminUser()).withCoreAPI().usingNode(contentHeld)
.deleteNode(contentHeld.getNodeRefWithoutVersion());
STEP("Check the request failed.");
restClient.assertStatusCodeIs(FORBIDDEN);
restClient.assertLastError().containsSummary("Frozen content can't be deleted.");
}
/**
* Given active content on hold
* When I try to copy the content
* Then I am not successful
*/
@Test
public void copyFrozenFile()
{
STEP("Copy frozen file");
String postBody = JsonBodyGenerator.keyValueJson("targetParentId", folderModel.getNodeRef());
getRestAPIFactory().getNodeAPI(contentHeld).copyNode(postBody);
STEP("Check the request failed.");
assertStatusCode(FORBIDDEN);
getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary("Permission was denied");
}
/**
* Given active content on hold
* When I try to move the content
* Then I am not successful
*/
@Test
public void moveFrozenFile() throws Exception
{
STEP("Move frozen file");
getRestAPIFactory().getNodeAPI(contentHeld).move(createBodyForMoveCopy(folderModel.getNodeRef()));
STEP("Check the request failed.");
assertStatusCode(FORBIDDEN);
getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary("Frozen content can't be moved.");
}
/**
* Given a record folder with a frozen record and another record not held
* When I update the record folder and make the records as vital
* Then I am successful and the records not held are marked as vital
* And the frozen nodes have the vital record search properties updated
*/
@Test
public void updateRecordFolderVitalProperties()
{
STEP("Update the vital record properties for the record folder");
// Create the record folder properties to update
RecordFolder recordFolderToUpdate = RecordFolder.builder()
.properties(RecordFolderProperties.builder()
.vitalRecordIndicator(true)
.reviewPeriod(new ReviewPeriod("month", "1"))
.build())
.build();
// Update the record folder
RecordFolder updatedRecordFolder = getRestAPIFactory().getRecordFolderAPI().updateRecordFolder
(recordFolderToUpdate,
recordFolder.getId());
assertStatusCode(OK);
assertTrue(updatedRecordFolder.getAspectNames().contains(ASPECTS_VITAL_RECORD_DEFINITION));
STEP("Check the frozen record was not marked as vital");
recordFrozen = getRestAPIFactory().getRecordsAPI().getRecord(recordFrozen.getId());
assertFalse(recordFrozen.getAspectNames().contains(ASPECTS_VITAL_RECORD));
assertTrue(recordFrozen.getProperties().getRecordSearchVitalRecordReviewPeriod().contains("month"));
assertTrue(recordFrozen.getProperties().getRecordSearchVitalRecordReviewPeriodExpression().contains("1"));
STEP("Check the record not held was marked as vital");
recordNotHeld = getRestAPIFactory().getRecordsAPI().getRecord(recordNotHeld.getId());
assertTrue(recordNotHeld.getAspectNames().contains(ASPECTS_VITAL_RECORD));
assertNotNull(recordNotHeld.getProperties().getReviewAsOf());
assertTrue(recordNotHeld.getProperties().getRecordSearchVitalRecordReviewPeriod().contains("month"));
assertTrue(recordNotHeld.getProperties().getRecordSearchVitalRecordReviewPeriodExpression().contains("1"));
}
/**
* Given a record folder with a frozen record and another record not held
* When I add a disposition schedule
* Then I am successful
* And the record search disposition schedule properties are updated
*/
@Test
public void createDispositionScheduleOnCategoryWithHeldChildren()
{
STEP("Create a retention schedule on the category with frozen children");
RecordCategory categoryWithRS = getRestAPIFactory().getRecordCategoryAPI()
.getRecordCategory(recordFolder.getParentId());
dispositionScheduleService.createCategoryRetentionSchedule(categoryWithRS.getName(), false);
dispositionScheduleService.addCutOffImmediatelyStep(categoryWithRS.getName());
dispositionScheduleService.addDestroyWithGhostingImmediatelyAfterCutOff(categoryWithRS.getName());
STEP("Check the record folder has a disposition schedule");
RecordFolder folderWithRS = getRestAPIFactory().getRecordFolderAPI().getRecordFolder(recordFolder.getId());
assertNotNull(folderWithRS.getProperties().getRecordSearchDispositionAuthority());
assertNotNull(folderWithRS.getProperties().getRecordSearchDispositionInstructions());
}
/**
* Given a record category with a disposition schedule applied to records
* And the disposition schedule has a retain step immediately and destroy step immediately
* And a complete record added to one hold
* When I execute the retain action
* Then the action is executed
* And the record search disposition schedule properties are updated
*/
@Test
public void retainActionOnFrozenHeldRecords()
{
STEP("Add a category with a disposition schedule.");
categoryWithRS = createRootCategory(getRandomName("CategoryWithRS"));
dispositionScheduleService.createCategoryRetentionSchedule(categoryWithRS.getName(), true);
dispositionScheduleService.addRetainAfterPeriodStep(categoryWithRS.getName(), "immediately");
dispositionScheduleService.addDestroyWithGhostingImmediatelyAfterCutOff(categoryWithRS.getName());
STEP("Create record folder with a record.");
RecordCategoryChild folder = createFolder(categoryWithRS.getId(), getRandomName("RecFolder"));
Record record = createElectronicRecord(folder.getId(), getRandomName("elRecord"));
completeRecord(record.getId());
STEP("Add the record to the hold");
getRestAPIFactory()
.getHoldsAPI(getAdminUser())
.addChildToHold(HoldChild.builder().id(record.getId()).build(), hold.getId());
STEP("Execute the retain action");
rmRolesAndActionsAPI.executeAction(getAdminUser().getUsername(), getAdminUser().getPassword(), record.getName(),
RM_ACTIONS.END_RETENTION, null, SC_INTERNAL_SERVER_ERROR);
STEP("Check the record search disposition properties");
Record recordUpdated = getRestAPIFactory().getRecordsAPI().getRecord(record.getId());
assertTrue(recordUpdated.getProperties().getRecordSearchDispositionActionName()
.contains(RM_ACTIONS.END_RETENTION.getAction()));
assertTrue(recordUpdated.getProperties().getRecordSearchDispositionPeriod().contains("immediately"));
}
private Hold createHold(String parentId, Hold hold, UserModel user)
{
FilePlanAPI filePlanAPI = getRestAPIFactory().getFilePlansAPI(user);
return filePlanAPI.createHold(hold, parentId);
}
@AfterClass(alwaysRun = true)
public void cleanUpPreventActionsOnFrozenContent()
{
getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(holdNodeRef);
dataSite.usingAdmin().deleteSite(testSite);
deleteRecordCategory(recordFolder.getParentId());
deleteRecordCategory(categoryWithRS.getId());
}
}

View File

@@ -52,7 +52,7 @@ import java.util.Set;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
import org.alfresco.rest.rm.community.model.hold.HoldEntry;
import org.alfresco.rest.rm.community.model.hold.v0.HoldEntry;
import org.alfresco.rest.rm.community.model.record.Record;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
import org.alfresco.rest.rm.community.model.user.UserRoles;

View File

@@ -0,0 +1,374 @@
/*-
* #%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 java.util.Arrays.asList;
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.fileplancomponents.FilePlanComponentAspects.FROZEN_ASPECT;
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.model.user.UserRoles.ROLE_RM_MANAGER;
import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.IMAGE_FILE;
import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.createElectronicRecordModel;
import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.createNonElectronicRecordModel;
import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.getFile;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.rest.model.RestNodeAssociationModelCollection;
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.HoldChild;
import org.alfresco.rest.rm.community.model.record.Record;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
import org.alfresco.rest.rm.community.model.user.UserRoles;
import org.alfresco.rest.rm.community.requests.gscore.api.FilePlanAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.RecordFolderAPI;
import org.alfresco.rest.rm.community.utils.CoreUtil;
import org.alfresco.rest.v0.service.RoleService;
import org.alfresco.utility.constants.UserRole;
import org.alfresco.utility.model.FileModel;
import org.alfresco.utility.model.SiteModel;
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.DataProvider;
import org.testng.annotations.Test;
/**
* V1 API tests for removing content/record folder/record from holds
*
* @author Damian Ujma
*/
public class RemoveFromHoldsV1Tests extends BaseRMRestTest
{
private static final String HOLD_ONE = "HOLD_ONE" + generateTestPrefix(RemoveFromHoldsV1Tests.class);
private static final String HOLD_TWO = "HOLD_TWO" + generateTestPrefix(RemoveFromHoldsV1Tests.class);
private static final String ACCESS_DENIED_ERROR_MESSAGE = "Access Denied. You do not have the appropriate " +
"permissions to perform this operation.";
private SiteModel testSite;
private SiteModel privateSite;
private String holdNodeRefOne;
private FileModel contentHeld;
private FileModel contentAddToManyHolds;
private List<String> holdsListRef = new ArrayList<>();
private final Set<UserModel> usersToBeClean = new HashSet<>();
private final Set<String> nodesToBeClean = new HashSet<>();
@Autowired
private RoleService roleService;
@BeforeClass(alwaysRun = true)
public void preconditionForRemoveContentFromHold()
{
STEP("Create two holds.");
holdNodeRefOne = createHold(FILE_PLAN_ALIAS,
Hold.builder().name(HOLD_ONE).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(),
getAdminUser()).getId();
String holdNodeRefTwo = createHold(FILE_PLAN_ALIAS,
Hold.builder().name(HOLD_TWO).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(),
getAdminUser()).getId();
holdsListRef = asList(holdNodeRefOne, holdNodeRefTwo);
STEP("Create test files.");
testSite = dataSite.usingAdmin().createPublicRandomSite();
privateSite = dataSite.usingAdmin().createPrivateRandomSite();
contentHeld = dataContent.usingAdmin().usingSite(testSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
contentAddToManyHolds = dataContent.usingSite(testSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
STEP("Add content to the holds.");
getRestAPIFactory()
.getHoldsAPI(getAdminUser())
.addChildToHold(HoldChild.builder().id(contentHeld.getNodeRefWithoutVersion()).build(), holdNodeRefOne);
getRestAPIFactory()
.getHoldsAPI(getAdminUser())
.addChildToHold(HoldChild.builder().id(contentAddToManyHolds.getNodeRefWithoutVersion()).build(),
holdNodeRefOne);
getRestAPIFactory()
.getHoldsAPI(getAdminUser())
.addChildToHold(HoldChild.builder().id(contentAddToManyHolds.getNodeRefWithoutVersion()).build(),
holdNodeRefTwo);
}
/**
* Valid nodes to be removed from hold
*/
@DataProvider(name = "validNodesToRemoveFromHold")
public Object[][] getValidNodesToRemoveFromHold()
{
//create electronic and nonElectronic record in record folder
RecordCategoryChild recordFolder = createCategoryFolderInFilePlan();
RecordFolderAPI recordFolderAPI = getRestAPIFactory().getRecordFolderAPI();
nodesToBeClean.add(recordFolder.getParentId());
Record electronicRecord = recordFolderAPI.createRecord(createElectronicRecordModel(), recordFolder.getId(),
getFile
(IMAGE_FILE));
assertStatusCode(CREATED);
Record nonElectronicRecord = recordFolderAPI.createRecord(createNonElectronicRecordModel(),
recordFolder.getId());
assertStatusCode(CREATED);
RecordCategoryChild folderToHeld = createCategoryFolderInFilePlan();
nodesToBeClean.add(folderToHeld.getParentId());
Stream.of(electronicRecord.getId(), nonElectronicRecord.getId(), folderToHeld.getId())
.forEach(id -> getRestAPIFactory()
.getHoldsAPI(getAdminUser())
.addChildToHold(HoldChild.builder().id(id).build(), holdNodeRefOne));
return new String[][]
{ // record folder
{ folderToHeld.getId() },
//electronic record
{ electronicRecord.getId() },
// non electronic record
{ nonElectronicRecord.getId() },
// document from collaboration site
{ contentHeld.getNodeRefWithoutVersion() },
};
}
/**
* Given content/record folder/record that is held
* And the corresponding hold
* When I use the existing REST API to remove the node from the hold
* Then the node is removed from the hold
* And is no longer frozen
*/
@Test(dataProvider = "validNodesToRemoveFromHold")
public void removeContentFromHold(String nodeId) throws Exception
{
STEP("Remove node from hold");
getRestAPIFactory()
.getHoldsAPI(getAdminUser()).deleteHoldChild(holdNodeRefOne, nodeId);
STEP("Check the node is not held");
assertFalse(hasAspect(nodeId, FROZEN_ASPECT));
STEP("Check node is not in any hold");
RestNodeAssociationModelCollection holdsEntries = getRestAPIFactory()
.getNodeAPI(CoreUtil.toContentModel(nodeId)).usingParams("where=(assocType='rma:frozenContent')")
.getParents();
assertTrue(holdsEntries.getEntries().isEmpty(), "Content held is still added to a hold.");
}
/**
* Given active content that is held on many holds
* When I use the existing REST API to remove the active content from one hold
* Then the active content is removed from the specific hold
* And is frozen
* And in the other holds
*/
@Test
public void removeContentAddedToManyHolds() throws Exception
{
STEP("Remove content from hold. ");
getRestAPIFactory().getHoldsAPI(getAdminUser())
.deleteHoldChild(holdNodeRefOne, contentAddToManyHolds.getNodeRefWithoutVersion());
STEP("Check the content is held. ");
assertTrue(hasAspect(contentAddToManyHolds.getNodeRefWithoutVersion(), FROZEN_ASPECT));
STEP("Check node is in hold HOLD_TWO. ");
RestNodeAssociationModelCollection holdsEntries = getRestAPIFactory()
.getNodeAPI(CoreUtil.toContentModel(contentAddToManyHolds.getNodeRefWithoutVersion()))
.usingParams("where=(assocType='rma:frozenContent')").getParents();
assertFalse(holdsEntries.getEntries().isEmpty(), "Content held is not held after removing from one hold.");
assertTrue(holdsEntries.getEntries().stream()
.anyMatch(restNodeModel -> restNodeModel.getModel().getName().equals(HOLD_TWO)),
"Content held is not held after removing from one hold.");
}
/**
* Data provider with user without right permission or capability to remove from hold a specific node
*
* @return user model and the node ref to be removed from hold
*/
@DataProvider(name = "userWithoutPermissionForRemoveFromHold")
public Object[][] getUserWithoutPermissionForAddToHold()
{
//create record folder
RecordCategoryChild recordFolder = createCategoryFolderInFilePlan();
nodesToBeClean.add(recordFolder.getParentId());
UserModel user = roleService.createUserWithRMRole(ROLE_RM_MANAGER.roleId);
getRestAPIFactory().getRMUserAPI().addUserPermission(holdNodeRefOne, user, PERMISSION_FILING);
//create files that will be removed from hold
FileModel contentNoHoldPerm = dataContent.usingAdmin().usingSite(testSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
FileModel contentNoHoldCap = dataContent.usingAdmin().usingSite(testSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
FileModel privateFile = dataContent.usingAdmin().usingSite(privateSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
//add files to hold
asList(recordFolder.getId(), contentNoHoldCap.getNodeRefWithoutVersion(),
contentNoHoldPerm.getNodeRefWithoutVersion(), privateFile.getNodeRefWithoutVersion())
.forEach(id -> getRestAPIFactory()
.getHoldsAPI(getAdminUser())
.addChildToHold(HoldChild.builder().id(id).build(), holdNodeRefOne));
return new Object[][]
{
// user with read permission on the content, with remove from hold capability and without
// filling permission on a hold
{
roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole.SiteCollaborator,
holdNodeRefOne, UserRoles.ROLE_RM_MANAGER, PERMISSION_READ_RECORDS),
contentNoHoldPerm.getNodeRefWithoutVersion()
},
// user with write permission on the content, filling permission on a hold without remove from
// hold capability
{
roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole
.SiteCollaborator,
holdNodeRefOne, UserRoles.ROLE_RM_POWER_USER, PERMISSION_FILING),
contentNoHoldCap.getNodeRefWithoutVersion()
},
//user without read permission on RM record folder
{
user, recordFolder.getId()
},
//user without read permission over the content from the private site
{
user, privateFile.getNodeRefWithoutVersion()
}
};
}
/**
* Given node on hold in a single hold location
* And the user does not have sufficient permissions or capabilities to remove the node from the hold
* When the user tries to remove the node from the hold
* Then it's unsuccessful
*
* @throws Exception
*/
@Test(dataProvider = "userWithoutPermissionForRemoveFromHold")
public void removeFromHoldWithUserWithoutPermission(UserModel userModel, String nodeIdToBeRemoved) throws Exception
{
STEP("Update the list of users to be deleted after running the tests");
usersToBeClean.add(userModel);
STEP("Remove node from hold with user without right permission or capability");
getRestAPIFactory().getHoldsAPI(userModel).deleteHoldChild(holdNodeRefOne, nodeIdToBeRemoved);
assertStatusCode(FORBIDDEN);
getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(ACCESS_DENIED_ERROR_MESSAGE);
STEP("Check node is frozen.");
assertTrue(hasAspect(nodeIdToBeRemoved, FROZEN_ASPECT));
}
/**
* Data provider with user with right permission or capability to remove from hold a specific node
*
* @return user model and the node ref to be removed from hold
*/
@DataProvider(name = "userWithPermissionForRemoveFromHold")
public Object[][] getUserWithPermissionForAddToHold()
{
//create record folder
RecordCategoryChild recordFolder = createCategoryFolderInFilePlan();
nodesToBeClean.add(recordFolder.getParentId());
UserModel user = roleService.createUserWithRMRoleAndRMNodePermission(ROLE_RM_MANAGER.roleId,
recordFolder.getId(),
PERMISSION_READ_RECORDS);
getRestAPIFactory().getRMUserAPI().addUserPermission(holdNodeRefOne, user, PERMISSION_FILING);
//create file that will be removed from hold
FileModel contentPermission = dataContent.usingAdmin().usingSite(testSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
//add files to hold
asList(recordFolder.getId(), contentPermission.getNodeRefWithoutVersion())
.forEach(id -> getRestAPIFactory()
.getHoldsAPI(getAdminUser())
.addChildToHold(HoldChild.builder().id(id).build(), holdNodeRefOne));
return new Object[][]
{
// user with write permission on the content
{
roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole.SiteConsumer,
holdNodeRefOne, UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING),
contentPermission.getNodeRefWithoutVersion()
},
//user with read permission on RM record folder
{
user, recordFolder.getId()
},
};
}
@Test(dataProvider = "userWithPermissionForRemoveFromHold")
public void removeFromHoldWithUserWithPermission(UserModel userModel, String nodeIdToBeRemoved) throws Exception
{
STEP("Update the list of users to be deleted after running the tests");
usersToBeClean.add(userModel);
STEP("Remove node from hold with user with right permission and capability");
getRestAPIFactory().getHoldsAPI(userModel).deleteHoldChild(holdNodeRefOne, nodeIdToBeRemoved);
STEP("Check node is not frozen.");
assertFalse(hasAspect(nodeIdToBeRemoved, FROZEN_ASPECT));
}
private Hold createHold(String parentId, Hold hold, UserModel user)
{
FilePlanAPI filePlanAPI = getRestAPIFactory().getFilePlansAPI(user);
return filePlanAPI.createHold(hold, parentId);
}
@AfterClass(alwaysRun = true)
public void cleanUpRemoveContentFromHold()
{
holdsListRef.forEach(holdRef -> getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(holdRef));
dataSite.usingAdmin().deleteSite(testSite);
dataSite.usingAdmin().deleteSite(privateSite);
usersToBeClean.forEach(user -> getDataUser().usingAdmin().deleteUser(user));
nodesToBeClean.forEach(this::deleteRecordCategory);
}
}

View File

@@ -538,6 +538,11 @@
<type>d:text</type>
<mandatory>true</mandatory>
</property>
<property name="rma:holdDeletionReason">
<title>Hold Deletion Reason</title>
<type>d:text</type>
<mandatory>false</mandatory>
</property>
</properties>
<associations>

View File

@@ -69,7 +69,32 @@
<property name="transactionService" ref="transactionService" />
</bean>
<bean class="org.alfresco.rm.rest.api.unfiledcontainers.UnfiledContainerEntityResource">
<bean class="org.alfresco.rm.rest.api.fileplans.FilePlanHoldsRelation">
<property name="apiUtils" ref="apiUtils" />
<property name="nodesModelFactory" ref="nodesModelFactory" />
<property name="holdService" ref="HoldService" />
<property name="fileFolderService" ref="FileFolderService" />
<property name="transactionService" ref="transactionService" />
</bean>
<bean class="org.alfresco.rm.rest.api.holds.HoldsEntityResource" >
<property name="holdService" ref="HoldService" />
<property name="apiUtils" ref="apiUtils" />
<property name="nodesModelFactory" ref="nodesModelFactory" />
<property name="fileFolderService" ref="FileFolderService" />
<property name="transactionService" ref="transactionService" />
</bean>
<bean class="org.alfresco.rm.rest.api.holds.HoldsChildrenRelation">
<property name="holdService" ref="HoldService" />
<property name="apiUtils" ref="apiUtils" />
<property name="nodesModelFactory" ref="nodesModelFactory" />
<property name="fileFolderService" ref="FileFolderService" />
<property name="transactionService" ref="transactionService" />
<property name="permissionService" ref="PermissionService" />
</bean>
<bean class="org.alfresco.rm.rest.api.unfiledcontainers.UnfiledContainerEntityResource">
<property name="apiUtils" ref="apiUtils" />
<property name="fileFolderService" ref="FileFolderService" />
<property name="nodesModelFactory" ref="nodesModelFactory" />

View File

@@ -1614,6 +1614,8 @@
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.createHold=RM_CAP.0.rma:filePlanComponent.CreateHold
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.getHoldReason=RM.Read.0
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.setHoldReason=RM_CAP.0.rma:filePlanComponent.EditHold
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.setHoldDeletionReason=RM_CAP.0.rma:filePlanComponent.EditHold
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.updateHold=RM_CAP.0.rma:filePlanComponent.EditHold
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.deleteHold=RM_CAP.0.rma:filePlanComponent.DeleteHold
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.addToHold=RM_CAP.0.rma:filePlanComponent.AddToHold
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.addToHolds=RM_ALLOW

View File

@@ -27,6 +27,7 @@
package org.alfresco.module.org_alfresco_module_rm.audit.event;
import static org.alfresco.module.org_alfresco_module_rm.audit.event.HoldUtils.HOLD_DELETION_REASON;
import static org.alfresco.repo.policy.Behaviour.NotificationFrequency.EVERY_EVENT;
import java.io.Serializable;
@@ -77,6 +78,8 @@ public class DeleteHoldAuditEvent extends AuditEvent implements NodeServicePolic
public void beforeDeleteNode(NodeRef holdNodeRef)
{
Map<QName, Serializable> auditProperties = HoldUtils.makePropertiesMap(holdNodeRef, nodeService);
auditProperties.put(HOLD_DELETION_REASON, nodeService.getProperty(holdNodeRef, PROP_HOLD_DELETION_REASON));
recordsManagementAuditService.auditEvent(holdNodeRef, getName(), auditProperties, null, true, false);
}
}

View File

@@ -47,6 +47,7 @@ class HoldUtils
{
/** A QName to display for the hold name. */
public static final QName HOLD_NAME = QName.createQName(RecordsManagementModel.RM_URI, "Hold Name");
public static final QName HOLD_DELETION_REASON = QName.createQName(RecordsManagementModel.RM_URI, "Hold deletion reason");
/** A QName to display for the hold node ref. */
public static final QName HOLD_NODEREF = QName.createQName(RecordsManagementModel.RM_URI, "Hold NodeRef");

View File

@@ -111,6 +111,24 @@ public interface HoldService
*/
void setHoldReason(NodeRef hold, String reason);
/**
* Sets the reason for the hold deletion
*
* @param hold The {@link NodeRef} of the hold
* @param reason {@link String} The reason for the hold
*/
void setHoldDeletionReason(NodeRef hold, String reason);
/**
* Updates a hold with the given name, reason and description
*
* @param hold The {@link NodeRef} of the hold
* @param name {@link String} The name of the hold
* @param reason {@link String} The reason of the hold
* @param description {@link String} The description of the hold
*/
void updateHold(NodeRef hold, String name, String reason, String description);
/**
* Deletes the hold
*

View File

@@ -29,6 +29,7 @@ package org.alfresco.module.org_alfresco_module_rm.hold;
import static org.alfresco.model.ContentModel.ASPECT_LOCKABLE;
import static org.alfresco.model.ContentModel.ASSOC_CONTAINS;
import static org.alfresco.model.ContentModel.PROP_DESCRIPTION;
import static org.alfresco.model.ContentModel.PROP_NAME;
import java.io.Serializable;
@@ -458,11 +459,11 @@ public class HoldServiceImpl extends ServiceBaseImpl
// create map of properties
Map<QName, Serializable> properties = new HashMap<>(3);
properties.put(ContentModel.PROP_NAME, name);
properties.put(PROP_NAME, name);
properties.put(PROP_HOLD_REASON, reason);
if (description != null && !description.isEmpty())
{
properties.put(ContentModel.PROP_DESCRIPTION, description);
properties.put(PROP_DESCRIPTION, description);
}
// create assoc name
@@ -512,6 +513,39 @@ public class HoldServiceImpl extends ServiceBaseImpl
}
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.hold.HoldService#setHoldDeletionReason(org.alfresco.service.cmr.repository.NodeRef, java.lang.String)
*/
@Override
public void setHoldDeletionReason(NodeRef hold, String reason)
{
ParameterCheck.mandatory("hold", hold);
ParameterCheck.mandatory("reason", reason);
if (nodeService.exists(hold) && isHold(hold))
{
nodeService.setProperty(hold, PROP_HOLD_DELETION_REASON, reason);
}
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.hold.HoldService#updateHold(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.lang.String, java.lang.String) (org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public void updateHold(NodeRef hold, String name, String reason, String description)
{
ParameterCheck.mandatory("hold", hold);
ParameterCheck.mandatory("name", name);
ParameterCheck.mandatory("reason", reason);
if (nodeService.exists(hold) && isHold(hold))
{
nodeService.setProperty(hold, PROP_NAME, name);
nodeService.setProperty(hold, PROP_HOLD_REASON, reason);
nodeService.setProperty(hold, PROP_DESCRIPTION, description);
}
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.hold.HoldService#deleteHold(org.alfresco.service.cmr.repository.NodeRef)
*/
@@ -563,7 +597,7 @@ public class HoldServiceImpl extends ServiceBaseImpl
if (permissionService.hasPermission(nodeRef, permission) == AccessStatus.DENIED)
{
heldNames.add((String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME));
heldNames.add((String) nodeService.getProperty(nodeRef, PROP_NAME));
}
}
catch (AccessDeniedException ade)
@@ -630,7 +664,7 @@ public class HoldServiceImpl extends ServiceBaseImpl
{
if (!isHold(hold))
{
final String holdName = (String) nodeService.getProperty(hold, ContentModel.PROP_NAME);
final String holdName = (String) nodeService.getProperty(hold, PROP_NAME);
throw new IntegrityException(I18NUtil.getMessage("rm.hold.not-hold", holdName), null);
}
@@ -688,7 +722,7 @@ public class HoldServiceImpl extends ServiceBaseImpl
{
if (!isRecordFolder(nodeRef) && !instanceOf(nodeRef, ContentModel.TYPE_CONTENT))
{
final String nodeName = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
final String nodeName = (String) nodeService.getProperty(nodeRef, PROP_NAME);
throw new IntegrityException(I18NUtil.getMessage("rm.hold.add-to-hold-invalid-type", nodeName), null);
}
@@ -795,7 +829,7 @@ public class HoldServiceImpl extends ServiceBaseImpl
{
if (!isHold(hold))
{
final String holdName = (String) nodeService.getProperty(hold, ContentModel.PROP_NAME);
final String holdName = (String) nodeService.getProperty(hold, PROP_NAME);
throw new IntegrityException(I18NUtil.getMessage("rm.hold.not-hold", holdName), null);
}

View File

@@ -35,6 +35,7 @@ import org.alfresco.service.namespace.QName;
*
* @author Roy Wetherall
*/
@SuppressWarnings("PMD.ConstantsInInterface")
@AlfrescoPublicApi
public interface RecordsManagementModel extends RecordsManagementCustomModel
{
@@ -200,6 +201,7 @@ public interface RecordsManagementModel extends RecordsManagementCustomModel
// Hold type
QName TYPE_HOLD = QName.createQName(RM_URI, "hold");
QName PROP_HOLD_REASON = QName.createQName(RM_URI, "holdReason");
QName PROP_HOLD_DELETION_REASON = QName.createQName(RM_URI, "holdDeletionReason");
//since 3.2
@Deprecated
QName ASSOC_FROZEN_RECORDS = QName.createQName(RM_URI, "frozenRecords");

View File

@@ -0,0 +1,155 @@
/*
* #%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.fileplans;
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.stream.Collectors;
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;
import org.alfresco.rest.framework.WebApiDescription;
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.ApiNodesModelFactory;
import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils;
import org.alfresco.rm.rest.api.model.HoldModel;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.transaction.TransactionService;
import org.springframework.beans.factory.InitializingBean;
/**
* File plan holds relation
*
* @author Damian Ujma
*/
@RelationshipResource(name = "holds", entityResource = FilePlanEntityResource.class, title = "Holds in a file plan")
public class FilePlanHoldsRelation implements
RelationshipResourceAction.Create<HoldModel>,
RelationshipResourceAction.Read<HoldModel>,
InitializingBean
{
private FilePlanComponentsApiUtils apiUtils;
private ApiNodesModelFactory nodesModelFactory;
private HoldService holdService;
private FileFolderService fileFolderService;
private TransactionService transactionService;
@Override
public void afterPropertiesSet() throws Exception
{
mandatory("apiUtils", this.apiUtils);
mandatory("nodesModelFactory", this.nodesModelFactory);
mandatory("holdService", this.holdService);
mandatory("fileFolderService", this.fileFolderService);
mandatory("transactionService", this.transactionService);
}
@Override
@WebApiDescription(title = "Return a paged list of holds for the file plan identified by 'filePlanId'")
public CollectionWithPagingInfo<HoldModel> readAll(String filePlanId, Parameters parameters)
{
checkNotBlank("filePlanId", filePlanId);
mandatory("parameters", parameters);
NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(filePlanId, RecordsManagementModel.TYPE_FILE_PLAN);
List<NodeRef> holds = holdService.getHolds(parentNodeRef);
List<HoldModel> page = holds.stream()
.map(hold -> fileFolderService.getFileInfo(hold))
.map(nodesModelFactory::createHoldModel)
.skip(parameters.getPaging().getSkipCount())
.limit(parameters.getPaging().getMaxItems())
.collect(Collectors.toCollection(LinkedList::new));
int totalItems = holds.size();
boolean hasMore = parameters.getPaging().getSkipCount() + parameters.getPaging().getMaxItems() < totalItems;
return CollectionWithPagingInfo.asPaged(parameters.getPaging(), page, hasMore, totalItems);
}
@Override
@WebApiDescription(title = "Create one (or more) holds in a file plan identified by 'filePlanId'")
public List<HoldModel> create(String filePlanId, List<HoldModel> holds, Parameters parameters)
{
checkNotBlank("filePlanId", filePlanId);
mandatory("holds", holds);
mandatory("parameters", parameters);
NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(filePlanId, RecordsManagementModel.TYPE_FILE_PLAN);
RetryingTransactionCallback<List<NodeRef>> callback = () -> {
List<NodeRef> createdNodes = new LinkedList<>();
for (HoldModel nodeInfo : holds)
{
NodeRef newNodeRef = holdService.createHold(parentNodeRef, nodeInfo.name(), nodeInfo.reason(),
nodeInfo.description());
createdNodes.add(newNodeRef);
}
return createdNodes;
};
List<NodeRef> createdNodes = transactionService.getRetryingTransactionHelper()
.doInTransaction(callback, false, true);
return createdNodes.stream()
.map(hold -> fileFolderService.getFileInfo(hold))
.map(nodesModelFactory::createHoldModel)
.collect(Collectors.toCollection(LinkedList::new));
}
public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
{
this.apiUtils = apiUtils;
}
public void setNodesModelFactory(ApiNodesModelFactory nodesModelFactory)
{
this.nodesModelFactory = nodesModelFactory;
}
public void setHoldService(HoldService holdService)
{
this.holdService = holdService;
}
public void setFileFolderService(FileFolderService fileFolderService)
{
this.fileFolderService = fileFolderService;
}
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
}

View File

@@ -0,0 +1,207 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.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.stream.Collectors;
import org.alfresco.module.org_alfresco_module_rm.hold.HoldService;
import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel;
import org.alfresco.repo.node.integrity.IntegrityException;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.rest.framework.WebApiDescription;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
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.ApiNodesModelFactory;
import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils;
import org.alfresco.rm.rest.api.model.HoldChild;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.transaction.TransactionService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.extensions.surf.util.I18NUtil;
/**
* Hold children relation
*
* @author Damian Ujma
*/
@RelationshipResource(name = "children", entityResource = HoldsEntityResource.class, title = "Children of a hold")
public class HoldsChildrenRelation implements
RelationshipResourceAction.Create<HoldChild>,
RelationshipResourceAction.Read<HoldChild>,
RelationshipResourceAction.Delete,
InitializingBean
{
private HoldService holdService;
private FilePlanComponentsApiUtils apiUtils;
private ApiNodesModelFactory nodesModelFactory;
private TransactionService transactionService;
private FileFolderService fileFolderService;
private PermissionService permissionService;
@Override
public void afterPropertiesSet() throws Exception
{
mandatory("holdService", holdService);
mandatory("apiUtils", apiUtils);
mandatory("nodesModelFactory", nodesModelFactory);
mandatory("transactionService", transactionService);
mandatory("fileFolderService", fileFolderService);
}
@Override
@WebApiDescription(title = "Add one (or more) children as children of a hold identified by 'holdId'")
public List<HoldChild> create(String holdId, List<HoldChild> children, Parameters parameters)
{
// validate parameters
checkNotBlank("holdId", holdId);
mandatory("children", children);
mandatory("parameters", parameters);
NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
RetryingTransactionCallback<List<NodeRef>> callback = () -> {
List<NodeRef> createdNodes = children.stream()
.map(holdChild -> new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, holdChild.id()))
.collect(Collectors.toList());
try
{
holdService.addToHold(parentNodeRef, createdNodes);
}
catch (IntegrityException exception)
{
// Throw 400 Bad Request when a node with id 'holdId' is not a hold or a child cannot be added to a hold
throw new InvalidArgumentException(exception.getMsgId()).initCause(exception);
}
return createdNodes;
};
List<NodeRef> nodeInfos = transactionService.getRetryingTransactionHelper()
.doInTransaction(callback, false, true);
return nodeInfos.stream()
.map(nodeRef -> new HoldChild(nodeRef.getId()))
.collect(Collectors.toCollection(LinkedList::new));
}
@Override
@WebApiDescription(title = "Return a paged list of hold children for the hold identified by 'holdId'")
public CollectionWithPagingInfo<HoldChild> readAll(String holdId, Parameters parameters)
{
checkNotBlank("holdId", holdId);
mandatory("parameters", parameters);
NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
List<NodeRef> children = holdService.getHeld(parentNodeRef);
List<HoldChild> page = children.stream()
.map(NodeRef::getId)
.map(HoldChild::new)
.skip(parameters.getPaging().getSkipCount())
.limit(parameters.getPaging().getMaxItems())
.collect(Collectors.toCollection(LinkedList::new));
int totalItems = children.size();
boolean hasMore = parameters.getPaging().getSkipCount() + parameters.getPaging().getMaxItems() < totalItems;
return CollectionWithPagingInfo.asPaged(parameters.getPaging(), page, hasMore, totalItems);
}
@Override
@WebApiDescription(title = "Remove a child from a hold", description = "Remove a child with id 'childId' from a hold with id 'holdId'")
public void delete(String holdId, String childId, Parameters parameters)
{
checkNotBlank("holdId", holdId);
checkNotBlank("childId", childId);
mandatory("parameters", parameters);
NodeRef nodeRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
NodeRef childRef = apiUtils.lookupByPlaceholder(childId);
if (permissionService.hasReadPermission(childRef) == AccessStatus.DENIED)
{
throw new PermissionDeniedException(I18NUtil.getMessage("permissions.err_access_denied"));
}
RetryingTransactionCallback<List<NodeRef>> callback = () -> {
try
{
holdService.removeFromHold(nodeRef, childRef);
}
catch (IntegrityException exception)
{
// Throw 400 Bad Request when a node with id 'holdId' is not a hold
throw new InvalidArgumentException(exception.getMsgId()).initCause(exception);
}
return null;
};
transactionService.getRetryingTransactionHelper().doInTransaction(callback, false, true);
}
public void setHoldService(HoldService holdService)
{
this.holdService = holdService;
}
public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
{
this.apiUtils = apiUtils;
}
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
public void setNodesModelFactory(ApiNodesModelFactory nodesModelFactory)
{
this.nodesModelFactory = nodesModelFactory;
}
public void setFileFolderService(FileFolderService fileFolderService)
{
this.fileFolderService = fileFolderService;
}
public void setPermissionService(PermissionService permissionService)
{
this.permissionService = permissionService;
}
}

View File

@@ -0,0 +1,184 @@
/*
* #%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 jakarta.servlet.http.HttpServletResponse;
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;
import org.alfresco.rest.framework.Operation;
import org.alfresco.rest.framework.WebApiDescription;
import org.alfresco.rest.framework.WebApiParam;
import org.alfresco.rest.framework.resource.EntityResource;
import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction;
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.HoldDeletionReason;
import org.alfresco.rm.rest.api.model.HoldModel;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
/**
* Hold entity resource
*
* @author Damian Ujma
*/
@EntityResource(name = "holds", title = "Holds")
public class HoldsEntityResource implements
EntityResourceAction.ReadById<HoldModel>,
EntityResourceAction.Update<HoldModel>,
EntityResourceAction.Delete,
InitializingBean
{
private FilePlanComponentsApiUtils apiUtils;
private FileFolderService fileFolderService;
private ApiNodesModelFactory nodesModelFactory;
private HoldService holdService;
private TransactionService transactionService;
@Override
public void afterPropertiesSet() throws Exception
{
mandatory("nodesModelFactory", nodesModelFactory);
mandatory("apiUtils", apiUtils);
mandatory("fileFolderService", fileFolderService);
mandatory("holdService", holdService);
mandatory("transactionService", transactionService);
}
@Override
@WebApiDescription(title = "Get hold information", description = "Get information for a hold with id 'holdId'")
@WebApiParam(name = "holdId", title = "The hold id")
public HoldModel readById(String holdId, Parameters parameters)
{
checkNotBlank("holdId", holdId);
mandatory("parameters", parameters);
NodeRef hold = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
FileInfo info = fileFolderService.getFileInfo(hold);
return nodesModelFactory.createHoldModel(info);
}
@Override
@WebApiDescription(title = "Update a hold", description = "Updates a hold with id 'holdId'")
public HoldModel update(String holdId, HoldModel holdModel, Parameters parameters)
{
checkNotBlank("holdId", holdId);
mandatory("holdModel", holdModel);
mandatory("holdModel.name", holdModel.name());
mandatory("holdModel.reason", holdModel.reason());
mandatory("parameters", parameters);
NodeRef nodeRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
RetryingTransactionCallback<Void> callback = () -> {
holdService.updateHold(nodeRef, holdModel.name(), holdModel.reason(), holdModel.description());
return null;
};
transactionService.getRetryingTransactionHelper().doInTransaction(callback, false, true);
RetryingTransactionCallback<FileInfo> readCallback = () -> fileFolderService.getFileInfo(nodeRef);
FileInfo info = transactionService.getRetryingTransactionHelper().doInTransaction(readCallback, false, true);
return nodesModelFactory.createHoldModel(info);
}
@Override
@WebApiDescription(title = "Delete hold", description = "Deletes a hold with id 'holdId'")
public void delete(String holdId, Parameters parameters)
{
checkNotBlank("holdId", holdId);
mandatory("parameters", parameters);
NodeRef hold = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
RetryingTransactionCallback<Void> callback = () -> {
holdService.deleteHold(hold);
return null;
};
transactionService.getRetryingTransactionHelper().doInTransaction(callback, false, true);
}
@Operation("delete")
@WebApiDescription(title = "Delete hold with a reason",
successStatus = HttpServletResponse.SC_OK)
public HoldDeletionReason deleteHoldWithReason(String holdId, HoldDeletionReason reason, Parameters parameters,
WithResponse withResponse)
{
checkNotBlank("holdId", holdId);
mandatory("reason", reason);
mandatory("parameters", parameters);
NodeRef hold = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
String deletionReason = reason.reason();
RetryingTransactionCallback<Void> callback = () -> {
if (StringUtils.isNotBlank(deletionReason))
{
holdService.setHoldDeletionReason(hold, deletionReason);
}
holdService.deleteHold(hold);
return null;
};
transactionService.getRetryingTransactionHelper().doInTransaction(callback, false, true);
return reason;
}
public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
{
this.apiUtils = apiUtils;
}
public void setFileFolderService(FileFolderService fileFolderService)
{
this.fileFolderService = fileFolderService;
}
public void setNodesModelFactory(ApiNodesModelFactory nodesModelFactory)
{
this.nodesModelFactory = nodesModelFactory;
}
public void setHoldService(HoldService holdService)
{
this.holdService = holdService;
}
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
}

View File

@@ -0,0 +1,36 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
/**
* Package info that defines the Information Governance Holds REST API
*
* @author Damian Ujma
*/
@WebApi(name="gs", scope=Api.SCOPE.PUBLIC, version=1)
package org.alfresco.rm.rest.api.holds;
import org.alfresco.rest.framework.Api;
import org.alfresco.rest.framework.WebApi;

View File

@@ -47,6 +47,7 @@ import org.alfresco.rest.api.model.UserInfo;
import org.alfresco.rest.framework.jacksonextensions.BeanPropertiesFilter;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rm.rest.api.model.FilePlan;
import org.alfresco.rm.rest.api.model.HoldModel;
import org.alfresco.rm.rest.api.model.RMNode;
import org.alfresco.rm.rest.api.model.Record;
import org.alfresco.rm.rest.api.model.RecordCategory;
@@ -637,6 +638,21 @@ public class ApiNodesModelFactory
}
}
/**
* Creates an object of type HoldModel
*
* @param info info of the hold
* @return HoldModel object
*/
public HoldModel createHoldModel(FileInfo info)
{
return new HoldModel(info.getNodeRef().getId(),
(String) info.getProperties().get(ContentModel.PROP_NAME),
(String) info.getProperties().get(ContentModel.PROP_DESCRIPTION),
(String) info.getProperties().get(RecordsManagementModel.PROP_HOLD_REASON));
}
/**
* Creates an object of type FilePlan
*

View File

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

View File

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

View File

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

View File

@@ -87,6 +87,7 @@ public class HoldServiceImplUnitTest extends BaseUnitTest
/** test values */
private static final String HOLD_NAME = "holdname";
private static final String HOLD_REASON = "holdreason";
private static final String HOLD_DELETION_REASON = "holddeletionreason";
private static final String HOLD_DESCRIPTION = "holddescription";
private static final String GENERIC_ERROR_MSG = "any error message text";
@@ -173,7 +174,7 @@ public class HoldServiceImplUnitTest extends BaseUnitTest
}
@Test (expected=AlfrescoRuntimeException.class)
public void getHold()
public void testGetHold()
{
// setup node service interactions
when(mockedNodeService.getChildByName(eq(holdContainer), eq(ContentModel.ASSOC_CONTAINS), anyString())).thenReturn(null)
@@ -194,19 +195,19 @@ public class HoldServiceImplUnitTest extends BaseUnitTest
}
@Test (expected=RuntimeException.class)
public void getHeldNotAHold()
public void testGetHeldNotAHold()
{
holdService.getHeld(recordFolder);
}
@Test
public void getHeldNoResults()
public void testGetHeldNoResults()
{
assertTrue(holdService.getHeld(hold).isEmpty());
}
@Test
public void getHeldWithResults()
public void testGetHeldWithResults()
{
// setup record folder in hold
List<ChildAssociationRef> holds = new ArrayList<>(2);
@@ -259,7 +260,7 @@ public class HoldServiceImplUnitTest extends BaseUnitTest
}
@Test
public void getHoldReason()
public void testGetHoldReason()
{
// setup node service interactions
when(mockedNodeService.exists(hold))
@@ -306,6 +307,80 @@ public class HoldServiceImplUnitTest extends BaseUnitTest
verify(mockedNodeService).setProperty(hold, PROP_HOLD_REASON, HOLD_REASON);
}
@Test
public void setHoldDeletionReasonForNodeDoesNotExist()
{
// setup node service interactions
when(mockedNodeService.exists(hold))
.thenReturn(false);
// node does not exist
holdService.setHoldDeletionReason(hold, HOLD_DELETION_REASON);
verify(mockedNodeService, never()).setProperty(hold, PROP_HOLD_DELETION_REASON, HOLD_DELETION_REASON);
}
@Test
public void setHoldDeletionReasonForNodeIsNotAHold()
{
// setup node service interactions
when(mockedNodeService.exists(hold))
.thenReturn(true);
// node isn't a hold
holdService.setHoldDeletionReason(recordFolder, HOLD_DELETION_REASON);
verify(mockedNodeService, never()).setProperty(hold, PROP_HOLD_DELETION_REASON, HOLD_DELETION_REASON);
}
@Test
public void setHoldDeletionReason()
{
// setup node service interactions
when(mockedNodeService.exists(hold))
.thenReturn(true);
// set hold deletion reason
holdService.setHoldDeletionReason(hold, HOLD_DELETION_REASON);
verify(mockedNodeService).setProperty(hold, PROP_HOLD_DELETION_REASON, HOLD_DELETION_REASON);
}
@Test
public void updateHoldThatDoesNotExist()
{
// setup node service interactions
when(mockedNodeService.exists(hold))
.thenReturn(false);
// node does not exist
holdService.updateHold(hold, HOLD_NAME, HOLD_REASON, HOLD_DESCRIPTION);
verify(mockedNodeService, never()).setProperty(any(NodeRef.class), any(QName.class), any(String.class));
}
@Test
public void updateHoldThatIsNotAHold()
{
// setup node service interactions
when(mockedNodeService.exists(hold))
.thenReturn(true);
// node isn't a hold
holdService.updateHold(recordFolder, HOLD_NAME, HOLD_REASON, HOLD_DESCRIPTION);
verify(mockedNodeService, never()).setProperty(any(NodeRef.class), any(QName.class), any(String.class));
}
@Test
public void updateHold()
{
// setup node service interactions
when(mockedNodeService.exists(hold))
.thenReturn(true);
// update hold
holdService.updateHold(hold, HOLD_NAME, HOLD_REASON, HOLD_DESCRIPTION);
verify(mockedNodeService).setProperty(hold, ContentModel.PROP_NAME, HOLD_NAME);
verify(mockedNodeService).setProperty(hold, ContentModel.PROP_DESCRIPTION, HOLD_DESCRIPTION);
verify(mockedNodeService).setProperty(hold, PROP_HOLD_REASON, HOLD_REASON);
}
@Test (expected=AlfrescoRuntimeException.class)
public void deleteHoldNotAHold()
{

View File

@@ -38,6 +38,8 @@ tags:
description: Retrieve and manage unfiled records containers
- name: unfiled-record-folders
description: Retrieve and manage unfiled record folders
- name: holds
description: Retrieve and manage holds
paths:
## GS sites
@@ -418,6 +420,124 @@ paths:
description: New name clashes with an existing node in the current parent container
'422':
description: Model integrity exception, including node name with invalid characters
'/file-plans/{filePlanId}/holds':
get:
tags:
- file-plans
summary: Get all holds in a file plan
description: |
Returns a list of holds.
operationId: getHolds
parameters:
- $ref: '#/parameters/filePlanIdWithAliasParam'
- $ref: '#/parameters/skipCountParam'
- $ref: '#/parameters/maxItemsParam'
consumes:
- application/json
produces:
- application/json
responses:
'200':
description: Successful response
schema:
$ref: '#/definitions/HoldPaging'
'401':
description: Authentication failed
'403':
description: Current user does not have permission to read **filePlanId**
'404':
description: "**filePlanId** does not exist"
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
post:
tags:
- file-plans
summary: Create holds for a file plan
description: |
Creates a new hold.
You must specify at least a **name** and a **reason** property.
**Note:** You can create more than one hold by specifying a list of holds in the JSON body.
For example, the following JSON body creates two holds:
```JSON
[
{
"name":"Hold1",
"description": "Description1",
"reason": "Reason1"
},
{
"name":"Hold2",
"description": "Description2",
"reason": "Reason2"
}
]
```
If you specify a list as input, then a paginated list rather than an entry is returned in the response body. For example:
```JSON
{
"list": {
"pagination": {
"count": 2,
"hasMoreItems": false,
"totalItems": 2,
"skipCount": 0,
"maxItems": 100
},
"entries": [
{
"entry": {
...
}
},
{
"entry": {
...
}
}
]
}
}
```
operationId: addHold
parameters:
- $ref: '#/parameters/filePlanIdWithAliasParam'
- in: body
name: nodeBodyCreate
description: The node information to create.
required: true
schema:
$ref: '#/definitions/HoldCreateBodyModel'
consumes:
- application/json
produces:
- application/json
responses:
'201':
description: Successful response
schema:
$ref: '#/definitions/HoldModelEntry'
'400':
description: |
Invalid parameter: **filePlanId** is not a valid format or **HoldCreateBodyModel** is not valid
'401':
description: Authentication failed
'403':
description: Current user does not have permission to create a hold
'404':
description: |
**filePlanId** does not exist
'409':
description: A hold with the name **name** already exists
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
## Unfiled records containers
'/unfiled-containers/{unfiledContainerId}':
get:
@@ -2092,6 +2212,289 @@ paths:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
## Holds
'/holds/{holdId}':
get:
tags:
- holds
summary: Get a hold
description: |
Gets information for hold with id **holdId**.
operationId: getHold
parameters:
- $ref: '#/parameters/holdIdParam'
produces:
- application/json
responses:
'200':
description: Successful response
schema:
$ref: '#/definitions/HoldModelEntry'
'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'
put:
tags:
- holds
summary: Update a hold
description: |
Updates the hold with id **holdId**. For example, you can rename a hold:
```JSON
{
"name":"My new name",
"description":"Existing description",
"reason":"Existing reason"
}
```
operationId: updateHold
parameters:
- $ref: '#/parameters/holdIdParam'
- in: body
name: holdBodyUpdate
description: The hold information to update.
required: true
schema:
$ref: '#/definitions/HoldCreateBodyModel'
produces:
- application/json
responses:
'200':
description: Successful response
schema:
$ref: '#/definitions/HoldModelEntry'
'400':
description: |
Invalid parameter: the update request is invalid or **holdId** is not a valid format or **holdBodyUpdate** is invalid
'401':
description: Authentication failed
'403':
description: Current user does not have permission to update **holdId**
'404':
description: "**holdId** does not exist"
'409':
description: Updated name clashes with an existing node in the current parent folder
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
delete:
tags:
- holds
summary: Delete a hold
description: |
Deletes the hold with id **holdId**.
operationId: deleteHold
parameters:
- $ref: '#/parameters/holdIdParam'
produces:
- application/json
responses:
'204':
description: Successful response
'400':
description: |
Invalid parameter: **holdId** is not a valid format
'401':
description: Authentication failed
'403':
description: Current user does not have permission to delete **holdId**
'404':
description: "**holdId** does not exist"
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
'/holds/{holdId}/delete':
post:
tags:
- holds
summary: Delete a hold with a reason
description: |
Deletes the hold with id **holdId** and stores a reason for deletion in the audit log.
A **reason** must be specified in the request body.
operationId: deleteHoldWithReason
parameters:
- $ref: '#/parameters/holdIdParam'
- in: body
name: holdDeletionReason
description: Reason for deletion.
required: true
schema:
$ref: '#/definitions/HoldDeletionReason'
produces:
- application/json
responses:
'200':
description: Successful response
schema:
$ref: '#/definitions/HoldDeletionReasonEntry'
'400':
description: |
Invalid parameter: **holdId** is not a valid format
'401':
description: Authentication failed
'403':
description: Current user does not have permission to update or delete **holdId**
'404':
description: "**holdId** does not exist"
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
'/holds/{holdId}/children':
post:
tags:
- holds
summary: Add children to a hold
description: |
Add a child of a hold with id **holdId**.
You must specify the child **id**.
The API returns a 201 Created if the child is already a child of the hold.
**Note:** You can add more than one child by specifying a list of children in the JSON body.
For example, the following JSON body adds two children:
```JSON
[
{
"id":"a7c10f46-b85b-4de5-af1c-930056b736a7"
},
{
"id":"e0d79b71-be2b-4ce7-a846-a7c50cba20fb"
}
]
```
If you specify a list as input, then a paginated list rather than an entry is returned in the response body. For example:
```JSON
{
"list": {
"pagination": {
"count": 2,
"hasMoreItems": false,
"totalItems": 2,
"skipCount": 0,
"maxItems": 100
},
"entries": [
{
"entry": {
...
}
},
{
"entry": {
...
}
}
]
}
}
```
operationId: addChildToHold
parameters:
- $ref: '#/parameters/holdIdParam'
- in: body
name: nodeId
description: The node id.
required: true
schema:
$ref: '#/definitions/HoldChild'
consumes:
- application/json
produces:
- application/json
responses:
'201':
description: Successful response
schema:
$ref: '#/definitions/HoldChildEntry'
'400':
description: |
Invalid parameter: **holdId** is not a valid format or **HoldChild** is not valid
'401':
description: Authentication failed
'403':
description: Current user does not have permission to add items to the hold
'404':
description: |
**holdId** does not exist
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
get:
tags:
- holds
summary: Get children of a hold
description: |
Returns a list of children of a hold with id **holdId**.
operationId: getHoldChildren
parameters:
- $ref: '#/parameters/holdIdParam'
- $ref: '#/parameters/skipCountParam'
- $ref: '#/parameters/maxItemsParam'
consumes:
- application/json
produces:
- application/json
responses:
'200':
description: Successful response
schema:
$ref: '#/definitions/HoldChildPaging'
'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}/children/{holdChildId}':
delete:
tags:
- holds
summary: Delete a child of a hold
description: |
Deletes the relationship between a child with id **holdChildId** and a parent hold with id **holdId**.
operationId: removeHoldChild
parameters:
- $ref: '#/parameters/holdIdParam'
- $ref: '#/parameters/holdChildIdParam'
produces:
- application/json
responses:
'204':
description: Successful response
'400':
description: |
Invalid parameter: **holdChildId** or **holdId** is not a valid format
'401':
description: Authentication failed
'403':
description: Current user does not have permission to delete **holdChildId**
'404':
description: " **holdChildId** or **holdId** does not exist"
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
parameters:
## File plans
@@ -2175,7 +2578,7 @@ parameters:
description: Also include **source** (in addition to **entries**) with folder information on the parent node the specified parent **unfiledContainerId**
required: false
type: boolean
## Unfiled record folders
## Unfiled record folders
unfiledRecordFolderIdParam:
name: unfiledRecordFolderId
in: path
@@ -2446,6 +2849,19 @@ parameters:
items:
type: string
collectionFormat: csv
# Holds
holdIdParam:
name: holdId
in: path
description: The identifier of a hold.
required: true
type: string
holdChildIdParam:
name: holdChildId
in: path
description: The identifier of a child of a hold.
required: true
type: string
## Record
recordIdParam:
name: recordId
@@ -3519,6 +3935,89 @@ definitions:
properties:
association:
$ref: '#/definitions/ChildAssociationInfo'
## Holds
HoldModelEntry:
type: object
required:
- entry
properties:
entry:
$ref: '#/definitions/HoldModel'
HoldModel:
type: object
properties:
id:
type: string
name:
type: string
description:
type: string
reason:
type: string
HoldCreateBodyModel:
type: object
required:
- name
- reason
properties:
name:
type: string
description:
type: string
reason:
type: string
HoldPaging:
type: object
properties:
list:
type: object
properties:
pagination:
$ref: '#/definitions/Pagination'
entries:
type: array
items:
$ref: '#/definitions/HoldModelEntry'
HoldChild:
type: object
required:
- id
properties:
id:
type: string
HoldChildEntry:
type: object
required:
- entry
properties:
entry:
$ref: '#/definitions/HoldChild'
HoldChildPaging:
type: object
properties:
list:
type: object
properties:
pagination:
$ref: '#/definitions/Pagination'
entries:
type: array
items:
$ref: '#/definitions/HoldChildEntry'
HoldDeletionReasonEntry:
type: object
required:
- entry
properties:
entry:
$ref: '#/definitions/HoldDeletionReason'
HoldDeletionReason:
type: object
required:
- reason
properties:
reason:
type: string
##
RequestBodyFile:
type: object