diff --git a/CHANGELOG.md b/CHANGELOG.md index 284bc83e..4274f315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Alfred API - Changelog +## 3.0.2 (2021-11-25) + +### Added +* [ALFREDAPI-492](https://xenitsupport.jira.com/browse/ALFREDAPI-492): /v1/nodes POST enpoint now accepts aspects to add/remove +* [ALFREDAPI-497](https://xenitsupport.jira.com/browse/ALFREDAPI-497): Re-enable `composeDown` after `integrationTest` in build + + + ## 3.0.1 (2021-09-29) ### Changed diff --git a/apix-impl/src/main/java/eu/xenit/apix/alfresco/metadata/NodeService.java b/apix-impl/src/main/java/eu/xenit/apix/alfresco/metadata/NodeService.java index da1c3ed9..74928bf0 100644 --- a/apix-impl/src/main/java/eu/xenit/apix/alfresco/metadata/NodeService.java +++ b/apix-impl/src/main/java/eu/xenit/apix/alfresco/metadata/NodeService.java @@ -584,6 +584,14 @@ public eu.xenit.apix.data.NodeRef createNode(eu.xenit.apix.data.NodeRef parent, public eu.xenit.apix.data.NodeRef createNode(eu.xenit.apix.data.NodeRef parent, Map properties, eu.xenit.apix.data.QName type, eu.xenit.apix.data.ContentData contentData) { + return createNode(parent, properties, null, null, type, contentData); + } + + @Override + public eu.xenit.apix.data.NodeRef createNode(eu.xenit.apix.data.NodeRef parent, + Map properties, eu.xenit.apix.data.QName[] aspectsToAdd, + eu.xenit.apix.data.QName[] aspectsToRemove, eu.xenit.apix.data.QName type, + eu.xenit.apix.data.ContentData contentData) { String[] names = properties.get(c.apix(ContentModel.PROP_NAME)); if (names == null || names.length == 0) { throw new InvalidArgumentException( @@ -599,6 +607,11 @@ public eu.xenit.apix.data.NodeRef createNode(eu.xenit.apix.data.NodeRef parent, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(name)), c.alfresco(type), toAlfrescoPropertyMap(properties)); + + MetadataChanges aspects = new MetadataChanges(); + aspects.setAspectsToAdd(aspectsToAdd); + aspects.setAspectsToRemove(aspectsToRemove); + setMetadata(c.apix(result.getChildRef()), aspects); if (contentData != null) { this.nodeService.setProperty(result.getChildRef(), ContentModel.PROP_CONTENT, c.alfresco(contentData)); diff --git a/apix-integrationtests/build.gradle b/apix-integrationtests/build.gradle index e05fb3ab..169752d4 100644 --- a/apix-integrationtests/build.gradle +++ b/apix-integrationtests/build.gradle @@ -105,5 +105,10 @@ subprojects { compileOnly project(':apix-integrationtests') } + integrationTest { + // After the tests, the docker setup should be stopped + finalizedBy composeDownTask + } + } diff --git a/apix-integrationtests/src/main/java/eu/xenit/apix/rest/v1/tests/CopyNodeTest.java b/apix-integrationtests/src/main/java/eu/xenit/apix/rest/v1/tests/CopyNodeTest.java index 4a4d3fb0..3a7c059b 100644 --- a/apix-integrationtests/src/main/java/eu/xenit/apix/rest/v1/tests/CopyNodeTest.java +++ b/apix-integrationtests/src/main/java/eu/xenit/apix/rest/v1/tests/CopyNodeTest.java @@ -4,8 +4,8 @@ import eu.xenit.apix.data.NodeRef; import eu.xenit.apix.data.QName; import eu.xenit.apix.node.INodeService; -import java.util.HashMap; import eu.xenit.apix.rest.v1.nodes.CreateNodeOptions; +import java.util.HashMap; import org.alfresco.model.ContentModel; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.service.transaction.TransactionService; @@ -57,6 +57,26 @@ public void testCopyFileNode() { checkCreatedNode(newRef, createNodeOptions); } + @Test + public void testCopyFileNodeWithAspectsToRemove() { + transactionService.getRetryingTransactionHelper() + .doInTransaction(() -> { + serviceRegistry.getNodeService().addAspect(c.alfresco(copyFromFile), ContentModel.ASPECT_TEMPORARY, new HashMap<>()); + return null; + }, false, true); + + QName[] aspectsToRemove = new QName[1]; + aspectsToRemove[0] = c.apix(ContentModel.ASPECT_TEMPORARY); + CreateNodeOptions createNodeOptions = getCreateNodeOptions(mainTestFolder, null, + null, null, null, aspectsToRemove, copyFromFile); + + NodeRef newRef = transactionService.getRetryingTransactionHelper() + .doInTransaction(() -> { + return doPostNodes(createNodeOptions, HttpStatus.SC_OK, null,null); + }, false, true); + checkCreatedNode(newRef, createNodeOptions); + } + @Test public void testCopyFolderNode() { CreateNodeOptions createNodeOptions = getCreateNodeOptions(mainTestFolder, null, diff --git a/apix-integrationtests/src/main/java/eu/xenit/apix/rest/v1/tests/NodesBaseTest.java b/apix-integrationtests/src/main/java/eu/xenit/apix/rest/v1/tests/NodesBaseTest.java index df444cb3..180b4e98 100644 --- a/apix-integrationtests/src/main/java/eu/xenit/apix/rest/v1/tests/NodesBaseTest.java +++ b/apix-integrationtests/src/main/java/eu/xenit/apix/rest/v1/tests/NodesBaseTest.java @@ -1,5 +1,11 @@ package eu.xenit.apix.rest.v1.tests; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -9,6 +15,9 @@ import eu.xenit.apix.data.QName; import eu.xenit.apix.rest.v1.nodes.CreateNodeOptions; import eu.xenit.apix.rest.v1.nodes.NodeInfo; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import org.alfresco.model.ContentModel; import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; @@ -19,11 +28,6 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.springframework.beans.factory.annotation.Autowired; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; public abstract class NodesBaseTest extends RestV1BaseTest { @@ -81,21 +85,29 @@ public eu.xenit.apix.data.NodeRef doPostNodes(CreateNodeOptions createNodeOption } } - protected CreateNodeOptions getCreateNodeOptions(eu.xenit.apix.data.NodeRef parentRef, String name, eu.xenit.apix.data.QName type, HashMap properties, eu.xenit.apix.data.NodeRef copyFrom) { + protected CreateNodeOptions getCreateNodeOptions(eu.xenit.apix.data.NodeRef parentRef, String name, + eu.xenit.apix.data.QName type, HashMap properties, QName[] aspectsToAdd, + QName[] aspectsToRemove, eu.xenit.apix.data.NodeRef copyFrom) { String parentRefString = (parentRef != null) ? parentRef.toString() : null; String copyFromString = (copyFrom != null) ? copyFrom.toString() : null; String typeString = (type != null) ? type.toString() : null; try { - return new CreateNodeOptions(parentRefString, name, typeString, properties, copyFromString); + return new CreateNodeOptions(parentRefString, name, typeString, properties, aspectsToAdd, aspectsToRemove, copyFromString); } catch (IOException e) { e.printStackTrace(); } return null; } + protected CreateNodeOptions getCreateNodeOptions(eu.xenit.apix.data.NodeRef parentRef, + String name, eu.xenit.apix.data.QName type, HashMap properties, + eu.xenit.apix.data.NodeRef copyFrom) { + return getCreateNodeOptions(parentRef, name, type, properties, null, null, copyFrom); + } + public void checkCreatedNode(NodeRef newRef, CreateNodeOptions createNodeOptions) { - assertEquals(true, nodeService.exists(newRef)); + assertTrue(nodeService.exists(newRef)); assertEquals(createNodeOptions.parent, nodeService.getParentAssociations(newRef).get(0).getTarget().toString()); if (createNodeOptions.type != null) { @@ -103,7 +115,7 @@ public void checkCreatedNode(NodeRef newRef, CreateNodeOptions createNodeOptions } if (createNodeOptions.copyFrom != null) { - assertEquals(true, nodeService.exists(new NodeRef(createNodeOptions.copyFrom))); + assertTrue(nodeService.exists(new NodeRef(createNodeOptions.copyFrom))); } if (createNodeOptions.properties != null) { @@ -111,6 +123,26 @@ public void checkCreatedNode(NodeRef newRef, CreateNodeOptions createNodeOptions assertArrayEquals(property.getValue(), nodeService.getMetadata(newRef).properties.get(property.getKey()).toArray()); } } + + if (createNodeOptions.aspectsToAdd != null) { + for (QName aspect : createNodeOptions.aspectsToAdd) { + assertNotNull(nodeService.getMetadata(newRef).aspects + .stream() + .filter(testAspect -> testAspect.equals(aspect)) + .findFirst() + .orElse(null)); + } + } + + if (createNodeOptions.aspectsToRemove != null) { + for (QName aspect : createNodeOptions.aspectsToRemove) { + assertNull(nodeService.getMetadata(newRef).aspects + .stream() + .filter(testAspect -> testAspect.equals(aspect)) + .findFirst() + .orElse(null)); + } + } } } diff --git a/apix-integrationtests/src/main/java/eu/xenit/apix/tests/metadata/NodeServiceTest.java b/apix-integrationtests/src/main/java/eu/xenit/apix/tests/metadata/NodeServiceTest.java index bbe2eef7..76f3f491 100644 --- a/apix-integrationtests/src/main/java/eu/xenit/apix/tests/metadata/NodeServiceTest.java +++ b/apix-integrationtests/src/main/java/eu/xenit/apix/tests/metadata/NodeServiceTest.java @@ -21,7 +21,9 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.Serializable; import java.io.StringWriter; +import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -522,7 +524,7 @@ public void testCreateNodeCustomTypeWithRequiredProperties() { try { eu.xenit.apix.data.NodeRef createdNodeRef = this.service - .createNode(c.apix(testFolder.getNodeRef()), propertiesToSet, type, null); + .createNode(c.apix(testFolder.getNodeRef()), propertiesToSet, null, null, type, null); assertNotNull(createdNodeRef); assertEquals( this.alfrescoNodeService.getPrimaryParent(c.alfresco(createdNodeRef)).getParentRef().toString(), @@ -560,7 +562,7 @@ public Void execute() throws Throwable { FileInfo mainTestFolder = self.createMainTestFolder(companyHomeRef); FileInfo testFolder = self.createTestFolder(mainTestFolder.getNodeRef(), "testfolder2"); - self.service.createNode(c.apix(testFolder.getNodeRef()), propertiesToSet, type, null); + self.service.createNode(c.apix(testFolder.getNodeRef()), propertiesToSet, null, null, type, null); return null; } }, false, true); @@ -598,7 +600,7 @@ public void testCreateReadContent() throws Exception { Map propertiesToSet = new HashMap<>(); propertiesToSet.put(c.apix(ContentModel.PROP_NAME), new String[]{"nodeWithContent"}); eu.xenit.apix.data.NodeRef createdNodeRef = this.service - .createNode(c.apix(testFolder.getNodeRef()), propertiesToSet, c.apix(ContentModel.TYPE_CONTENT), + .createNode(c.apix(testFolder.getNodeRef()), propertiesToSet, null, null, c.apix(ContentModel.TYPE_CONTENT), content); // re-read content of the node. @@ -615,6 +617,53 @@ public void testCreateReadContent() throws Exception { } } + @Test + public void testCreateNodeWithMetadata() throws UnsupportedEncodingException { + this.cleanUp(); + NodeRef companyHomeRef = repository.getCompanyHome(); + final NodeServiceTest self = this; + + try { + FileInfo mainTestFolder = this.createMainTestFolder(companyHomeRef); + FileInfo testFolder = this.createTestFolder(mainTestFolder.getNodeRef(), "testfolder"); + + String mimeType = "text/plain"; + String contentStr = "TEST CONTENT"; + InputStream is = new ByteArrayInputStream(contentStr.getBytes("UTF-8")); + + //properties to set + Map propertiesToSet = new HashMap<>(); + propertiesToSet.put(c.apix(ContentModel.PROP_NAME), new String[]{"nodeWithContent"}); + eu.xenit.apix.data.QName documentStatusQname = + new eu.xenit.apix.data.QName("{http://test.apix.xenit.eu/model/content}documentStatus"); + propertiesToSet.put(documentStatusQname, new String[]{"Draft"}); + + //aspects to add + eu.xenit.apix.data.QName[] aspectsToAdd = new eu.xenit.apix.data.QName[1]; + aspectsToAdd[0] = c.apix(ContentModel.ASPECT_TEMPORARY); + + //type to set + eu.xenit.apix.data.QName type = new eu.xenit.apix.data.QName( + "{http://test.apix.xenit.eu/model/content}withMandatoryPropDocument"); + + eu.xenit.apix.data.NodeRef createdNodeRef = self.service.createNode( + c.apix(testFolder.getNodeRef()), propertiesToSet, aspectsToAdd, null, type, null); + + assertNotNull(createdNodeRef); + assertEquals( + alfrescoNodeService.getPrimaryParent(c.alfresco(createdNodeRef)).getParentRef().toString(), + testFolder.getNodeRef().toString()); + assertEquals(c.apix(alfrescoNodeService.getType(c.alfresco(createdNodeRef))), type); + Map testProperties = alfrescoNodeService.getProperties(c.alfresco(createdNodeRef)); + assertNotNull("the cm:name property could not be found", testProperties.get(ContentModel.PROP_NAME)); + assertNotNull("", testProperties.get(c.alfresco(documentStatusQname))); + assertTrue(alfrescoNodeService.hasAspect(c.alfresco(createdNodeRef), ContentModel.ASPECT_TEMPORARY)); + } + finally { + this.cleanUp(); + } + } + @Test public void testCheckoutCheckin() { this.cleanUp(); diff --git a/apix-interface/src/main/java/eu/xenit/apix/data/QName.java b/apix-interface/src/main/java/eu/xenit/apix/data/QName.java index b0d2b294..8aa8679c 100644 --- a/apix-interface/src/main/java/eu/xenit/apix/data/QName.java +++ b/apix-interface/src/main/java/eu/xenit/apix/data/QName.java @@ -1,6 +1,7 @@ package eu.xenit.apix.data; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.JsonValue; /** @@ -10,6 +11,11 @@ public class QName { private String value; + @JsonCreator + public QName() { + + } + @JsonCreator public QName(String s) { if (s == null) { diff --git a/apix-interface/src/main/java/eu/xenit/apix/node/INodeService.java b/apix-interface/src/main/java/eu/xenit/apix/node/INodeService.java index bdfcf8f5..2cd5d472 100644 --- a/apix-interface/src/main/java/eu/xenit/apix/node/INodeService.java +++ b/apix-interface/src/main/java/eu/xenit/apix/node/INodeService.java @@ -147,6 +147,21 @@ public interface INodeService { */ NodeRef createNode(NodeRef parent, Map properties, QName type, ContentData contentData); + /** + * Creation of a node giving the list of properties as well as the type. To be used when using a custom type has + * required properties. + * + * @param parent The parent node of the new node. + * @param properties list of properties to add to node. + * @param aspectsToAdd list of aspects to add to node. + * @param aspectsToRemove list of aspects to remove from node. + * @param type The type of the node. + * @param contentData can contain returned result of function createContent (or null). + * @return The noderef of the new node. + */ + NodeRef createNode(NodeRef parent, Map properties, QName[] aspectsToAdd, QName[] aspectsToRemove, + QName type, ContentData contentData); + /** * Convenience method to create a node giving the type, the list of properties, * and requesting metadata extraction from its content. diff --git a/apix-rest-v1/src/main/java/eu/xenit/apix/rest/v1/nodes/CreateNodeOptions.java b/apix-rest-v1/src/main/java/eu/xenit/apix/rest/v1/nodes/CreateNodeOptions.java index 29d8069c..ec0e66da 100644 --- a/apix-rest-v1/src/main/java/eu/xenit/apix/rest/v1/nodes/CreateNodeOptions.java +++ b/apix-rest-v1/src/main/java/eu/xenit/apix/rest/v1/nodes/CreateNodeOptions.java @@ -21,6 +21,9 @@ public class CreateNodeOptions { public String name; public String type; public Map properties; + + public QName[] aspectsToAdd; + public QName[] aspectsToRemove; public String copyFrom; private ObjectMapper mapper = new ObjectMapper(); @@ -29,6 +32,8 @@ public CreateNodeOptions(@JsonProperty("parent") String parent, @JsonProperty("name") String name, @JsonProperty("type") String type, @JsonProperty("properties") Map properties, + @JsonProperty("aspectsToAdd") QName[] aspectsToAdd, + @JsonProperty("aspectsToRemove") QName[] aspectsToRemove, @JsonProperty("copyFrom") String copyFrom) throws IOException { this.parent = parent; this.name = name; @@ -40,6 +45,21 @@ public CreateNodeOptions(@JsonProperty("parent") String parent, if ((name != null && !this.properties.containsKey(PROP_NAME_QNAME))) { this.properties.put(PROP_NAME_QNAME, new String[]{name}); } + + if (aspectsToAdd == null) { + this.aspectsToAdd = new QName[0]; + } + else { + this.aspectsToAdd = aspectsToAdd; + } + + if (aspectsToRemove == null) { + this.aspectsToRemove = new QName[0]; + } + else { + this.aspectsToRemove = aspectsToRemove; + } + this.copyFrom = copyFrom; } @@ -59,6 +79,14 @@ public Map getProperties() { return properties; } + public QName[] getAspectsToAdd() { + return aspectsToAdd; + } + + public QName[] getAspectsToRemove() { + return aspectsToRemove; + } + public String getCopyFrom() { return copyFrom; } diff --git a/apix-rest-v1/src/main/java/eu/xenit/apix/rest/v1/nodes/NodesWebscript1.java b/apix-rest-v1/src/main/java/eu/xenit/apix/rest/v1/nodes/NodesWebscript1.java index f7bd32c0..689175a3 100644 --- a/apix-rest-v1/src/main/java/eu/xenit/apix/rest/v1/nodes/NodesWebscript1.java +++ b/apix-rest-v1/src/main/java/eu/xenit/apix/rest/v1/nodes/NodesWebscript1.java @@ -26,7 +26,6 @@ import eu.xenit.apix.permissions.IPermissionService; import eu.xenit.apix.permissions.NodePermission; import eu.xenit.apix.permissions.PermissionValue; -import eu.xenit.apix.exceptions.FileExistsException; import eu.xenit.apix.rest.v1.ApixV1Webscript; import eu.xenit.apix.rest.v1.RestV1Config; import eu.xenit.apix.rest.v1.nodes.ChangeAclsOptions.Access; @@ -584,7 +583,37 @@ public void retrieveAncestors(@UriVariable String space, @UriVariable String sto } } - @ApiOperation("Creates or copies a node") + @ApiOperation(value = "Creates or copies a node", + notes = "Example of POST body:\n" + + "\n" + + "```\n" + + "POST /apix/v1/nodes\n" + + "{\n" + + "\"parent\" : \"workspace://SpacesStore/d5dac928-e581-4507-9be7-9a2416adc318\", \n" + + "\"name\" : \"mydocument.txt\", \n" + + "\"type\" : \"{http://www.alfresco.org/model/content/1.0}content\", \n" + + "\"properties\" : {\n" + + " \"{namespace}property1\": [\n" + + " \"string\"\n" + + " ],\n" + + " \"{namespace}property2\": [\n" + + " \"string\"\n" + + " ],\n" + + " \"{namespace}property3\": [\n" + + " \"string\"\n" + + " ]\n" + + "}, \n" + + "\"aspectsToAdd\" : [\n" + + " \"{namespace}aspect1\"\n" + + "], \n" + + "\"aspectsToRemove\" : [\n" + + " \"{namespace}aspect1\"\n" + + "], \n" + + "\"copyFrom\" : \"workspace://SpacesStore/f0d15919-3841-4170-807f-b81d2ebdeb80\", \n" + + "}\n" + + "```" + + "\n" + + "\"aspectsToRemove\" is only relevant when copying a node.\n") @Uri(value = "/nodes", method = HttpMethod.POST) @ApiResponses({@ApiResponse(code = 200, message = "Success", response = NodeInfo.class), @ApiResponse(code = 403, message = "Not Authorized")}) @@ -629,7 +658,10 @@ public Object execute() throws Throwable { "Please provide parameter \"type\" when creating a new node"); return null; } - metadataChanges = new MetadataChanges(type, null, null, + + metadataChanges = new MetadataChanges(type, + createNodeOptions.aspectsToAdd, + createNodeOptions.aspectsToRemove, createNodeOptions.properties); nodeService.setMetadata(nodeRef, metadataChanges); diff --git a/build.gradle b/build.gradle index ec9a706e..cbe12e64 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ buildscript { } ext { - versionWithoutQualifier = '3.0.1' + versionWithoutQualifier = '3.0.2' jackson_version = '2.8.3' swagger_version = "1.5.7"