From bd54639d22d665feeff373adab104a93744fd6b1 Mon Sep 17 00:00:00 2001 From: Alan Davis Date: Tue, 6 Sep 2016 14:41:44 +0000 Subject: [PATCH] Merged 5.2.N (5.2.1) to HEAD (5.2) 129716 amorarasu: REPO-340 / REPO-1136 - V1 REST API: Unlock Node git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@130220 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/public-rest-context.xml | 1 + source/java/org/alfresco/rest/api/Nodes.java | 10 + .../org/alfresco/rest/api/impl/NodesImpl.java | 35 ++++ .../alfresco/rest/api/model/UnlockInfo.java | 60 ++++++ .../rest/api/nodes/NodesEntityResource.java | 10 + .../rest/api/tests/AbstractBaseApiTest.java | 12 ++ .../alfresco/rest/api/tests/NodeApiTest.java | 171 +++++++++++++++--- 7 files changed, 275 insertions(+), 24 deletions(-) create mode 100644 source/java/org/alfresco/rest/api/model/UnlockInfo.java diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml index 12835be9ce..1d24675c4a 100644 --- a/config/alfresco/public-rest-context.xml +++ b/config/alfresco/public-rest-context.xml @@ -154,6 +154,7 @@ + diff --git a/source/java/org/alfresco/rest/api/Nodes.java b/source/java/org/alfresco/rest/api/Nodes.java index 34f24a26ca..280ed7b8a4 100644 --- a/source/java/org/alfresco/rest/api/Nodes.java +++ b/source/java/org/alfresco/rest/api/Nodes.java @@ -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; @@ -254,6 +255,15 @@ public interface Nodes * @return */ 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 diff --git a/source/java/org/alfresco/rest/api/impl/NodesImpl.java b/source/java/org/alfresco/rest/api/impl/NodesImpl.java index 0a5cbed504..82ad6f70e3 100644 --- a/source/java/org/alfresco/rest/api/impl/NodesImpl.java +++ b/source/java/org/alfresco/rest/api/impl/NodesImpl.java @@ -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 */ diff --git a/source/java/org/alfresco/rest/api/model/UnlockInfo.java b/source/java/org/alfresco/rest/api/model/UnlockInfo.java new file mode 100644 index 0000000000..74b7b36a7f --- /dev/null +++ b/source/java/org/alfresco/rest/api/model/UnlockInfo.java @@ -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 . + * #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; + } +} diff --git a/source/java/org/alfresco/rest/api/nodes/NodesEntityResource.java b/source/java/org/alfresco/rest/api/nodes/NodesEntityResource.java index b9e1ac2e11..322eee6ca2 100644 --- a/source/java/org/alfresco/rest/api/nodes/NodesEntityResource.java +++ b/source/java/org/alfresco/rest/api/nodes/NodesEntityResource.java @@ -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; @@ -179,6 +180,15 @@ 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); + } } diff --git a/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java b/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java index bb1cb2e4d1..dac8ae4096 100644 --- a/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java @@ -787,6 +787,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; diff --git a/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java b/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java index 35eac7e231..536d654abb 100644 --- a/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java @@ -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,36 +3706,161 @@ 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); + body.put("lifetime", "PERSISTENT123"); + 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 + *

POST:

+ * {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes//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 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 params = Collections.singletonMap("include", "aspectNames,properties,isLocked"); + response = getAll(getNodeChildrenUrl(folderAId), null, params, 200); + List 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 public String getScope()