diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml index 36e745d237..12835be9ce 100644 --- a/config/alfresco/public-rest-context.xml +++ b/config/alfresco/public-rest-context.xml @@ -153,6 +153,7 @@ + diff --git a/source/java/org/alfresco/rest/api/Nodes.java b/source/java/org/alfresco/rest/api/Nodes.java index 328226f553..62c8b19185 100644 --- a/source/java/org/alfresco/rest/api/Nodes.java +++ b/source/java/org/alfresco/rest/api/Nodes.java @@ -34,6 +34,7 @@ import org.alfresco.rest.api.model.AssocChild; import org.alfresco.rest.api.model.AssocTarget; 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.UserInfo; import org.alfresco.rest.framework.resource.content.BasicContentInfo; @@ -244,6 +245,15 @@ public interface Nodes * @return */ List addTargets(String sourceNodeId, List entities); + + /** + * Lock a node + * @param nodeId + * @param lockInfo + * @param parameters + * @return + */ + Node lock(String nodeId, LockInfo lockInfo, 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 eded636f15..ebfef14fc4 100644 --- a/source/java/org/alfresco/rest/api/impl/NodesImpl.java +++ b/source/java/org/alfresco/rest/api/impl/NodesImpl.java @@ -25,6 +25,28 @@ */ package org.alfresco.rest.api.impl; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; + import org.alfresco.model.ApplicationModel; import org.alfresco.model.ContentModel; import org.alfresco.model.QuickShareModel; @@ -34,6 +56,7 @@ import org.alfresco.repo.action.executer.ContentMetadataExtracter; import org.alfresco.repo.activities.ActivityType; import org.alfresco.repo.content.ContentLimitViolationException; import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.lock.mem.Lifetime; import org.alfresco.repo.model.Repository; import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; import org.alfresco.repo.node.getchildren.FilterProp; @@ -59,6 +82,7 @@ import org.alfresco.rest.api.model.AssocChild; import org.alfresco.rest.api.model.AssocTarget; 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.PathInfo; import org.alfresco.rest.api.model.PathInfo.ElementInfo; @@ -97,6 +121,7 @@ import org.alfresco.service.cmr.dictionary.AspectDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.lock.LockService; import org.alfresco.service.cmr.model.FileExistsException; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; @@ -136,28 +161,6 @@ import org.springframework.dao.ConcurrencyFailureException; import org.springframework.extensions.surf.util.Content; import org.springframework.extensions.webscripts.servlet.FormData; -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; -import java.math.BigInteger; -import java.nio.charset.Charset; -import java.util.AbstractList; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.concurrent.ConcurrentHashMap; - /** * Centralises access to file/folder/node services and maps between representations. * @@ -184,8 +187,6 @@ public class NodesImpl implements Nodes DOCUMENT, FOLDER } - private static final String DEFAULT_MIMETYPE = MimetypeMap.MIMETYPE_BINARY; - private NodeService nodeService; private DictionaryService dictionaryService; private FileFolderService fileFolderService; @@ -203,6 +204,7 @@ public class NodesImpl implements Nodes private ActivityPoster poster; private RetryingTransactionHelper retryingTransactionHelper; private NodeAssocService nodeAssocService; + private LockService lockService; private enum Activity_Type { @@ -253,6 +255,7 @@ public class NodesImpl implements Nodes this.thumbnailService = sr.getThumbnailService(); this.siteService = sr.getSiteService(); this.retryingTransactionHelper = sr.getRetryingTransactionHelper(); + this.lockService = sr.getLockService(); if (defaultIgnoreTypesAndAspects != null) { @@ -908,7 +911,7 @@ public class NodesImpl implements Nodes // special case: do not return "create" (as an allowable op) for file/content types - note: 'type' can be null continue; } - else if (perm.equals(PermissionService.DELETE) && (isSpecialNodeDoNotDelete(nodeRef, nodeTypeQName))) + else if (perm.equals(PermissionService.DELETE) && (isSpecialNode(nodeRef, nodeTypeQName))) { // special case: do not return "delete" (as an allowable op) for specific system nodes continue; @@ -1503,7 +1506,7 @@ public class NodesImpl implements Nodes { NodeRef nodeRef = validateOrLookupNode(nodeId, null); - if (isSpecialNodeDoNotDelete(nodeRef, getNodeType(nodeRef))) + if (isSpecialNode(nodeRef, getNodeType(nodeRef))) { throw new PermissionDeniedException("Cannot delete: " + nodeId); } @@ -1927,9 +1930,9 @@ public class NodesImpl implements Nodes } } - // special case: additional delete validation (pending common lower-level service support) - // for blacklist of system nodes that should not be deleted, eg. Company Home, Sites, Data Dictionary - private boolean isSpecialNodeDoNotDelete(NodeRef nodeRef, QName type) + // special case: additional node validation (pending common lower-level service support) + // for blacklist of system nodes that should not be deleted or locked, eg. Company Home, Sites, Data Dictionary + private boolean isSpecialNode(NodeRef nodeRef, QName type) { // Check for Company Home, Sites and Data Dictionary (note: must be tenant-aware) @@ -2180,7 +2183,7 @@ public class NodesImpl implements Nodes else { // move - if ((! nodeRef.equals(parentNodeRef)) && isSpecialNodeDoNotDelete(nodeRef, getNodeType(nodeRef))) + if ((! nodeRef.equals(parentNodeRef)) && isSpecialNode(nodeRef, getNodeType(nodeRef))) { throw new PermissionDeniedException("Cannot move: "+nodeRef.getId()); } @@ -2904,6 +2907,44 @@ public class NodesImpl implements Nodes return result; } + @Override + public Node lock(String nodeId, LockInfo lockInfo, Parameters parameters) + { + NodeRef nodeRef = validateOrLookupNode(nodeId, null); + + if (isSpecialNode(nodeRef, getNodeType(nodeRef))) + { + throw new PermissionDeniedException("Current user doesn't have permission to lock node " + nodeId); + } + + lockInfo = validateLockInformation(lockInfo); + lockService.lock(nodeRef, lockInfo.getType(), lockInfo.getTimeToExpire(), lockInfo.getLifetime(), lockInfo.getIncludeChildren()); + + return getFolderOrDocument(nodeId, parameters); + } + + private LockInfo validateLockInformation(LockInfo lockInfo) + { + // Set default values for the lock details. + if (lockInfo.getType() == null) + { + lockInfo.setType(LockInfo.LockType2.ALLOW_OWNER_CHANGES.name()); + } + if (lockInfo.getLifetime() == null) + { + lockInfo.setLifetime(Lifetime.PERSISTENT.name()); + } + if (lockInfo.getIncludeChildren() == null) + { + lockInfo.setIncludeChildren(false); + } + if (lockInfo.getTimeToExpire() == null) + { + lockInfo.setTimeToExpire(0); + } + return lockInfo; + } + /** * @author Jamal Kaabi-Mofrad */ diff --git a/source/java/org/alfresco/rest/api/model/LockInfo.java b/source/java/org/alfresco/rest/api/model/LockInfo.java new file mode 100644 index 0000000000..db14ebf911 --- /dev/null +++ b/source/java/org/alfresco/rest/api/model/LockInfo.java @@ -0,0 +1,120 @@ +/* + * #%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; + +import org.alfresco.repo.lock.mem.Lifetime; +import org.alfresco.service.cmr.lock.LockType; + +/** + * Representation of a lock info + * + * @author Ancuta Morarasu + */ +public class LockInfo +{ + private Integer timeToExpire; + private Boolean includeChildren; + private LockType2 type; + private Lifetime lifetime; + + /** + * Lock Type enum that maps to the current values in {@link org.alfresco.service.cmr.lock.LockType}. + * These values describe better the meanings of the lock types. + */ + @SuppressWarnings("deprecation") + public static enum LockType2 + { + FULL(LockType.READ_ONLY_LOCK), + ALLOW_ADD_CHILDREN(LockType.NODE_LOCK), + ALLOW_OWNER_CHANGES(LockType.WRITE_LOCK); + + private LockType type; + + private LockType2(LockType type) + { + this.type = type; + } + public LockType getType() + { + return type; + } + } + + public LockInfo() {} + + public void setTimeToExpire(Integer timeToExpire) + { + this.timeToExpire = timeToExpire; + } + + public Integer getTimeToExpire() + { + return timeToExpire; + } + + public void setIncludeChildren(Boolean includeChildren) + { + this.includeChildren = includeChildren; + } + + public Boolean getIncludeChildren() + { + return includeChildren; + } + + public LockType getType() + { + return type != null ? type.getType() : null; + } + + public void setType(String type) + { + this.type = LockType2.valueOf(type); + } + + public Lifetime getLifetime() + { + return lifetime; + } + + public void setLifetime(String lifetimeStr) + { + this.lifetime = Lifetime.valueOf(lifetimeStr); + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder("LockInfo{"); + sb.append("includeChildren='").append(includeChildren).append('\''); + sb.append(", timeToExpire=").append(timeToExpire).append('\''); + sb.append(", type=").append(type).append('\''); + sb.append(", lifetime=").append(lifetime).append('\''); + sb.append('}'); + return sb.toString(); + } + +} diff --git a/source/java/org/alfresco/rest/api/nodes/NodesEntityResource.java b/source/java/org/alfresco/rest/api/nodes/NodesEntityResource.java index a5ad376a0f..b9e1ac2e11 100644 --- a/source/java/org/alfresco/rest/api/nodes/NodesEntityResource.java +++ b/source/java/org/alfresco/rest/api/nodes/NodesEntityResource.java @@ -25,11 +25,16 @@ */ package org.alfresco.rest.api.nodes; +import java.io.InputStream; + +import javax.servlet.http.HttpServletResponse; + 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.framework.BinaryProperties; import org.alfresco.rest.framework.Operation; -import org.alfresco.rest.framework.BinaryProperties; import org.alfresco.rest.framework.WebApiDescription; import org.alfresco.rest.framework.WebApiParam; import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; @@ -41,12 +46,8 @@ import org.alfresco.rest.framework.resource.content.BinaryResource; import org.alfresco.rest.framework.resource.parameters.Parameters; import org.alfresco.rest.framework.webscripts.WithResponse; import org.alfresco.util.ParameterCheck; -import org.apache.lucene.store.Lock; import org.springframework.beans.factory.InitializingBean; -import javax.servlet.http.HttpServletResponse; -import java.io.InputStream; - /** * An implementation of an Entity Resource for a Node (file or folder) * @@ -169,6 +170,15 @@ public class NodesEntityResource implements { return nodes.moveOrCopyNode(nodeId, target.getTargetParentId(), target.getName(), parameters, false); } + + @Operation("lock") + @WebApiDescription(title = "Lock Node", + description="Places a lock on a node.", + successStatus = HttpServletResponse.SC_OK) + public Node lock(String nodeId, LockInfo lockInfo, Parameters parameters, WithResponse withResponse) + { + return nodes.lock(nodeId, lockInfo, parameters); + } } 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 79ea7be7c4..2112ef5c25 100644 --- a/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java @@ -35,6 +35,21 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.UUID; + import org.alfresco.repo.content.ContentLimitProvider.SimpleFixedLimitProvider; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.security.authentication.AuthenticationUtil; @@ -57,7 +72,6 @@ import org.alfresco.rest.api.tests.client.data.Folder; import org.alfresco.rest.api.tests.client.data.Node; import org.alfresco.rest.api.tests.client.data.PathInfo; import org.alfresco.rest.api.tests.client.data.PathInfo.ElementInfo; -import org.alfresco.rest.api.tests.client.data.SiteMember; import org.alfresco.rest.api.tests.client.data.SiteRole; import org.alfresco.rest.api.tests.client.data.UserInfo; import org.alfresco.rest.api.tests.util.MultiPartBuilder; @@ -70,26 +84,12 @@ import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.site.SiteVisibility; import org.alfresco.util.GUID; import org.alfresco.util.TempFileProvider; +import org.apache.http.HttpStatus; import org.json.simple.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.Test; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.UUID; - /** * V1 REST API tests for Nodes (files, folders and custom node types) * @@ -3573,6 +3573,138 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest // some cleanup deleteNode(folderId, true, 204); } + + /** + * Tests lock of a node + *

POST:

+ * {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes//lock} + */ + @Test + public void testLock() throws Exception + { + setRequestContext(user1); + + // create folder + Folder folderResp = createFolder(Nodes.PATH_MY, "folderT"); + 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(); + + Map body = new HashMap<>(); + body.put("includeChildren", "true"); + body.put("timeToExpire", "60"); + body.put("type", "FULL"); + body.put("lifetime", "PERSISTENT"); + + HttpResponse response = post(URL_NODES, d1Id, "lock", toJsonAsStringNonNull(body).getBytes(), null, null, 200); + Document documentResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class); + + assertEquals(d1Name, documentResp.getName()); + assertEquals(d1Id, documentResp.getId()); + assertEquals("READ_ONLY_LOCK", documentResp.getProperties().get("cm:lockType")); + assertNotNull(documentResp.getProperties().get("cm:lockOwner")); + + // Empty lock body, the default values are used + post("nodes/"+folderId+"/lock", "{}", 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 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(); + + body = new HashMap<>(); + body.put("includeChildren", "true"); + body.put("timeToExpire", "60"); + body.put("type", "FULL"); + body.put("lifetime", "EPHEMERAL"); + + // lock the folder + response = post(URL_NODES, folderAId, "lock", toJsonAsStringNonNull(body).getBytes(), null, null, 200); + documentResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class); + + assertEquals("folderA", documentResp.getName()); + assertEquals(folderAId, documentResp.getId()); + assertNotNull(documentResp.getProperties().get("cm:lockType")); + assertNotNull(documentResp.getProperties().get("cm:lockOwner")); + + Map params = Collections.singletonMap("include", "aspectNames,properties"); + response = getAll(getNodeChildrenUrl(folderAId), null, params, 200); + List nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + // Test if children nodes are locked as well. + for (Node child : nodes) + { + assertNotNull(child.getProperties().get("cm:lockType")); + assertNotNull(child.getProperties().get("cm:lockOwner")); + } + + Folder folderB = createFolder(Nodes.PATH_MY, "folderB"); + String folderBId = folderB.getId(); + + body = new HashMap<>(); + body.put("timeToExpire", "-100"); // values lower than 0 are considered as no expiry time + post("nodes/" + folderBId + "/lock", toJsonAsStringNonNull(body), null, 200); + + // -ve tests + + // Missing target node + body = new HashMap<>(); + body.put("timeToExpire", "60"); + + post("nodes/" + "fakeId" + "/lock", toJsonAsStringNonNull(body), null, 404); + + // Cannot lock Data Dictionary node + params = new HashMap<>(); + params.put(Nodes.PARAM_RELATIVE_PATH, "/Data Dictionary"); + response = getSingle(NodesEntityResource.class, getRootNodeId(), params, 200); + Node nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + String ddNodeId = nodeResp.getId(); + + setRequestContext(networkAdmin); + post("nodes/"+ddNodeId+"/lock", toJsonAsStringNonNull(body), null, 403); + + // Lock node already locked by another user - UnableToAquireLockException + post("nodes/"+folderId+"/lock", "{}", null, 422); + + // Invalid lock body values + setRequestContext(user1); + + Folder folderC = createFolder(Nodes.PATH_MY, "folderC"); + String folderCId = folderB.getId(); + body = new HashMap<>(); + body.put("includeChildren", "true123"); + post("nodes/"+folderBId+"/lock", toJsonAsStringNonNull(body), null, 400); + + body = new HashMap<>(); + body.put("type", "FULL123"); + post("nodes/"+folderBId+"/lock", toJsonAsStringNonNull(body), null, 400); + + body = new HashMap<>(); + body.put("lifetime", "PERSISTENT123"); + post("nodes/"+folderBId+"/lock", toJsonAsStringNonNull(body), null, 400); + + body = new HashMap<>(); + body.put("timeToExpire", "NaN"); + post("nodes/"+folderBId+"/lock", toJsonAsStringNonNull(body), null, 400); + + body = new HashMap<>(); + body.put("invalid_property", "true"); + post("nodes/"+folderBId+"/lock", toJsonAsStringNonNull(body), null, 400); + } @Override public String getScope()