In previous posts we've looked at navigation, creation, management and versioning of nodes, this time we're going to look at the last set of functionality exposed via /nodes and that's associations. This is quite a long post as we have a lot to cover so grab yourself a coffee!
For this post we're going to use a custom model. I'm going to presume you already have knowledge of Alfresco content modelling, if not read this introduction first.
For those of you who've been in the Alfresco for a while may remember the Forms Development Kit (FDK) custom model. It was removed back in the 5.0 release so I've resurrected the custom model and packaged it as a simple module. Download the JAR file, copy it to $INSTALL_HOME/modules/platform and re-start the Tomcat Server.
Before we start trying to use the model let's take a quick look at the part we're going to use.
As you can see in the diagram above the model defines a type called fdk:gadget which extends cm:content. The type might be used to represent a review of a gadget in a magazine. It defines a number of properties (not shown for brevity), peer associations (blue arrows) and child associations (green arrows).
The full type definition is shown below, you can also look at the source in my Github repo or download the source JAR from nexus.
<type name="fdk:gadget">
<parent>cm:content</parent>
<properties>
<property name="fdk:make">
<type>d:text</type>
<mandatory>true</mandatory>
</property>
<property name="fdk:model">
<type>d:text</type>
<mandatory>true</mandatory>
</property>
<property name="fdk:summary">
<type>d:text</type>
<mandatory>true</mandatory>
<constraints>
<constraint ref="fdk:summary" />
</constraints>
</property>
<property name="fdk:type">
<type>d:text</type>
<constraints>
<constraint ref="fdk:type" />
</constraints>
</property>
<property name="fdk:subType">
<type>d:text</type>
<constraints>
<constraint ref="fdk:subType" />
</constraints>
</property>
<property name="fdk:rrp">
<type>d:float</type>
</property>
<property name="fdk:releaseDate">
<type>d:datetime</type>
</property>
<property name="fdk:endOfLifeDate">
<type>d:date</type>
</property>
<property name="fdk:retailers">
<type>d:text</type>
<multiple>true</multiple>
</property>
<property name="fdk:rating">
<type>d:int</type>
<constraints>
<constraint ref="fdk:percentage" />
</constraints>
</property>
</properties>
<associations>
<association name="fdk:contact">
<source>
<mandatory>false</mandatory>
<many>true</many>
</source>
<target>
<class>cm:person</class>
<mandatory>false</mandatory>
<many>false</many>
</target>
</association>
<association name="fdk:reviews">
<source>
<mandatory>false</mandatory>
<many>true</many>
</source>
<target>
<class>cm:content</class>
<mandatory>false</mandatory>
<many>true</many>
</target>
</association>
<association name="fdk:company">
<source>
<mandatory>false</mandatory>
<many>true</many>
</source>
<target>
<class>fdk:company</class>
<mandatory>false</mandatory>
<many>false</many>
</target>
</association>
<child-association name="fdk:pressRelease">
<source>
<mandatory>false</mandatory>
<many>true</many>
</source>
<target>
<class>cm:content</class>
<mandatory>false</mandatory>
<many>false</many>
</target>
</child-association>
<child-association name="fdk:images">
<source>
<mandatory>false</mandatory>
<many>true</many>
</source>
<target>
<class>cm:content</class>
<mandatory>true</mandatory>
<many>true</many>
</target>
</child-association>
</associations>
</type>
As usual a Postman collection accompanies this post, click the "Run in Postman" button below to import it into your client.
So let's start by trying to create an instance of an fdk:gagdet node in the test users home folder, try POSTing the body below to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-/children (1st request in the Postman collection😞
{
"name": "fdk.txt",
"nodeType": "fdk:gadget"
}
Unfortunately we get the error message below. If you look closely at the definition of the fdk:images child association you'll notice that the target is mandatory and we haven't provided anything!
{
"error": {
"errorKey": "framework.exception.ApiDefault",
"statusCode": 422,
"briefSummary": "10190002 Found 1 integrity violations:\nThe association child multiplicity has been violated: \n Source Node: workspace:\/\/SpacesStore\/0fe758d7-3c1d-40c0-8c02-5805ca895351\n Association: Association[ class=ClassDef[name={http:\/\/www.alfresco.org\/model\/fdk\/1.0}gadget], name={http:\/\/www.alfresco.org\/model\/fdk\/1.0}images, target class={http:\/\/www.alfresco.org\/model\/content\/1.0}content, source role=null, target role=null]\n Required child Multiplicity: 1..*\n Actual child Multiplicity: 0",
"stackTrace": "For security reasons the stack trace is no longer displayed, but the property is kept for previous versions.",
"descriptionURL": "https:\/\/api-explorer.alfresco.com"
}
}
Note: If you get an error saying "fdk:gadget isn't a valid QName" it means you haven't copied the simple JAR to the correct location.
We need something to "associate" our new node with so let's create an "Images" folder, upload some images to it and upload some content in the home folder to use as a review.
I won't detail all the API calls to do that here, either refer back to part 3 or examine requests 2 through 5 in the Postman collection. If you do this yourself, copy the id of each of the nodes you create as we'll need them later.
Let's also create an fdk:company object so that we can create an fdk:company association. I've uploaded some images of the Amazon Echo so lets create a node representing Amazon in the home folder by POSTing the body below to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-/children (6th request in the Postman collection😞
{
"name": "Amazon",
"nodeType": "fdk:company",
"properties": {
"fdk:email": "info@amazon.com",
"fdk:url": "http://www.amazon.com",
"fdk:city": "Seattle"
}
}
Now we're finally ready to create our gadget node. We can specify the child associations using the secondaryChildren property and the peer associations using the targets property.
The body below POSTed to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-/children (7th request in the Postman collection) will create a gadget node named "Amazon Echo" in the home folder. It will also create two fdk:images child associations to the images we uploaded earlier (lines 7 and 11), an fdk:reviews peer association to the review text (line 17) and an fdk:company peer association to the company node (line 21).
{
"name": "Amazon Echo",
"nodeType": "fdk:gadget",
"secondaryChildren": [
{
"childId": "{{firstImageId}}",
"assocType": "fdk:images"
},
{
"childId": "{{secondImageId}}",
"assocType": "fdk:images"
}
],
"targets": [
{
"targetId": "{{reviewTextId}}",
"assocType": "fdk:reviews"
},
{
"targetId": "{{companyId}}",
"assocType": "fdk:company"
}
]
}
Now that we've provided the mandatory information we get a successful 201 response and a representation of the new node we created. Copy the id of the gadget node, hereinafter referred to as gadgetId.
There's no mention of the new associations we specified, so how do we know they were created?
As you may have guessed there are endpoints to return them. To get a list of the targets (peer associations) we created use the URL http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{gadgetId}}/targets (8th request in the Postman collection), this returns the following response:
{
"list": {
"pagination": {
"count": 2,
"hasMoreItems": false,
"totalItems": 2,
"skipCount": 0,
"maxItems": 100
},
"entries": [
{
"entry": {
"createdAt": "2016-11-19T10:07:11.559+0000",
"isFolder": false,
"isFile": true,
"createdByUser": {
"id": "test",
"displayName": "Test Test"
},
"modifiedAt": "2016-11-19T10:07:11.559+0000",
"modifiedByUser": {
"id": "test",
"displayName": "Test Test"
},
"name": "review-text.txt",
"association": {
"assocType": "fdk:reviews"
},
"id": "85f6e9c7-0271-446f-b817-24c6d9fd338a",
"nodeType": "cm:content",
"content": {
"mimeType": "text\/plain",
"mimeTypeName": "Plain Text",
"sizeInBytes": 3186,
"encoding": "ISO-8859-1"
},
"parentId": "bd8f1283-3e84-4585-aafc-12da26db760f"
}
},
{
"entry": {
"createdAt": "2016-11-19T10:17:25.997+0000",
"isFolder": false,
"isFile": true,
"createdByUser": {
"id": "test",
"displayName": "Test Test"
},
"modifiedAt": "2016-11-19T10:17:25.997+0000",
"modifiedByUser": {
"id": "test",
"displayName": "Test Test"
},
"name": "Amazon",
"association": {
"assocType": "fdk:company"
},
"id": "7bbbaf43-e295-46d9-9dd2-df999e42dcf9",
"nodeType": "fdk:company",
"content": {
"mimeType": "application\/octet-stream",
"mimeTypeName": "Binary File (Octet Stream)",
"sizeInBytes": 0,
"encoding": "UTF-8"
},
"parentId": "bd8f1283-3e84-4585-aafc-12da26db760f"
}
}
]
}
}
The response shows the nodes at the end of the association and which association it is (lines 27 and 56). We can also combine some of the techniques we've learnt in previous posts and just request the fdk:reviews association and show the path and properties of the target node using http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{gadgetId}}/targets?i... (9th request in the Postman collection).
We can retrieve the list of secondary child associations by using http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{gadgetId}}/secondary... (10th request in the Postman collection). The response is similar to the previous example except we're seeing the two fdk:images child associations (lines 28 and 58).
{
"list": {
"pagination": {
"count": 2,
"hasMoreItems": false,
"totalItems": 2,
"skipCount": 0,
"maxItems": 100
},
"entries": [
{
"entry": {
"createdAt": "2016-11-19T10:00:27.038+0000",
"isFolder": false,
"isFile": true,
"createdByUser": {
"id": "test",
"displayName": "Test Test"
},
"modifiedAt": "2016-11-19T10:23:12.485+0000",
"modifiedByUser": {
"id": "test",
"displayName": "Test Test"
},
"name": "alexa.jpg",
"association": {
"isPrimary": false,
"assocType": "fdk:images"
},
"id": "e7f8a006-e027-4f06-8c30-0f5f11bc9211",
"nodeType": "cm:content",
"content": {
"mimeType": "image\/jpeg",
"mimeTypeName": "JPEG Image",
"sizeInBytes": 24397,
"encoding": "UTF-8"
},
"parentId": "c67c8ea2-7ec3-415e-9a27-94d6cb58b2ec"
}
},
{
"entry": {
"createdAt": "2016-11-19T10:01:53.353+0000",
"isFolder": false,
"isFile": true,
"createdByUser": {
"id": "test",
"displayName": "Test Test"
},
"modifiedAt": "2016-11-19T10:23:12.564+0000",
"modifiedByUser": {
"id": "test",
"displayName": "Test Test"
},
"name": "amazon-echo-alexa.jpg",
"association": {
"isPrimary": false,
"assocType": "fdk:images"
},
"id": "6733f33f-f708-4747-acc5-1ed5147d0cb2",
"nodeType": "cm:content",
"content": {
"mimeType": "image\/jpeg",
"mimeTypeName": "JPEG Image",
"sizeInBytes": 81789,
"encoding": "UTF-8"
},
"parentId": "c67c8ea2-7ec3-415e-9a27-94d6cb58b2ec"
}
}
]
}
}
What if we want to go in the other direction and see what links to a particular node, for peer associations we can use http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{reviewTextId}}/sourc... (11th request in the Postman collection) to see what links to the review text content, you should see it's the "Amazon Echo" node.
For child associations we use http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{firstImageId}}/paren... (12th request in the Postman collection) to see the parents of the first image we uploaded earlier, you should see it has 2 parents, the "Amazon Echo" node and folder where the image itself was uploaded.
It's also possible to create associations on nodes that already exist, let's add some more to our "Amazon Echo" fdk:gadget node. Before we do that though upload another image to the images folder and some more content to represent another review (13th and 14th requests in the Postman collection) and copy the ids of the new nodes.
To create another child association we use the same URL we used earlier to retrieve the list of secondary child associations (http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{gadgetId}}/secondary...) except this time we POST to it (15th request in the Postman collection). Sending the body below will create another child association to the 3rd image:
{
"childId": "{{thirdImageId}}",
"assocType": "fdk:images"
}
We can do the same thing to create another peer association by POSTing the body below to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{gadgetId}}/targets (16th request in the Postman collection😞
{
"targetId": "{{secondReviewTextId}}",
"assocType": "fdk:reviews"
}
If you now do a GET on the same URLs you should now see three fdk:images child associations and three peer associations, two of type fdk:reviews and one of type fdk:company.
Alfresco has a feature called multi-filing, this is where a node can appear in multiple folders, think of it as a unix symbolic link. This feature has been available via the CMIS API for a long time but we've now exposed this via the v1 REST API too.
When we navigate around the repository we're actually following the cm:contains child association, to make a node appear in multiple folders we can create a secondary child association from the folder to the node. To make the review text we uploaded earlier also appear in the images folder POST the following body to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{imagesFolderId}}/sec... (17th request in the Postman collection😞
{
"childId": "{{reviewTextId}}",
"assocType": "cm:contains"
}
Now request a listing of the images folder's children using http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{imagesFolderId}}/chi... (18th request in the Postman collection). The trimmed response below shows that the review-text.txt node now also appears in the images folder (line 44). By asking for the association information to be included we can see that the review-text.txt node is a secondary child association via the isPrimary flag (line 46), this allows clients to handle these "linked" nodes differently i.e. restrict deletion.
{
"list": {
"pagination": {
"count": 4,
"hasMoreItems": false,
"totalItems": 4,
"skipCount": 0,
"maxItems": 100
},
"entries": [
{
"entry": {
...
"name": "alexa.jpg",
"association": {
"isPrimary": true,
"assocType": "cm:contains"
}
}
},
{
"entry": {
...
"name": "amazon-echo-alexa.jpg",
"association": {
"isPrimary": true,
"assocType": "cm:contains"
}
}
},
{
"entry": {
...
"name": "echo-dot.jpg",
"association": {
"isPrimary": true,
"assocType": "cm:contains"
}
}
},
{
"entry": {
...
"name": "review-text.txt",
"association": {
"isPrimary": false,
"assocType": "cm:contains"
}
}
}
]
}
}
Using the where clause we can actually hide these "linked" nodes completely, try http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{imagesFolderId}}/chi... (19th request in the Postman collection) and you'll notice only the three images are returned.
The last thing to cover is deleting associations, let's start by removing the second review from our fdk:gadget node. To do this we send a DELETE request to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{gadgetId}}/targets/{... (20th request in the Postman collection).
We can do the same thing for child associations using DELETE http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/{{gadgetId}}/secondary... (21st request in the Postman collection), this will remove the child association between the fdk:gagdet node and the third image we uploaded.
Although it's not mandatory it's important to call out the assocType query parameter, this defines which type of associations to remove, if we omit this parameter ALL associations (peer or child depending on the URL used) between the two nodes are removed.
Well done if you made it this far, I did warn you this one was going to be a long post!
That concludes our journey through the new /nodes endpoints added for the 5.2 release, next time we're going to cover some of the collaboration endpoints that have been in the product for a few releases now.