REPO-340 / REPO-1136 - V1 REST API: Unlock Node

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/BRANCHES/DEV/5.2.N/root@129716 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Ancuta Morarasu
2016-08-19 18:37:10 +00:00
parent 0376e880d0
commit 063dde5d8b
7 changed files with 593 additions and 342 deletions

View File

@@ -154,6 +154,7 @@
<entry key="org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_CONFLICT}" />
<entry key="org.alfresco.service.cmr.lock.NodeLockedException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_CONFLICT}" />
<entry key="org.alfresco.service.cmr.lock.UnableToAquireLockException" value="422" />
<entry key="org.alfresco.service.cmr.lock.UnableToReleaseLockException" value="422" />
<entry key="org.alfresco.service.cmr.repository.DuplicateChildNodeNameException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_CONFLICT}" />
<entry key="org.alfresco.rest.framework.core.exceptions.StaleEntityException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_CONFLICT}" />
<entry key="org.alfresco.rest.framework.core.exceptions.RequestEntityTooLargeException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_REQUEST_ENTITY_TOO_LARGE}" />

View File

@@ -36,6 +36,7 @@ import org.alfresco.rest.api.model.Document;
import org.alfresco.rest.api.model.Folder;
import org.alfresco.rest.api.model.LockInfo;
import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.api.model.UnlockInfo;
import org.alfresco.rest.api.model.UserInfo;
import org.alfresco.rest.framework.resource.content.BasicContentInfo;
import org.alfresco.rest.framework.resource.content.BinaryResource;
@@ -255,6 +256,15 @@ public interface Nodes
*/
Node lock(String nodeId, LockInfo lockInfo, Parameters parameters);
/**
* Unlock a node
* @param nodeId
* @param unlockInfo
* @param parameters
* @return
*/
Node unlock(String nodeId, UnlockInfo unlockInfo, Parameters parameters);
/**
* API Constants - query parameters, etc
*/

View File

@@ -87,6 +87,7 @@ import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.api.model.PathInfo;
import org.alfresco.rest.api.model.PathInfo.ElementInfo;
import org.alfresco.rest.api.model.QuickShareLink;
import org.alfresco.rest.api.model.UnlockInfo;
import org.alfresco.rest.api.model.UserInfo;
import org.alfresco.rest.api.nodes.NodeAssocService;
import org.alfresco.rest.framework.core.exceptions.ApiException;
@@ -2970,6 +2971,40 @@ public class NodesImpl implements Nodes
return lockInfo;
}
@Override
public Node unlock(String nodeId, UnlockInfo unlockInfo, Parameters parameters)
{
NodeRef nodeRef = validateOrLookupNode(nodeId, null);
if (isSpecialNode(nodeRef, getNodeType(nodeRef)))
{
throw new PermissionDeniedException("Current user doesn't have permission to unlock node " + nodeId);
}
if (unlockInfo.getIncludeChildren() == null)
{
unlockInfo.setIncludeChildren(false);
}
if (unlockInfo.getAllowCheckedOut() == null)
{
unlockInfo.setAllowCheckedOut(false);
}
// If there is no lock placed on the node skip the operation.
if (lockService.getLockStatus(nodeRef) != LockStatus.NO_LOCK)
{
if (permissionService.hasPermission(nodeRef, PermissionService.UNLOCK).equals(AccessStatus.ALLOWED))
{
lockService.unlock(nodeRef, unlockInfo.getIncludeChildren(), unlockInfo.getAllowCheckedOut());
}
else
{
throw new PermissionDeniedException("Current user doesn't have permission to unlock node " + nodeId);
}
}
return getFolderOrDocument(nodeId, parameters);
}
/**
* @author Jamal Kaabi-Mofrad
*/

View File

@@ -0,0 +1,60 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* 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.api.model;
/**
* Representation of a unlock info
*
* @author Ancuta Morarasu
*/
public class UnlockInfo
{
private Boolean includeChildren;
private Boolean allowCheckedOut;
public UnlockInfo() {}
public Boolean getIncludeChildren()
{
return includeChildren;
}
public void setIncludeChildren(Boolean includeChildren)
{
this.includeChildren = includeChildren;
}
public Boolean getAllowCheckedOut()
{
return allowCheckedOut;
}
public void setAllowCheckedOut(Boolean allowCheckedOut)
{
this.allowCheckedOut = allowCheckedOut;
}
}

View File

@@ -33,6 +33,7 @@ import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.model.LockInfo;
import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.api.model.NodeTarget;
import org.alfresco.rest.api.model.UnlockInfo;
import org.alfresco.rest.framework.BinaryProperties;
import org.alfresco.rest.framework.Operation;
import org.alfresco.rest.framework.WebApiDescription;
@@ -180,5 +181,14 @@ public class NodesEntityResource implements
return nodes.lock(nodeId, lockInfo, parameters);
}
@Operation("unlock")
@WebApiDescription(title = "Unlock Node",
description="Removes a lock on a node.",
successStatus = HttpServletResponse.SC_OK)
public Node unlock(String nodeId, UnlockInfo unlockInfo, Parameters parameters, WithResponse withResponse)
{
return nodes.unlock(nodeId, unlockInfo, parameters);
}
}

View File

@@ -788,6 +788,18 @@ public abstract class AbstractBaseApiTest extends EnterpriseTestApi
return ResourceUtils.getFile(url);
}
protected Document lock(String nodeId, String body) throws Exception
{
HttpResponse response = post("nodes/" + nodeId + "/lock", body, null, 200);
return RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class);
}
protected Document unlock(String nodeId, String body) throws Exception
{
HttpResponse response = post("nodes/" + nodeId + "/unlock", body, null, 200);
return RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class);
}
protected static final long PAUSE_TIME = 5000; //millisecond
protected static final int MAX_RETRY = 20;

View File

@@ -78,6 +78,7 @@ import org.alfresco.rest.api.tests.util.MultiPartBuilder;
import org.alfresco.rest.api.tests.util.MultiPartBuilder.FileData;
import org.alfresco.rest.api.tests.util.MultiPartBuilder.MultiPartRequest;
import org.alfresco.rest.api.tests.util.RestApiUtil;
import org.alfresco.service.cmr.lock.LockType;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.PermissionService;
@@ -111,6 +112,7 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest
private static final String PROP_OWNER = "cm:owner";
private static final String URL_DELETED_NODES = "deleted-nodes";
private static final String EMPTY_BODY = "{}";
protected PermissionService permissionService;
@@ -3585,7 +3587,7 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest
setRequestContext(user1);
// create folder
Folder folderResp = createFolder(Nodes.PATH_MY, "folderT");
Folder folderResp = createFolder(Nodes.PATH_MY, "folder" + RUNID);
String folderId = folderResp.getId();
// create doc d1
@@ -3617,29 +3619,25 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest
assertEquals(d1Name, documentResp.getName());
assertEquals(d1Id, documentResp.getId());
assertEquals("READ_ONLY_LOCK", documentResp.getProperties().get("cm:lockType"));
assertEquals(LockType.READ_ONLY_LOCK.toString(), documentResp.getProperties().get("cm:lockType"));
assertNotNull(documentResp.getProperties().get("cm:lockOwner"));
assertNull(documentResp.getIsLocked());
// Empty lock body, the default values are used
post("nodes/"+folderId+"/lock", "{}", null, 200);
post("nodes/"+folderId+"/lock", EMPTY_BODY, null, 200);
// Test delete on a folder which contains a locked node - NodeLockedException
deleteNode(folderId, true, HttpStatus.SC_CONFLICT);
// Test lock children
// create folder
Folder folderA = createFolder(Nodes.PATH_MY, "folderA");
String folderAName = "folder" + RUNID + "_A";
Folder folderA = createFolder(Nodes.PATH_MY, folderAName);
String folderAId = folderA.getId();
// create 2 children files
String dA1Name = "content" + RUNID + "_A1";
Document dA1 = createTextFile(folderAId, dA1Name, "A1 content");
String dA1Id = dA1.getId();
String dA2Name = "content" + RUNID + "_A2";
Document dA2 = createTextFile(folderId, dA2Name, "A2 content");
String dA2Id = dA2.getId();
// create 2 files in the folderA
createTextFile(folderAId, "content" + RUNID + "_A1", "A1 content");
createTextFile(folderAId, "content" + RUNID + "_A2", "A2 content");
params = Collections.singletonMap("include", "isLocked");
response = getSingle(URL_NODES, folderAId, params, null, 200);
@@ -3668,7 +3666,7 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest
response = post(URL_NODES, folderAId, "lock", toJsonAsStringNonNull(body).getBytes(), null, null, 200);
documentResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class);
assertEquals("folderA", documentResp.getName());
assertEquals(folderAName, documentResp.getName());
assertEquals(folderAId, documentResp.getId());
assertNotNull(documentResp.getProperties().get("cm:lockType"));
assertNotNull(documentResp.getProperties().get("cm:lockOwner"));
@@ -3685,7 +3683,7 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest
assertTrue(child.getIsLocked());
}
Folder folderB = createFolder(Nodes.PATH_MY, "folderB");
Folder folderB = createFolder(Nodes.PATH_MY, "folder" + RUNID + "_B");
String folderBId = folderB.getId();
body = new HashMap<>();
@@ -3708,35 +3706,160 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest
String ddNodeId = nodeResp.getId();
setRequestContext(networkAdmin);
post("nodes/"+ddNodeId+"/lock", toJsonAsStringNonNull(body), null, 403);
post("nodes/" + ddNodeId + "/lock", toJsonAsStringNonNull(body), null, 403);
// Lock node already locked by another user - UnableToAquireLockException
post("nodes/"+folderId+"/lock", "{}", null, 422);
post("nodes/" + folderId + "/lock", EMPTY_BODY, null, 422);
// Invalid lock body values
setRequestContext(user1);
Folder folderC = createFolder(Nodes.PATH_MY, "folderC");
String folderCId = folderB.getId();
Folder folderC = createFolder(Nodes.PATH_MY, "folder" + RUNID + "_C");
String folderCId = folderC.getId();
body = new HashMap<>();
body.put("includeChildren", "true123");
post("nodes/"+folderBId+"/lock", toJsonAsStringNonNull(body), null, 400);
post("nodes/" + folderCId + "/lock", toJsonAsStringNonNull(body), null, 400);
body = new HashMap<>();
body.put("type", "FULL123");
post("nodes/"+folderBId+"/lock", toJsonAsStringNonNull(body), null, 400);
post("nodes/" + folderCId + "/lock", toJsonAsStringNonNull(body), null, 400);
body = new HashMap<>();
body.put("lifetime", "PERSISTENT123");
post("nodes/"+folderBId+"/lock", toJsonAsStringNonNull(body), null, 400);
post("nodes/" + folderCId + "/lock", toJsonAsStringNonNull(body), null, 400);
body = new HashMap<>();
body.put("timeToExpire", "NaN");
post("nodes/"+folderBId+"/lock", toJsonAsStringNonNull(body), null, 400);
post("nodes/" + folderCId + "/lock", toJsonAsStringNonNull(body), null, 400);
body = new HashMap<>();
body.put("invalid_property", "true");
post("nodes/"+folderBId+"/lock", toJsonAsStringNonNull(body), null, 400);
post("nodes/" + folderCId + "/lock", toJsonAsStringNonNull(body), null, 400);
//cleanup
setRequestContext(user1); // all locks were made by user1
unlock(folderId, toJsonAsStringNonNull(Collections.singletonMap("includeChildren", "true")));
deleteNode(folderId);
unlock(folderAId, toJsonAsStringNonNull(Collections.singletonMap("includeChildren", "true")));
deleteNode(folderAId);
unlock(folderBId, EMPTY_BODY);
deleteNode(folderBId);
deleteNode(folderCId);
}
/**
* Tests unlock of a node
* <p>POST:</p>
* {@literal <host>:<port>/alfresco/api/-default-/public/alfresco/versions/1/nodes/<nodeId>/unlock}
*/
@Test
public void testUnlock() throws Exception
{
setRequestContext(user1);
// create folder
Folder folderResp = createFolder(Nodes.PATH_MY, "folder" + RUNID);
String folderId = folderResp.getId();
// create doc d1
String d1Name = "content" + RUNID + "_1l";
Document d1 = createTextFile(folderId, d1Name, "The quick brown fox jumps over the lazy dog 1.");
String d1Id = d1.getId();
lock(d1Id, EMPTY_BODY);
Map<String, String> body = new HashMap<>();
body.put("includeChildren", "true");
body.put("allowCheckedOut", "true");
HttpResponse response = post(URL_NODES, d1Id, "unlock", toJsonAsStringNonNull(body).getBytes(), null, null, 200);
Document documentResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class);
assertEquals(d1Name, documentResp.getName());
assertEquals(d1Id, documentResp.getId());
assertNull(documentResp.getProperties().get("cm:lockType"));
assertNull(documentResp.getProperties().get("cm:lockOwner"));
lock(d1Id, EMPTY_BODY);
// Users with admin rights can unlock nodes locked by other users.
setRequestContext(networkAdmin);
post("nodes/" + d1Id + "/unlock", EMPTY_BODY, null, 200);
setRequestContext(user1);
//Unlock on a not locked node should do nothing
post("nodes/" + d1Id + "/unlock", EMPTY_BODY, null, 200);
post("nodes/" + folderId + "/unlock", EMPTY_BODY, null, 200);
// Test unlock children
// create folder
Folder folderA = createFolder(Nodes.PATH_MY, "folder" + RUNID + "_A");
String folderAId = folderA.getId();
// create 2 files in the folderA
String dA1Name = "content" + RUNID + "_A1";
Document dA1 = createTextFile(folderAId, dA1Name, "A1 content");
String dA1Id = dA1.getId();
String dA2Name = "content" + RUNID + "_A2";
Document dA2 = createTextFile(folderAId, dA2Name, "A2 content");
String dA2Id = dA2.getId();
// lock the folder and children
body = new HashMap<>();
body.put("includeChildren", "true");
lock(folderAId, toJsonAsStringNonNull(body));
body.put("includeChildren", "true");
body.put("allowCheckedOut", "true");
post(URL_NODES, folderAId, "unlock", toJsonAsStringNonNull(body).getBytes(), null, null, 200);
Map<String, String> params = Collections.singletonMap("include", "aspectNames,properties,isLocked");
response = getAll(getNodeChildrenUrl(folderAId), null, params, 200);
List<Node> nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class);
// Test if children nodes are unlocked as well.
for (Node child : nodes)
{
assertNull(child.getProperties().get("cm:lockType"));
assertNull(child.getProperties().get("cm:lockOwner"));
assertFalse(child.getIsLocked());
}
// -ve
// Missing target node
post("nodes/" + "fakeId" + "/unlock", EMPTY_BODY, null, 404);
// Unlock by a user without permission
lock(d1Id, EMPTY_BODY);
setRequestContext(user2);
post("nodes/" + d1Id + "/unlock", EMPTY_BODY, null, 403);
// Invalid lock body values
setRequestContext(user1);
Folder folderC = createFolder(Nodes.PATH_MY, "folder" + RUNID + "_C");
String folderCId = folderC.getId();
lock(folderCId, EMPTY_BODY);
body = new HashMap<>();
body.put("includeChildren", "true123");
post("nodes/" + folderCId + "/unlock", toJsonAsStringNonNull(body), null, 400);
body = new HashMap<>();
body.put("allowCheckedOut", "false123");
post("nodes/" + folderCId + "/unlock", toJsonAsStringNonNull(body), null, 400);
body = new HashMap<>();
body.put("invalid_property", "true");
post("nodes/" + folderCId + "/unlock", toJsonAsStringNonNull(body), null, 400);
// clean up
setRequestContext(user1); // all locks were made by user1
unlock(folderId, toJsonAsStringNonNull(Collections.singletonMap("includeChildren", "true")));
deleteNode(folderId);
unlock(folderAId, toJsonAsStringNonNull(Collections.singletonMap("includeChildren", "true")));
deleteNode(folderAId);
unlock(folderCId, EMPTY_BODY);
deleteNode(folderCId);
}
@Override