Skip navigation
All Places > Alfresco Content Services (ECM) > Blog > 2016 > November
2016

Introduction

After I wrote my last blog post on customizing the Create and Edit Site dialogs I realised that there was no single point of consolidated information on all of the many capabilities that are available in Aikau forms. Rather than trying to write everything up in a blog post I've decided to record a series of videos demonstrating as many of the different features as I could. The videos (that I've completed so far!) are embedded in this post but are available separately within the Alfresco Community platform. I've tried to break the content into manageable chunks (and still have a couple more videos to record) but there is still a lot of footage to get through!

 

Part 1 - The Basics

This video goes through the basics of setting up a form and the common configuration attributes that all form controls share.

 

Part 2 - Advanced Data Handling

This video goes through options for setting values in the form (via publication, URL hash, etc) as well as covering form warning configuration.

 

Part 3 - Options Handling

This video covers dynamic options handling showing how to access data from the Alfresco Repository in a variety of different form controls as well as examples for configuring how multi-value data is handled.

 

Part 4 - Layout and Dialogs

Coming soon!

 

Part 5 - Multiple Entry Form Controls

Coming soon!

We will now follow  Gavin Cornwell  v1 REST API - Part 6 - Associations examples and see how we can achieve the same experience using the SDK

 

To make the exercise more concise we will execute each request in a synchronous way.

Important Notice

Alfresco Java Client is currently in Early Access mode. It evolves as you use them, as you give feedback, and as the developers update and add file. We like to think app & lib development as services that grow and evolve with the involvement of the community.

 

Prerequisites

In order to follow along you'll need an environment to do so, firstly download and install the 5.2.c Early Access Community Release. In our case we will consider Alfresco is available at http://localhost:8080/alfresco and the "admin" user is available and has "admin" as password.

 

Attempt creation of fdk:gadget node

// Select NodesAPI
NodesAPI nodesAPI = client.getNodesAPI();

//Create Empty Node
NodeBodyCreate emptyFileBody = new NodeBodyCreate("fdk.txt", "fdk:gadget");
Response<NodeRepresentation> initialNodeResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, emptyFileBody).execute();
Assert.assertFalse(initialNodeResponse.isSuccessful());
Assert.assertEquals(initialNodeResponse.code(), 422);

 

Create Images folder

//Create Images Folder
NodeBodyCreate creationBody = new NodeBodyCreate("Images", ContentModel.TYPE_FOLDER);
Response<NodeRepresentation> folderCreationResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, creationBody).execute();
Assert.assertTrue(folderCreationResponse.isSuccessful());
Assert.assertEquals(folderCreationResponse.body().getName(), "Images");

 

Upload first image

//Upload First Image
File file = new File("W:\\image.png");
RequestBody requestBody = RequestBody.create(MediaType.parse("image/png"), file);
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "image.png", requestBody);
RequestBody fileRequestBody = multipartBuilder.build();

//Create Content
Response<NodeRepresentation> firstImageResponse = nodesAPI.createNodeCall(folderCreationResponse.body().getId(), fileRequestBody).execute();

 

Upload second image

//Upload Second Image
file = new File("W:\\image.png");
requestBody = RequestBody.create(MediaType.parse("image/png"), file);
multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "image2.png", requestBody);
fileRequestBody = multipartBuilder.build();

//Create Content
Response<NodeRepresentation> secondImageResponse = nodesAPI.createNodeCall(folderCreationResponse.body().getId(), fileRequestBody).execute();

 

Upload review text

//Upload review text
file = new File("W:\\test.txt");
requestBody = RequestBody.create(MediaType.parse("text/plain"), file);
multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "test.txt", requestBody);
fileRequestBody = multipartBuilder.build();

HashMap<String, RequestBody> map = new HashMap<>();
map.put("filedata", fileRequestBody);
map.put("name", RequestBody.create(MediaType.parse("multipart/form-data"), "review-text.txt"));

//Create Content
Response<NodeRepresentation> reviewTextResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, map).execute();

 

Create Amazon company node

//Create Amazon company node
Map<String, Object> props = new HashMap<>();
props.put("fdk:email", "info@amazon.com");
props.put("fdk:url", "http://www.amazon.com");
props.put("fdk:city", "Seattle");

NodeBodyCreate amazonNode = new NodeBodyCreate("Amazon", "fdk:company", props, null);
Response<NodeRepresentation> amazonNodeResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, amazonNode).execute();
Assert.assertTrue(amazonNodeResponse.isSuccessful());

 

 

Create fdk:gadget node

//Create fdk:gadget node
//Secondary Children
ChildAssociationBody firstImage = new ChildAssociationBody(firstImageResponse.body().getId(), "fdk:images");
ChildAssociationBody secondImage = new ChildAssociationBody(secondImageResponse.body().getId(), "fdk:images");
List<ChildAssociationBody> secondaryChildren = Arrays.asList(firstImage, secondImage);

//Target
AssociationBody review = new AssociationBody(reviewTextResponse.body().getId(), "fdk:reviews");
AssociationBody company = new AssociationBody(amazonNodeResponse.body().getId(), "fdk:company");
List<AssociationBody> targets = Arrays.asList(review, company);

//Create Gadget
NodeBodyCreate gadgetNode = new NodeBodyCreate("Amazon Echo", "fdk:gadget", null, null, null, null, secondaryChildren, targets);
Response<NodeRepresentation> amazonEchoResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, gadgetNode).execute();
Assert.assertTrue(amazonEchoResponse.isSuccessful());

 

Get fdk:gadget peer associations

//Get fdk:gadget peer associations
Response<ResultPaging<NodeRepresentation>> targetsAssocsResponse = nodesAPI.listTargetAssociationsCall(amazonEchoResponse.body().getId()).execute();
Assert.assertTrue(targetsAssocsResponse.isSuccessful());
Assert.assertEquals(targetsAssocsResponse.body().getCount(), 2);

 

Get fdk:reviews association with path and properties

//Get fdk:gadget peer associations
Response<ResultPaging<NodeRepresentation>> targetsAssocsResponse2 = nodesAPI.listTargetAssociationsCall(
amazonEchoResponse.body().getId(),
"(assocType='fdk:reviews')",
new IncludeParam(Arrays.asList("properties","path")), null).execute();
Assert.assertTrue(targetsAssocsResponse2.isSuccessful());
Assert.assertEquals(targetsAssocsResponse2.body().getCount(), 1);

 

Get fdk:gadget secondary child associations

//Get fdk:gadget secondary child associations
Response<ResultPaging<NodeRepresentation>> childAssocsResponse = nodesAPI.listSecondaryChildrenCall(amazonEchoResponse.body().getId()).execute();
Assert.assertTrue(childAssocsResponse.isSuccessful());
Assert.assertEquals(childAssocsResponse.body().getCount(), 2);

 

Get review text sources

Response<ResultPaging<NodeRepresentation>> reviewAssocsResponse = nodesAPI.listSourceAssociationsCall(reviewTextResponse.body().getId()).execute();
Assert.assertTrue(reviewAssocsResponse.isSuccessful());
Assert.assertEquals(reviewAssocsResponse.body().getCount(), 1);

 

Get first image parents

Response<ResultPaging<NodeRepresentation>> firstImageParentResponse = nodesAPI.listParentsCall(firstImageResponse.body().getId()).execute();
Assert.assertTrue(firstImageParentResponse.isSuccessful());
Assert.assertEquals(firstImageParentResponse.body().getCount(), 2);

 

Upload third image

//Upload Third image
file = new File("W:\\image.png");
requestBody = RequestBody.create(MediaType.parse("image/png"), file);
multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "image3.png", requestBody);
fileRequestBody = multipartBuilder.build();

//Create Content
Response<NodeRepresentation> thirdImageResponse = nodesAPI.createNodeCall(folderCreationResponse.body().getId(), fileRequestBody).execute();

 

Upload second review text

file = new File("W:\\test.txt");
requestBody = RequestBody.create(MediaType.parse("text/plain"), file);
multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "test.txt", requestBody);
fileRequestBody = multipartBuilder.build();

HashMap<String, RequestBody> map2 = new HashMap<>();
map2.put("filedata", fileRequestBody);
map2.put("name", RequestBody.create(MediaType.parse("multipart/form-data"), "2nd-review-text.txt"));

//Create Content
Response<NodeRepresentation> secondReviewTextResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, map2).execute();

 

Create fdk:images child association

ChildAssociationBody thirdImage = new ChildAssociationBody(thirdImageResponse.body().getId(), "fdk:images");
Response<ChildAssociationRepresentation> imageAssoc = nodesAPI.createSecondaryChildAssocationCall(amazonEchoResponse.body().getId(), thirdImage, null).execute();
Assert.assertTrue(imageAssoc.isSuccessful());

 

Create fdk:reviews peer association

AssociationBody secondReview = new AssociationBody(secondReviewTextResponse.body().getId(), "fdk:reviews");
Response<AssociationRepresentation> reviewAssoc = nodesAPI.createAssocationCall(amazonEchoResponse.body().getId(), secondReview, null).execute();
Assert.assertTrue(reviewAssoc.isSuccessful());

 

Multi-file review text

ChildAssociationBody reviewChild = new ChildAssociationBody(reviewTextResponse.body().getId(), "cm:contains");
Response<ChildAssociationRepresentation> reviewChildAssoc = nodesAPI.createSecondaryChildAssocationCall(folderCreationResponse.body().getId(), reviewChild, null).execute();
Assert.assertTrue(reviewChildAssoc.isSuccessful());

 

Get images folder children

Response<ResultPaging<NodeRepresentation>> imageFolderChildrenResponse =
nodesAPI.listNodeChildrenCall(folderCreationResponse.body().getId(), null, null, null, null, new IncludeParam(Arrays.asList("association")), null, null, null).execute();
Assert.assertTrue(imageFolderChildrenResponse.isSuccessful());
Assert.assertEquals(imageFolderChildrenResponse.body().getCount(), 4);

 

Get images folder primary children only

Response<ResultPaging<NodeRepresentation>> imageFolderPrimaryChildrenResponse =
nodesAPI.listNodeChildrenCall(folderCreationResponse.body().getId(), null, null, null, "(isPrimary=true)", null, null, null, null).execute();
Assert.assertTrue(imageFolderPrimaryChildrenResponse.isSuccessful());
Assert.assertEquals(imageFolderPrimaryChildrenResponse.body().getCount(), 3);

 

Delete second review text peer association

Response<Void> deleteAssocResponse = nodesAPI.deleteAssocationCall(amazonEchoResponse.body().getId(), secondReviewTextResponse.body().getId(), "fdk:reviews").execute();
Assert.assertTrue(deleteAssocResponse.isSuccessful());

 

Delete third image child association

Response<Void> deleteChildAssocResponse = nodesAPI.deleteSecondaryChildAssocationCall(amazonEchoResponse.body().getId(), thirdImageResponse.body().getId(), "fdk:images").execute();
Assert.assertTrue(deleteChildAssocResponse.isSuccessful());

 

Alfresco Java Client SDK Series

Introduction

If you've tried out the Early Access Community Release (201611) of Alfresco you might have noticed that in version 5.2 the Create and Edit site dialogs are now being rendered by Aikau. As well as providing some user experience improvements the main benefit of switching to Aikau is to make it much easier to customize both dialogs. In this post I'm going to provide some examples of the options that are now available to you.

PLEASE NOTE: The early access release doesn't have all of the customization options described here - to try them all out you'll need version 1.0.98 of Aikau.

 

Don't Panic

If you have already made significant customizations to the the YUI2 based versions of the dialog then don't panic. We've provided a very simple way in which to use the old dialogs so that your existing customizations can continue to work just as they've always done. The SiteService that controls which dialogs are used has a legacyMode configuration attribute that when set to true will use the YUI2 dialogs. In Share 5.2 we are re-configuring this to be false so if you want to continue using the YUI2 dialogs you just need to create an extension that configures it back to true again!

 

Download Extension

The first job is to actually build an extension module to customize the SiteService on all pages within Share.... however, you may not want to do that so I've provided an extension module that does exactly this in this GitHub repository. You can download a pre-built extension from here (to re-enable the YUI dialogs) or use it as a starting point for implementing the other customizations described in this post (but don't forget to switch legacyMode to false!!!)

 

Custom Presets

Alfresco only comes with a single site preset (the "Collaboration Site"). In order to make more site presets show up in the old dialog it was necessary to create an extension module to customize the create-site.get.js WebScript JavaScript controller. All customization is now done via the SiteService and you now have options to:

 

  • Explicitly define the presets (via the sitePresets attribute)
  • Provide additional presets (via the additionalSitePresets attribute)
  • Remove existing presets (via the sitePresetsToRemove attribute)

 

Let's add some additional presets. In the share-header.get.js customization file create the following:

 

var siteService = widgetUtils.findObject(model.jsonModel, "id", "SITE_SERVICE");
if (siteService && siteService.config)
{
  siteService.config.additionalSitePresets = [{ label: "Custom Preset", value: "CUSTOM" } ];
}

 

This will add the additional preset into the create site dialog, like so:

 

Customized create site dialog with additional preset

 

Dialog Model Overrides

The widgets model for the create site dialog is defined in the widgetsForCreateSiteDialog attribute. You can completely replace the model if you wish - but a more efficient way to make changes would be to make use of the widgetsForCreateSiteDialogOverrides attribute. This is an additional model that lets you...

 

  • add widgets (at the start and end or before/after a specific widget in the default model)
  • remove widgets
  • replace widgets
  • merge additional configuration into existing widgets

 

Removing Fields

Let's start by removing the "Description" field. Update the share-header.get.js file so that it looks like this:

 

var siteService = widgetUtils.findObject(model.jsonModel, "id", "SITE_SERVICE");
if (siteService && siteService.config)
{
  siteService.config.additionalSitePresets = [{ label: "Custom Preset", value: "CUSTOM" } ];
  siteService.config.widgetsForCreateSiteDialogOverrides = [
    {
      id: "CREATE_SITE_FIELD_DESCRIPTION",
      remove: true
    }
  ];
}

 

This instructs the SiteService to remove any widget with the id attribute "CREATE_SITE_FIELD_DESCRIPTION" and has the effect of removing the "Description" field as shown here:

 

Create site dialog with the description field removed

 

Updating Fields

There are two ways in which you can update an existing field. The first method is using the replace attribute. As the name suggests this will completely replace any widget with a matching id. Add the following entry into the widgetsForCreateSiteDialogOverrides array:

 

{
  id: "CREATE_SITE_FIELD_VISIBILITY",
  replace: true,
  name: "alfresco/forms/controls/Select",
  config: {
    fieldId: "VISIBILITY",
    label: "How am I seen?",
    name: "visibility",
    optionsConfig: {
      fixed: [
        {
          label: "By everyone",
          value: "PUBLIC"
        },
        {
          label: "By Some",
          value: "MODERATED"
        }
      ]
    }
  }
}

 

...and the result is that the original RadioButtons widget is replaced by a Select widget like this:

 

Create site dialog with a replaced field

 

If you just want to merge some existing configuration into an existing field then you can just omit the replace attribute and just provide the new or changed attributes. Change the replace entry to be this:

 

{
  id: "CREATE_SITE_FIELD_VISIBILITY",
  config: {
    label: "How am I seen?",
  }
}

 

This has the effect of simply updating the label displayed:

 

Create site dialog with an updated field

 

Adding New Fields

If you just want to add a brand new field into the dialog then you can do so using the targetPosition attribute. This accepts the following 4 different values:

 

  • "START"
  • "END"
  • "BEFORE"
  • "AFTER"

 

When using "BEFORE" or "AFTER" it is also necessary to specific a targetId attribute to indicate which existing field the new fields should be added in relation to.

 

For example, let's say that you want to add a new field at the start of the dialog. Add the following entry into the widgetsForCreateSiteDialogOverrides array:

 

{
  id: "FIRST",
  name: "alfresco/forms/controls/TextBox",
  targetPosition: "START",
  config: {
    fieldId: "FIRST",
    label: "First!",
    name: "first",
    description: "This field has been added as the first entry"
  }
}

 

This will result in the following dialog:

 

Create site dialog with a new field at the start of the dialog

 

If you wanted to insert a field between the "Site ID" and "Name" fields then use a combination of targetPostition and targetId like this:

 

{
  id: "INSERTED",
  name: "alfresco/forms/controls/TextBox",
  targetPosition: "BEFORE",
  targetId: "CREATE_SITE_FIELD_SHORTNAME",
  config: {
    fieldId: "INSERTED",
    label: "Inserted",
    name: "inserted",
    description: "This field has been inserted before another field"
  }
}

 

This would then result in the following dialog:

 

Create site dialog with a new field inserted in the middle of the form

 

How the Form Works

The examples shown are very simplistic and are provided to demonstrate the principals of customizing the dialog forms. You can make full use of any and all of the capabilities provided by the many Aikau form controls that are available. You can find a full list of the form controls available in the JSDoc (look under the alfresco/forms/controls package) and can find examples of the basic form concepts in a Sandpit page.  Lots of information about form control configuration can also be found in videos embedded in this blog post.

 

Any form control added to the dialog will automatically have it's value included in the XHR request to the /modules/create-site and /modules/edit-site WebScripts respectively. It will therefore be necessary to also customize or override these WebScripts to make use of the extra data that your customized dialogs provide (for example you may wish to apply custom Aspects to the site and set additional metadata on them). However, these changes are outside the scope of this article which is focused solely on the client-side user interface changes.

 

Reusable Mix-in Module

The overriding capabilities are provided through the new module WidgetsOverrideMixin introduced in Aikau 1.0.97. This module can be mixed into other widgets and services to provide the same model manipulation. If there are any existing areas of Aikau code that you feel could benefit from this capability then please let me know!

 

Missing Use Cases?

It's quite possible that these capabilities still do not meet your specific customization requirements for the create and edit site dialogs. I did create a poll a few weeks ago to try and gather as much information as possible but didn't get many concrete use cases to address. If you feel that these updates don't support your specific requirements then please let me know in order that we can address them. 

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?include=properties,path&where=(assocType='fdk:reviews') (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-children (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}}/sources (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}}/parents (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-children) 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}}/secondary-children (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}}/children?include=association (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}}/children?where=(isPrimary=true) (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/{{secondReviewTextId}}?assocType=fdk:reviews (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-children/{{thirdImageId}}?assocType=fdk:images (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.

Introduction

With the 1.0.96 release of Aikau we've introduced a new AMD package (simply called "aikau") that will give you a sneak peak of where things are going to be heading. This post is going to explain why we've made this addition, what it will do for you and how you can contribute to its success.

 

Future Directions

If you watched my recent Tech Talk Live presentation you'll have a good idea about the history of Aikau and you'll also have been given a brief overview of where we want to take it. The first couple of items on the road map were performance improvements and the introduction of Material Design styled widgets. Alfresco has decided to adopt Material Design and this means that at some point in the future we may want to update Share to use this "visual language" (Google's terminology, not mine). 

 

The simplest approach to achieving this aim is to convert more of Share to use Aikau, and then either update the styling of the widgets used to define the pages to use Material Design or swap them out with dedicated Material Designed widgets. 

 

At the same time we also want to apply the lessons that have been learned over the last 4 years to this new suite of widgets. One of our most valuable Community members (Axel Faust) provided us with some great analysis of how Aikau performs and we know that there is room for improvement. 

 

Right and Wrong

Some things we got right... Fine grained components that follow the "Single Responsibility Principle" (having learned our lesson from the coarse grained YUI2 components) being one and another being decoupling over a publication / subscription model (whilst adding complexity) to provide clean customization, easy enhancement and promote reuse. I did however initially imagine that a widget would only ever have one set of child widgets... and this proved to be incorrect and I also didn't focus enough on the performance of generating child widget at large scale. 

 

New Package, New Rules

Aikau is released on a weekly basis and every release is guaranteed to be backwards compatible with the last. We also try to avoid removing functions or changing their signatures to ensure that 3rd party widgets that rely on them don't break. This makes working on something as complex as performance quite an interesting challenge.

 

Therefore we've introduced a new package called "aikau" (the original package is called "alfresco") in which we're going to create our new Material Design style widgets that inherit from from new, performance enhanced modules. In order to make it easier to collaborate with the Alfresco Community the new "aikau" package will not be subject to the same stringent, backwards compatibility rules as the original "alfresco" package.

 

This means that we will be able to make breaking changes between releases should it be necessary and therefore you shouldn't start using these in production yet. Ultimately we want the "aikau" package to be a safe collaboration space that will eventually form a 1.1 release of Aikau. At which point the original "alfresco" package will be deprecated (but not removed) as there will be a full complement of Material Design widgets and production applications (such as Share) can begin to transition to using them. 

 

What This Means

This gives us the best of both worlds - the "alfresco" package will continue to be developed, new widgets will be added, bugs will be fixed and weekly releases will continue... but we will open up the contribution process of the "aikau" package to encourage more active participation without needing to worry about backwards compatibility or regression testing until we're confident that we've got the implementation right. 

 

If you have an interest in the future direction of Aikau and Share then now is the time to get involved. In my next post I'll showcase the new Material Design widgets and discuss what we've done so far and the other areas where we're looking to improve performance. If you have additional ideas of where you'd like to see Aikau go then we'd love to hear about them.

 

We will now follow Gavin Cornwell v1 REST API - Part 5 - Versioning & Locking examples and see how we can achieve the same experience using the SDK

 

To make the exercise more concise we will execute each request in a synchronous way.

Important Notice

Alfresco Java Client is currently in Early Access mode. It evolves as you use them, as you give feedback, and as the developers update and add file. We like to think app & lib development as services that grow and evolve with the involvement of the community.

 

Prerequisites

In order to follow along you'll need an environment to do so, firstly download and install the 5.2.c Early Access Community Release. In our case we will consider Alfresco is available at http://localhost:8080/alfresco and the "admin" user is available and has "admin" as password.

 

Create Empty File

// Select NodesAPI
NodesAPI nodesAPI = client.getNodesAPI();

//Create Empty Node
NodeBodyCreate emptyFileBody = new NodeBodyCreate("version.txt", ContentModel.TYPE_CONTENT);
Response<NodeRepresentation> initialNodeResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, emptyFileBody).execute();

 

Update content as major version

//Update content as Major Version
String nodeId = initialNodeResponse.body().getId();
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), "This is the initial content for the file.");
NodeRepresentation updatedContentNode = nodesAPI.updateNodeContentCall(nodeId, requestBody, true, "First version").execute().body();
Assert.assertEquals(updatedContentNode.getContent().getSizeInBytes(), 41);
Assert.assertEquals(updatedContentNode.getProperties().get(ContentModel.PROP_VERSION_LABEL), "1.0");

 

Retrieve version history

//Retrieve version history
VersionAPI versionAPI = client.getVersionAPI();
ResultPaging<VersionRepresentation> versions = versionAPI.listVersionHistoryCall(nodeId).execute().body();
Assert.assertEquals(versions.getCount(), 1);
Assert.assertEquals(versions.getPagination().getCount(), 1);

 

 

Update content as minor version

//Update content as minor version
RequestBody minorRequestBody = RequestBody.create(MediaType.parse("text/plain"), "This is the second version of the content, v1.1.");
NodeRepresentation minorupdatedContentNode = nodesAPI.updateNodeContentCall(nodeId, minorRequestBody, false, "Second version").execute().body();
Assert.assertEquals(updatedContentNode.getContent().getSizeInBytes(), 50);
Assert.assertEquals(updatedContentNode.getProperties().get(ContentModel.PROP_VERSION_LABEL), "1.1");

 

Get content

//Get Content
Call<ResponseBody> downloadCall = nodesAPI.getNodeContentCall(nodeId);
File dlFile = new File("W:\\", "version-1.1.txt");
IOUtils.copyFile(downloadCall.execute().body().byteStream(), dlFile);
Assert.assertEquals(dlFile.length(), 48);

 

 

Retrieve version content

//Retrieve Version Content
Call<ResponseBody> downloadOriginalCall = versionAPI.getVersionContentCall(nodeId, "1.0");
File originalFile = new File("W:\\", "version-1.0.txt");
IOUtils.copyFile(downloadOriginalCall.execute().body().byteStream(), originalFile);
Assert.assertEquals(originalFile.length(), 41);

 

Revert version

//Revert Version
RevertBody revertBody = new RevertBody("Reverted to original", true);
VersionRepresentation versionReverted = versionAPI.revertVersionCall(nodeId, "1.0", revertBody).execute().body();
Assert.assertEquals(versionReverted.getContent().getSizeInBytes(), 41);
Assert.assertEquals(versionReverted.getProperties().get(ContentModel.PROP_VERSION_LABEL), "2.0");

 

Lock file

//Lock File
NodeRepresentation lockedNode = nodesAPI.lockNodeCall(nodeId, new NodeBodyLock(), new IncludeParam(Arrays.asList("isLocked")), null).execute().body();
Assert.assertTrue(lockedNode.isLocked());
Assert.assertTrue(lockedNode.getAspects().contains(ContentModel.ASPECT_LOCKABLE));
Assert.assertEquals(lockedNode.getProperties().get(ContentModel.PROP_LOCK_TYPE), "WRITE_LOCK");
Assert.assertEquals(lockedNode.getProperties().get(ContentModel.PROP_LOCK_LIFETIME), NodeBodyLock.LifetimeEnum.PERSISTENT.toString());

 

Unlock file

//UnLock File
NodeRepresentation unlockedNode = nodesAPI.unlockNodeCall(nodeId, new NodeBodyUnLock(), new IncludeParam(Arrays.asList("isLocked")), null).execute().body();
Assert.assertFalse(unlockedNode.isLocked());
Assert.assertFalse(unlockedNode.getAspects().contains(ContentModel.ASPECT_LOCKABLE));

 

Alfresco Java Client SDK Series

We will now follow  Gavin Cornwell  v1 REST API - Part 4 - Managing Nodes  examples and see how we can achieve the same experience using the SDK

 

To make the exercise more concise we will execute each request in a synchronous way.

 

Important Notice

Alfresco Java Client is currently in Early Access mode. It evolves as you use them, as you give feedback, and as the developers update and add file. We like to think app & lib development as services that grow and evolve with the involvement of the community.

 

Prerequisites

In order to follow along you'll need an environment to do so, firstly download and install the 5.2.c Early Access Community ReleaseIn our case we will consider Alfresco is available at http://localhost:8080/alfresco and the "admin" user is available and has "admin" as password.

 

Part 4 - Managing Nodes

 

Create Empty File

//Create Empty Node with Properties
LinkedTreeMap<String, Object> properties = new LinkedTreeMap<>();
properties.put(ContentModel.PROP_TITLE, "The Title");

NodeBodyCreate emptyFileBody = new NodeBodyCreate("my-file.txt", ContentModel.TYPE_CONTENT, properties, null);

Response<NodeRepresentation> emptyNodeResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, emptyFileBody).execute();

NodeRepresentation emptyNode = emptyNodeResponse.body();
Assert.assertEquals(emptyNode.getContent().getSizeInBytes(), 0);

Get Node Information

//Get Node Information
String nodeId = emptyNode.getId();
NodeRepresentation node = nodesAPI.getNodeCall(nodeId).execute().body();
Assert.assertNotNull(node.getProperties());
Assert.assertNotNull(node.getAspects());
Assert.assertTrue(node.isFile());
Assert.assertFalse(node.isFolder());
Assert.assertNull(node.isLink());

Get Node Information with isLink and path

//Get node information with isLink and path
node = nodesAPI.getNodeCall(nodeId, new IncludeParam(Arrays.asList("isLink", "path")), null, null).execute().body();
Assert.assertFalse(node.isLink());
Assert.assertNotNull(node.getPath());

Update Node Information

//Update Node Information
LinkedTreeMap<String, Object> props = new LinkedTreeMap<>();
props.put(ContentModel.PROP_DESCRIPTION, "The Description");
props.put(ContentModel.PROP_MANUFACTURER, "Canon");
NodeRepresentation updatedNode = nodesAPI.updateNodeCall(nodeId, new NodeBodyUpdate(props)).execute().body();
Assert.assertEquals(updatedNode.getProperties().get(ContentModel.PROP_DESCRIPTION), "The Description");
Assert.assertEquals(updatedNode.getProperties().get(ContentModel.PROP_MANUFACTURER), "Canon");

 

Rename Node

//Rename Node
NodeRepresentation renamedNode = nodesAPI.updateNodeCall(nodeId, new NodeBodyUpdate("renamed-file.txt")).execute().body();
Assert.assertEquals(renamedNode.getName(), "renamed-file.txt");

 

Change Owner

//Change owner
props = new LinkedTreeMap<>();
props.put("cm:owner", "gavinc");
NodeRepresentation ownerNode = nodesAPI.updateNodeCall(nodeId, new NodeBodyUpdate(props)).execute().body();
Assert.assertEquals(((LinkedTreeMap)ownerNode.getProperties().get("cm:owner")).get("id"), "gavinc");

 

Remove EXIF Aspect

//Remove Exif Aspects
List<String> aspectNames = new ArrayList<>();
aspectNames.add(ContentModel.ASPECT_TITLED);
aspectNames.add(ContentModel.ASPECT_AUDITABLE);
aspectNames.add("cm:ownable");
NodeRepresentation aspectNode = nodesAPI.updateNodeCall(nodeId, new NodeBodyUpdate(null, null, aspectNames)).execute().body();
Assert.assertTrue(aspectNode.getAspects().contains(ContentModel.ASPECT_TITLED));
Assert.assertTrue(aspectNode.getAspects().contains(ContentModel.ASPECT_AUDITABLE));
Assert.assertTrue(aspectNode.getAspects().contains("cm:ownable"));
Assert.assertFalse(aspectNode.getAspects().contains(ContentModel.ASPECT_EXIF));

 

Change Node type

//Change Node Type
NodeRepresentation changedTypeNode = nodesAPI.updateNodeCall(nodeId, new NodeBodyUpdate(null, "cm:savedquery", null, null)).execute().body();
Assert.assertEquals(changedTypeNode.getNodeType(), "cm:savedquery");

 

Update Content

//Update Content
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), "This is the initial content for the file.");
NodeRepresentation updatedContentNode = nodesAPI.updateNodeContentCall(nodeId, requestBody).execute().body();
Assert.assertEquals(updatedContentNode.getContent().getSizeInBytes(), 41);

 

Get Content

//Get Content
Call<ResponseBody> downloadCall = nodesAPI.getNodeContentCall(nodeId);
File dlFile = new File("W:\\", "dl.txt");
IOUtils.copyFile(downloadCall.execute().body().byteStream(), dlFile);

 

Delete Node

//Delete Node
Response<Void> deleteCall = nodesAPI.deleteNodeCall(nodeId).execute();
Assert.assertTrue(deleteCall.isSuccessful());

 

Permanently delete node

//Delete Node Permanently
Response<Void> deleteCall = nodesAPI.deleteNodeCall(nodeId, true).execute();
Assert.assertTrue(deleteCall.isSuccessful());

 

Alfresco Java Client SDK Series

We will now follow Gavin Cornwell v1 REST API - Part 3 - Creating Nodes  examples and see how we can achieve the same experience using the SDK

 

To make the exercise more concise we will execute each request in a synchronous way.

 

Important Notice

Alfresco Java Client is currently in Early Access mode. It evolves as you use them, as you give feedback, and as the developers update and add file. We like to think app & lib development as services that grow and evolve with the involvement of the community.

 

Prerequisites

In order to follow along you'll need an environment to do so, firstly download and install the 5.2.c Early Access Community Release. In our case we will consider Alfresco is available at http://localhost:8080/alfresco and the "admin" user is available and has "admin" as password.

 

 

Part 3 - Creating Nodes

 

Create folder

//Create Folder
NodeBodyCreate nodeBodyCreate = new NodeBodyCreate("My Folder", ContentModel.TYPE_FOLDER);
Response<NodeRepresentation> nodeResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, nodeBodyCreate).execute();
NodeRepresentation folder = nodeResponse.body();
Assert.assertEquals(folder.getName(), "My Folder");

 

Create empty file

//Create Empty File
NodeBodyCreate emptyFileBody = new NodeBodyCreate("my-file.txt", ContentModel.TYPE_CONTENT);
Response<NodeRepresentation> emptyNodeResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, emptyFileBody).execute();
NodeRepresentation emptyNode = emptyNodeResponse.body();
Assert.assertEquals(emptyNode.getContent().getSizeInBytes(), 0);

 

Create content

// Create Body
File file = new File("path/to/test.txt");
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), file);
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "test.txt", requestBody);
RequestBody fileRequestBody = multipartBuilder.build();

//Create Content
Response<NodeRepresentation> createdNodeResponse = nodesAPI
.createNodeCall(NodesAPI.FOLDER_MY, fileRequestBody).execute();

 

Create content with name

// Create Body
File file = new File("path/to/test.txt");
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), file);
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "test.txt", requestBody);
RequestBody fileRequestBody = multipartBuilder.build();

HashMap<String, RequestBody> map = new HashMap<>();
map.put("filedata", fileRequestBody);
map.put("name", RequestBody.create(MediaType.parse("multipart/form-data"), "my-name.txt"));

//Create Content
Response<NodeRepresentation> createdNodeResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, map).execute();

 

Create content with auto rename

File file = new File("path/to/test.txt");
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), file);
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "test.txt", requestBody);
RequestBody fileRequestBody = multipartBuilder.build();

HashMap<String, RequestBody> map = new HashMap<>();
map.put("filedata", fileRequestBody);
map.put("name", RequestBody.create(MediaType.parse("multipart/form-data"), "my-name.txt"));
map.put("autorename", RequestBody.create(MediaType.parse("multipart/form-data"), "true"));

//Create Content
Response<NodeRepresentation> createdNodeResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, map).execute();

 

Create content in relative path

// Create content in relative path
// Create Body
File file = new File("W:\\test.txt");
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), file);
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "test.txt", requestBody);
RequestBody fileRequestBody = multipartBuilder.build();

HashMap<String, RequestBody> map = new HashMap<>();
map.put("filedata", fileRequestBody);
map.put("name", RequestBody.create(MediaType.parse("multipart/form-data"), "my-file.txt"));
map.put("relativePath", RequestBody.create(MediaType.parse("multipart/form-data"), "/My Folder"));

//Create Content
Response<NodeRepresentation> createdNodeResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, map).execute();

 

Create content with rendition

// Create content with rendition
// Create Body
File file = new File("W:\\test.txt");
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), file);
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "test.txt", requestBody);
RequestBody fileRequestBody = multipartBuilder.build();
HashMap<String, RequestBody> map = new HashMap<>();
map.put("filedata", fileRequestBody);
map.put("renditions", RequestBody.create(MediaType.parse("multipart/form-data"), "doclib"));
//Create Content
Response<NodeRepresentation> createdNodeResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, map).execute();

 

Create content with properties

// Create content with properties
// Create Body
File file = new File("Path/To/test.txt");
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), file);
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "test.txt", requestBody);
RequestBody fileRequestBody = multipartBuilder.build();
HashMap<String, RequestBody> map = new HashMap<>();
map.put("filedata", fileRequestBody);
map.put("name", RequestBody.create(MediaType.parse("multipart/form-data"), "my-file.txt"));
map.put("autoRename", RequestBody.create(MediaType.parse("multipart/form-data"), "true"));
map.put(ContentModel.PROP_TITLE, RequestBody.create(MediaType.parse("multipart/form-data"), "The Title"));
map.put(ContentModel.PROP_DESCRIPTION, RequestBody.create(MediaType.parse("multipart/form-data"), "The Description"));
map.put(ContentModel.PROP_MANUFACTURER, RequestBody.create(MediaType.parse("multipart/form-data"), "Canon"));

//Create Content
Response<NodeRepresentation> createdNodeResponse = nodesAPI.createNodeCall(NodesAPI.FOLDER_MY, map).execute();

 

 

Alfresco Java Client SDK Series

Following the Alfresco Java Client SDK introduction in my last post it's now time to dive in and start using the SDK. We will follow Gavin Cornwell v1 REST API - Part 2 - Navigation  examples and see how we can achieve the same experience.

 

To make the exercise more concise we will execute each request in a synchronous way.

 

Important Notice

Alfresco Java Client is currently in Early Access mode. It evolves as you use them, as you give feedback, and as the developers update and add file. We like to think app & lib development as services that grow and evolve with the involvement of the community.

 

Prerequisites

In order to follow along you'll need an environment to do so, firstly download and install the 5.2.c Early Access Community Release. In our case we will consider Alfresco is available at http://localhost:8080/alfresco and the "admin" user is available and has "admin" as password.

 

Part 2 - Navigation 

To retrieve the children for a folder we need it's id, to help get started the Alfresco REST API have provided three aliases, -root-, -my- and -shared-. Those constant are available as constants with NodesAPI.FOLDER_ROOT, NodesAPI.FOLDER_MY and NodesAPI.FOLDER_SHARED

 

In this section we will retrieve a collection of node object. As mentionned in previous blog collection of objects are retrieved with the SDK as ResultPaging of NodeRepresentation.

 

Retrieve children

//List Root Children
Response<ResultPaging<NodeRepresentation>> rootChildrenResponse = nodesAPI.listNodeChildrenCall(NodesAPI.FOLDER_ROOT).execute();

//Check Response status
if (rootChildrenResponse.isSuccessful())
{
   //Iterate over the listing
   for (NodeRepresentation node : rootChildrenResponse.body().getList())
   {
     Assert.assertNotNull(node.getId());
   }
}

 

Retrieve children with properties and aspects

// List Root Children with Include Parameters
ResultPaging<NodeRepresentation> rootChildren = nodesAPI
   .listNodeChildrenCall(NodesAPI.FOLDER_ROOT, null, null, null, null,
          new IncludeParam(Arrays.asList("properties", "aspectNames")), null, null, null)
   .execute().body();

 

Retrieve child nodes 3 through 5

// Retrieve child nodes 3 through 5
Assert.assertEquals(nodesAPI.listNodeChildrenCall(NodesAPI.FOLDER_ROOT, 2, 3, null).execute().body()
.getPagination().getCount(), 3);

 

Retrieve files only

// Retrieve files only
Assert.assertEquals(nodesAPI
.listNodeChildrenCall(NodesAPI.FOLDER_ROOT, null, null, null, "(isFile=true)", null, null, null, null)
.execute().body().getPagination().getCount(), 0);

 

Retrieve Sites folder only

// Retrieve Sites folder only
Assert.assertEquals(nodesAPI.listNodeChildrenCall(NodesAPI.FOLDER_ROOT, null, null, null, "(nodeType=st:sites)",
null, null, null, null).execute().body().getList().get(0).getName(), "Sites");

 

Retrieve children ordered by name 

// Retrieve children ordered by name
Assert.assertEquals(nodesAPI
.listNodeChildrenCall(NodesAPI.FOLDER_ROOT, null, null, new OrderByParam(Arrays.asList("name ASC")))
.execute().body().getList().size(), 7);

 

Alfresco Java Client SDK Series

Alfresco Java Client SDK

Posted by jm.pascal Employee Nov 17, 2016

Introduction

Alfresco has recently focused a lot of time and energy to develop and strengthen its Public API. Gavin Cornwell talked a lot about it in his series of blog post 

 

With Alfresco Community Edition 201611 EA Release we are happy to announce a new project called Alfresco Java Client SDK available in Early Access Mode. 

 

This project contains Java lib project to consume easily Alfresco Public REST API. It include a set of APIs that allows developers to quickly build Alfresco-enabled Java & Android applications.

 

Installation

In your Java/Android application using Maven/Gradle simply add the following dependency: 

 

MAVEN

<dependency>
<groupId>org.alfresco.client</groupId>
<artifactId>alfresco-java-client</artifactId>
<version>1.0.0-beta1</version>
</dependency>

 

GRADLE

compile 'org.alfresco.client:alfresco-java-client:1.0.0-beta1'

 

You are now able to use the SDK.

 

Prerequisites

In order to follow along you'll need an environment to do so, firstly download and install the 5.2.c Early Access Community Release. In our case we will consider Alfresco is available at http://localhost:8080/alfresco and the "admin" user is available and has "admin" as password.

 

Overview

Setup Client Object

To use the SDK, the first thing to do is to create an AlfrescoClient object. This object is responsible to interact with Alfresco REST API via various specialised Services object. For those who already used Alfresco Android SDK the approach is very similar except the Alfresco Java Client SDK is compatible on both Java/Android platform.

 

To setup the Client there's a Builder where you can define parameters like Logging Level, HTTP Layer, Serialization/Deserialization, authentication mechanism etc... For the simplicity of this blog post we will use the quickest way.

AlfrescoClient client = new AlfrescoClient.Builder().connect("http://localhost:8080/alfresco", "admin", "admin").build();

 

 

Using Services

With AlfrescoClient you can now use services as they are defined in API Explorer

The following list illustrates what services are currently present.

//Retrieve user activities and site activities
ActivityStreamAPI activityAPI = client.getActivityStreamAPI();
//Retrieve and manage comments
CommentAPI commentAPI = client.getCommentsAPI();
//Retrieve Favorites information,
FavoritesAPI favoritesAPI = client.getFavoritesAPI();
//Retrieve and manage file/folder
NodesAPI nodesAPI = client.getNodesAPI();
//Retrieve user information,
PeopleAPI peopleAPI = client.getPeopleAPI();
//Retrieve and manage ratings
RatingsAPI ratingAPI = client.getRatingsAPI();
//Retrieve and manage sites informations
SitesAPI siteAPI = client.getSitesAPI();
//Retrieve and manage tags
TagAPI tagAPI = client.getTagsAPI();
//Retrieve and manage tags
RenditionsAPI renditionAPI = client.getRenditionsAPI();
//Retrieve and manage Shared Links
SharedLinksAPI sharedLinkAPI = client.getSharedLinksAPI();
//Retrieve and manage Trashcan
TrashcanAPI trashcanAPI = client.getTrashcanAPI();

 

Let's say we want to retrieve the root folder. In this case we need the NodesAPI

NodesAPI nodesAPI = client.getNodesAPI();

so now we can request the API to retrieve the list of children

Response<NodeRepresentation> rootFolderSyncResponse = nodesAPI.getNodeCall(NodesAPI.FOLDER_ROOT).execute();

Make a Request

You might not completely understand the preceding code snippet and it's normal. Indeed the main difference between Alfresco Android SDK and the Alfresco Java Client is how you create network request and interact with the remote Alfresco server.

 

The Java Client allows the developer to make a synchronous or asynchronous HTTP request via a Call Object (Thanks to Retrofit). Each service methods are Call Objects you can execute synchronously via execute() method or asynchronuously via enqueue() method. If we take our example we have something like

// Retrieve Root Node Info Synchronuously
Response<NodeRepresentation> rootFolderResponse = nodesAPI.getNodeCall(NodesAPI.FOLDER_ROOT).execute();

// Retrieve Root Node Info Asynchronuously
nodesAPI.getNodeCall(NodesAPI.FOLDER_ROOT).enqueue(new Callback<NodeRepresentation>()
{
   @Override
   public void onResponse(Call<NodeRepresentation> call, Response<NodeRepresentation> response)
   {
      //Retrieve Root Node Info
   }

   @Override
   public void onFailure(Call<NodeRepresentation> call, Throwable t)
   {
      //Something wrong happened
            }
});

An advantage with this approach is the possibility to support with only one lib two different platforms. For Android development network requests can't be done in the main thread and requires a mechanism to do it in another thread. In this case the SDK supports this use case. It also support the use case of a simple Java Batch file where we can execute everything in the main thread.

 

Retrieve the Response

The result Object after a Call execution is called a Response. This object contains all information received from the web server. It's here you can retrieve the HTTP Info like Response status with isSuccessful() method, HTTP Response via code() or HTTP Headers via headers().

If the Response is successful you can retrieve the model object associated with body() i.e in our case the NodeRepresentation.

If the Response is not successful error information are available via errorBody() method.

 

Collections and model Object

The Alfresco REST API has a specific format to handle collection like a list of Nodes, list of comments, list of sites... Each collection item is represented within an entry object which is contained in an entries array. In the SDK we removed this notion of entries/entry and replace it with ResultPaging/Object.

 

Object or model Object is the representation of the REST API Response. Very often it's the Json data parsed into Plain Old Java Object (POJO).

ResultPaging contains information about the listing i.e pagination, count and list of objects like  ResultPaging<NodeRepresentation> or ResultPaging<CommentRepresentation>

 

As you can see with this approach we have access to both the HTTP Information AND Model Object. The Client SDK is responsible to create all those final, ready to use objects for you without having to parse it.

 

An extra thing

And that's not all. Thanks again to Retrofit the Java Client SDK is able to interact with Alfresco REST API in Reactive way using RxJava. In this case instead of using Call Object we use Observable Object in RxJava. Our example become something like 

// Retrieve Root Node Info in Reactive Way
nodesAPI.getNodeObservable(NodesAPI.FOLDER_ROOT)
.subscribe(root -> Assert.assertEquals(root.getName(), "Company Home"));

 

Conclusion

As you saw during this post the main goals of this SDK is to let the developer choose the best approach to interact with Alfresco REST API regarding its platform and requirements. Our goal is to create the choice, make it easy to use and let you play with it.

 

 

More Usage

With the following Blog Posts we will follow Gavin Cornwell  examples (as listed in the introduction) and see how we can achieve the same experience using the Java Client SDK.

 

davidcognite

RM Pull Request Process

Posted by davidcognite Nov 14, 2016

Alfresco Records Management is now able to accept Pull Requests to our GitHub project. Following the success that Dave Draper and the Aikau project has had in receiving community contributions, the RM team was keen to get the process sorted out to enable us to accept pull requests from our GitHub mirror. We accepted our first pull request from the community last week: Fix for Dynamic Extensions by cetra3 · Pull Request #2 · Alfresco/records-management · GitHub  - thanks to Aussie partner Parashift for sending that one in.

 

As explained previously, we use a mirror script to create a public copy of our community code base. This script now pushes any PRs made in GitHub back to our internal Git server and creates a bamboo branch for us to review the code from.

 

As part of this change, our scrum master Christine Thompson has opened up the RM JIRA project so that we can create issues that are publicly readable without an account, which we feel better matches the openness of working with Community members on pull requests.

 

So, if you find an issue you'd like to fix:

1) Raise a JIRA when you discover the issue (it'll be invisible after you create it as RM tickets default to "Internal" access only, but we'll open up the access when we triage it).

2) Send us a Pull Request.

3) Our build servers will pick up the change and run our automated tests on it.

4) We'll try to review the code as quickly as possible (how long that takes depends on the size of the change, severity of the bug and balancing it with other priorities), but will keep JIRA and the PR discussion updated.

 

If you're planning on starting work on any significant code submission, it might be worth checking the approach with us first to avoid disappointment - we may already be working on a fix or have a refactor of that area of the code planned. It's likely that we'll also need to get a contributor agreement signed.

 

I'm eagerly awaiting the next PR; if you could change any code in the RM module, what would it be and why?

In the last post we looked at how to retrieve, update and delete nodes, this time we're going to concentrate on versioning.

 

 

As always there is a Postman collection to accompany this post. To import the collection click on the "Run in Postman" button below.

 

Let's start by creating an empty file like we did in the last post by POSTing the following body to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-/children (1st request in the Postman collection):

 

{
  "name": "version.txt",
  "nodeType": "cm:content"
}

 

If you examine the response below you'll notice there's no mention of versioning at all, that's because empty files are created without versioning enabled.

Note: If you want to create content with versioning enabled by default use multipart/form-data instead of JSON, refer back to part 3 or see http://localhost:8080/api-explorer/#!/nodes/addNode for details.

{
  "entry": {
    "aspectNames": [
      "cm:auditable"
    ],
    "createdAt": "2016-11-10T21:35:04.389+0000",
    "isFolder": false,
    "isFile": true,
    "createdByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "modifiedAt": "2016-11-10T21:35:04.389+0000",
    "modifiedByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "name": "version.txt",
    "id": "fa100bae-9903-44e4-9e19-5a8523be7422",
    "nodeType": "cm:content",
    "content": {
      "mimeType": "text\/plain",
      "mimeTypeName": "Plain Text",
      "sizeInBytes": 0,
      "encoding": "UTF-8"
    },
    "parentId": "bd8f1283-3e84-4585-aafc-12da26db760f"
  }
}

 

Now let's update the content like we did in the last post but this time also provide the majorVersion and comment query parameters.

 

To do this, PUT the body below with a Content-Type of text/plain to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/fa100bae-9903-44e4-9e19-5a8523be7422/content?majorVersion=true&comment=First version (2nd request in Postman collection):

Note: You'll obviously need to replace fa100bae-9903-44e4-9e19-5a8523be7422 with the id from your response throughout this post.

This is the initial content for the file.

 

This should return a response similar to the one below:

 

{
  "entry": {
    "isFile": true,
    "createdByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "modifiedAt": "2016-11-10T21:48:28.014+0000",
    "nodeType": "cm:content",
    "content": {
      "mimeType": "text\/plain",
      "mimeTypeName": "Plain Text",
      "sizeInBytes": 41,
      "encoding": "ISO-8859-1"
    },
    "parentId": "bd8f1283-3e84-4585-aafc-12da26db760f",
    "aspectNames": [
      "cm:versionable",
      "cm:titled",
      "cm:auditable",
      "cm:author"
    ],
    "createdAt": "2016-11-10T21:35:04.389+0000",
    "isFolder": false,
    "modifiedByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "name": "version.txt",
    "id": "fa100bae-9903-44e4-9e19-5a8523be7422",
    "properties": {
      "cm:versionLabel": "1.0",
      "cm:versionType": "MAJOR"
    }
  }
}

 

You can see from the response that the cm:versionable aspect has been applied (line 18) and two version properties have been set (lines 32 and 33), cm:versionLabel and cm:versionType.

 

Now versioning is enabled we can retrieve the version history for the file by using the URL http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/fa100bae-9903-44e4-9e19-5a8523be7422/versions (3rd request in the Postman collection):

 

{
  "list": {
    "pagination": {
      "count": 1,
      "hasMoreItems": false,
      "totalItems": 1,
      "skipCount": 0,
      "maxItems": 100
    },
    "entries": [
      {
        "entry": {
          "isFolder": false,
          "isFile": true,
          "modifiedAt": "2016-11-10T21:48:28.014+0000",
          "modifiedByUser": {
            "id": "test",
            "displayName": "Test Test"
          },
          "name": "version.txt",
          "versionComment": "First version",
          "id": "1.0",
          "nodeType": "cm:content",
          "content": {
            "mimeType": "text\/plain",
            "mimeTypeName": "Plain Text",
            "sizeInBytes": 41,
            "encoding": "ISO-8859-1"
          }
        }
      }
    ]
  }
}

 

We can see from the response above that there's only one version right now so let's create another one. This time though we'll create a minor version by using http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/fa100bae-9903-44e4-9e19-5a8523be7422/content?majorVersion=false&comment=Second version (4th request in Postman collection) and setting the content to:

 

This is the second version of the content, v1.1.

Now retrieve the version history again and you should see a response like the one shown below. This is also where you'll see the comments we specified (lines 21 and 42) when creating new versions.

 

{
  "list": {
    "pagination": {
      "count": 2,
      "hasMoreItems": false,
      "totalItems": 2,
      "skipCount": 0,
      "maxItems": 100
    },
    "entries": [
      {
        "entry": {
          "isFolder": false,
          "isFile": true,
          "modifiedAt": "2016-11-10T22:03:11.658+0000",
          "modifiedByUser": {
            "id": "test",
            "displayName": "Test Test"
          },
          "name": "version.txt",
          "versionComment": "Second version",
          "id": "1.1",
          "nodeType": "cm:content",
          "content": {
            "mimeType": "text\/plain",
            "mimeTypeName": "Plain Text",
            "sizeInBytes": 48,
            "encoding": "ISO-8859-1"
          }
        }
      },
      {
        "entry": {
          "isFolder": false,
          "isFile": true,
          "modifiedAt": "2016-11-10T21:48:28.014+0000",
          "modifiedByUser": {
            "id": "test",
            "displayName": "Test Test"
          },
          "name": "version.txt",
          "versionComment": "First version",
          "id": "1.0",
          "nodeType": "cm:content",
          "content": {
            "mimeType": "text\/plain",
            "mimeTypeName": "Plain Text",
            "sizeInBytes": 41,
            "encoding": "ISO-8859-1"
          }
        }
      }
    ]
  }
}

 

As you'd expect, if we retrieve the content for the node using http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/fa100bae-9903-44e4-9e19-5a8523be7422/content (5th request in Postman collection) we get:

 

This is the second version of the content, v1.1.


We can still get the content of the initial version though, to do this we have to use http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/fa100bae-9903-44e4-9e19-5a8523be7422/versions/1.0/content (6th request in the Postman collection), this gives us our original content:

 

This is the initial content for the file.


Imagine we change our mind and decide we want to revert to a previous version. To revert to version 1.0 we have to POST the following body to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/fa100bae-9903-44e4-9e19-5a8523be7422/versions/1.0/revert (7th request in the Postman collection):

 

{
  "majorVersion": true,
  "comment": "Reverted to original"
}


We are able to specify whether the reverted version will create a new minor or major version and again provide a comment describing the reason for the additional version.

 

If you get the content now it should be back to what it was when we originally created the file and if you get the version history you'll see we now have an extra version, a 2.0.

 

Now, what if you wanted to make some changes to a file and not let anyone else make changes until you've finished?

 

For this situation we can lock the file by POSTing an empty JSON object (see below) to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/fa100bae-9903-44e4-9e19-5a8523be7422/lock (8th request in the Postman collection).

 

{}


This results in the response below, which shows the node has been locked (lines 31, 32 and 38).

 

{
  "entry": {
    "isFile": true,
    "createdByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "modifiedAt": "2016-11-10T22:07:41.857+0000",
    "nodeType": "cm:content",
    "content": {
      "mimeType": "text\/plain",
      "mimeTypeName": "Plain Text",
      "sizeInBytes": 41,
      "encoding": "ISO-8859-1"
    },
    "parentId": "bd8f1283-3e84-4585-aafc-12da26db760f",
    "aspectNames": [
      "cm:versionable",
      "cm:lockable",
      "cm:auditable"
    ],
    "createdAt": "2016-11-10T21:35:04.389+0000",
    "isFolder": false,
    "modifiedByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "name": "version.txt",
    "id": "fa100bae-9903-44e4-9e19-5a8523be7422",
    "properties": {
      "cm:lockType": "WRITE_LOCK",
      "cm:lockOwner": {
        "id": "test",
        "displayName": "Test Test"
      },
      "cm:versionType": "MAJOR",
      "cm:versionLabel": "2.0",
      "cm:lockLifetime": "PERSISTENT"
    }
  }
}

 

There are a few options when using lock, see http://localhost:8080/api-explorer/#!/nodes/lockNode for more details. It's also possible to include an isLocked property when retrieving a node or a children listing so that your client does not need to parse these properties.

 

As the owner of the lock we can make changes to the file including the content, use the same update content URL we used earlier (2nd or 4th request in the Postman collection) to update the content and generate a new version.

 

However, if you try the same request as another user you'll get a 409 Conflict error response.

 

To unlock the file once you're done with your changes you can POST an empty JSON object (see below) to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/fa100bae-9903-44e4-9e19-5a8523be7422/unlock (9th request in the Postman collection)

 

{}


We've almost covered all the functionality provided by the /nodes API, next time we'll look at the last remaining area, associations.

In the last post we looked at how to create files and folders in the repository, this time we're going to retrieve and update node information, retrieve and update content and remove nodes from the repository.

 

To keep with tradition, all of the endpoints we'll cover in this post have been provided in a Postman collection and can be imported by clicking on the "Run in Postman" button below.

 

button.svg

 

There is something a little different about this one though, it uses the testing capabilities of Postman. After the create request is executed some JavaScript is run to grab the id of the newly created file and store it in a global variable. The URL of subsequent requests in the collection then refer to the global variable using Postman's {{variable}} syntax. Explaining the full testing capabilities of Postman is beyond the scope of this blog post so I'll leave that as an exercise for the reader!

 

OK, let's start by creating an empty file. POST the body below using a Content-Type of application/json to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-/children

 

{
  "name": "content.txt",
  "nodeType": "cm:content",
  "properties": {
    "cm:title": "The Title"
  }
}

 

Copy the value of the id property from the resulting response (or use the first request in the Postman collection).

 

Now let's retrieve some information about this node. We do this by doing a GET on http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/8c37b5b7-b96f-49ef-817e-9808bf2309f9 (you'll obviously need to replace the id with the one you copied from your response or use the 2nd request in the Postman collection). This results in the following response:

 

{
  "entry": {
    "isFile": true,
    "createdByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "modifiedAt": "2016-11-01T14:57:24.388+0000",
    "nodeType": "cm:content",
    "content": {
      "mimeType": "text\/plain",
      "mimeTypeName": "Plain Text",
      "sizeInBytes": 0,
      "encoding": "UTF-8"
    },
    "parentId": "bd8f1283-3e84-4585-aafc-12da26db760f",
    "aspectNames": [
      "cm:titled",
      "cm:auditable"
    ],
    "createdAt": "2016-11-01T14:57:24.388+0000",
    "isFolder": false,
    "modifiedByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "name": "my-file.txt",
    "id": "8c37b5b7-b96f-49ef-817e-9808bf2309f9",
    "properties": {
      "cm:title": "The Title"
    }
  }
}

 

All the data returned by the /nodes/{id}/children endpoint we examined in the second post is present plus the node's properties (line 29) and a list of aspect names (line 17). As we saw with create in the previous post this endpoint is also following our "performance first" principle. If we wanted to also determine whether the node represents a link and see it's full path, we can use the include query parameter to ask for this data, for example (3rd request in the Postman collection):

 

 

http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/8c37b5b7-b96f-49ef-817e-9808bf2309f9?include=isLink,path

 

{
  "entry": {
    "isLink": false,
    "isFolder": false,
    "isFile": true,
    "path": {
      "name": "/Company Home/User Homes/test",
      "isComplete": true,
      "elements": [
        {
          "id": "03acc816-b42f-4d87-ab1f-4d4ae16e73ef",
          "name": "Company Home"
        },
        {
          "id": "fb402fa3-3a59-446e-a69e-a1c769b62281",
          "name": "User Homes"
        },
        {
          "id": "bd8f1283-3e84-4585-aafc-12da26db760f",
          "name": "test"
        }
      ]
    },
    ...
  }
}

 

Take a look at the OpenAPI specification http://localhost:8080/api-explorer/#!/nodes/getNode for other additional data you can request.

 

Let's now turn our attention to updating a node. We have decided to implement partial update via PUT (although technically this is not RESTful we feel it's worth bending the rules here to keep things as simple as possible for clients) meaning the client only needs to send the data that is changing, with one exception, that we'll come to shortly.

 

To set some properties we use the same base URL (4th request in the Postman collection) of http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/8c37b5b7-b96f-49ef-817e-9808bf2309f9 but PUT a body using a Content-Type of application/json.

 

{
  "properties":
  {
    "cm:description": "The Description",
    "exif:manufacturer": "Canon"
  }
}

 

The response shows the state of the updated node, note that the exif aspect has also been added automatically (line 20).

 

{
  "entry": {
    "isFile": true,
    "createdByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "modifiedAt": "2016-11-02T00:40:51.931+0000",
    "nodeType": "cm:content",
    "content": {
      "mimeType": "text\/plain",
      "mimeTypeName": "Plain Text",
      "sizeInBytes": 0,
      "encoding": "UTF-8"
    },
    "parentId": "bd8f1283-3e84-4585-aafc-12da26db760f",
    "aspectNames": [
      "cm:titled",
      "cm:auditable",
      "exif:exif"
    ],
    "createdAt": "2016-11-01T14:57:24.388+0000",
    "isFolder": false,
    "modifiedByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "name": "my-file.txt",
    "id": "8c37b5b7-b96f-49ef-817e-9808bf2309f9",
    "properties": {
      "cm:title": "The Title",
      "exif:manufacturer": "Canon",
      "cm:description": "The Description"
    }
  }
}

 

PUT can also be used to rename by just providing a cm:name property in the properties as shown below:

 

{
  "properties":
  {
    "cm:name": "renamed-name.txt"
  }
}

 

Alternatively, the top level name property can also be used (5th request in the Postman collection):

 

{
  "name": "renamed-file.txt"
}

 

Similarly, the owner of the node can be updated, just provide the cm:owner property as follows (6th request in the Postman collection):

 

{
  "properties":
  {
    "cm:owner": "gavinc"
  }
}

 

As mentioned earlier there is one exception to the partial update rule and that is for managing aspects. To change the aspects applied to a node the whole complete array has to be provided. Any aspects the node has applied but are not present in the array will be removed. Conversely, any aspects in the array that the node does not have applied are added.

 

To remove the exif aspect from the node we created earlier PUT the following body (7th request in the Postman collection):

 

{
  "aspectNames": [
    "cm:titled",
    "cm:ownable",
    "cm:auditable"
  ]
}

 

Finally, the type of the node can also be changed by updating the nodeType property, for example to change our node type to cm:savedquery use the following body (8th request in the Postman collection):

 

{
  "nodeType": "cm:savedquery"
}

 

In the examples above we've used a file, everything we went through can obviously also be done for folders.

 

Let's now turn our attention to the actual content. At the start of the post we created an empty text file, you can see this via the content property:

 

"content": {
  "mimeType": "text/plain",
  "mimeTypeName": "Plain Text",
  "sizeInBytes": 0,
  "encoding": "UTF-8"
}

 

To set some plain text content do a PUT against http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/8c37b5b7-b96f-49ef-817e-9808bf2309f9/content using a Content-Type of text/plain and the following body (9th request in the Postman collection):

 

This is the initial content for the file.

 

The response will show the content has been updated and the encoding set accordingly:

 

{
  "entry": {
     ....
    "content": {
      "mimeType": "text/plain",
      "mimeTypeName": "Plain Text",
      "sizeInBytes": 41,
      "encoding": "ISO-8859-1"
    }
  }
}

 

The PUT endpoint accepts any binary stream so we could also use Postman to choose a file to upload as shown in the screenshot below:

 

Screen_Shot_2016-11-02_at_08_10_27.png

 

To retrieve the content simply use a GET against the content URL (10th request in the Postman collection):

 

http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/8c37b5b7-b96f-49ef-817e-9808bf2309f9/content

 

The last thing we're going to cover in this post is deleting. To delete the file we just created use the DELETE method against the URL we've been using throughout this post (11th request in the Postman collection), for example:

 

DELETE http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/8c37b5b7-b96f-49ef-817e-9808bf2309f9

 

This will actually perform a soft delete, the node gets moved to the trash can so it can be restored if necessary, we'll cover the trash can endpoints in a future post. If you want to take a look before then have a look through the documentation.

 

To permanently delete the node i.e. skip the trash can, use the permanent query parameter set to true, for example (12th request in the Postman collection):

 

DELETE http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/8c37b5b7-b96f-49ef-817e-9808bf2309f9?permanent=true

 

Hopefully you've found these posts useful so far, if there's anything I can improve to make things easier or any other suggestions, please let me know via comments.

 

In the next post we're going to start looking into some more advanced topics, starting with versioning and locking.

Extending Aikau Widgets

Posted by ddraper Nov 1, 2016

Introduction

I'm very keen to understand the pain points of using Aikau for customizing Alfresco Share and for creating standalone clients. I recently got some very valuable feedback from an Alfresco Community member suggesting that one area that I could try to describe better would be in extending existing Aikau widgets. I'm always very want to write posts that help people overcome any issues so I've written this to try and help people understand the various options available to them - the Aikau tutorial provides a chapter on creating new widgets but doesn't have any information on extending them.

 

Custom Styling

The specific use case that was raised was the issue of customizing the CSS used for a widget. Ideally we aim to make it possible to customize the styling of widgets through LESS variables in the Alfresco Share themes. Aikau provides a set of default styling variables that are referenced by the widgets covering attributes such as:

  • fonts (colour, size, family, etc)
  • borders
  • colours
  • spacing (horizontal and vertical margins and padding, line heights, etc)

...and the CSS files associated with a widget will reference these variables. This means that it is possible to customize styling by editing or creating an Alfresco Share theme as shown in this example.

 

An example use of these variables is shown here (taken from AlfBreadcrumb.css) - see the use of @link-font-color, @link-text-decoration and @breadcrumb-background-color - these are all examples of using the LESS variables.

 

.alfresco-documentlibrary-AlfBreadcrumb a {
   color: @link-font-color;
   text-decoration: @link-text-decoration; 
   padding: 5px 15px 5px 30px;
   background: @breadcrumb-background-color;
   position: relative; 
   display: block;
   float: left;
   cursor: pointer;
   height: 20px;
   &:hover {
      colour: @link-font-color-hover;
      text-decoration: @link-text-decoration-hover;
   }
}

 

It is always worth referencing the LESS variables in the CSS files of your custom widgets - and if you find that an Aikau widget is not making use of a variable or you feel that styling customization would be easier if a variable was used then please raise an issue.

 

Extending to Add Custom Styles

If it is not possible to set a LESS variable in your Alfresco Share theme to achieve your desired styling then you can always extend a widget and provide additional CSS files. The Surf framework that Aikau uses to build the resources required for each page will parse the source of each widget to identify the CSS dependencies that need to be included (as described in this blog post). Therefore in order to provide additional dependencies it is necessary to include them in a widget source file.

 

Let's say you want to extend add in a new CSS dependency for the alfresco/menus/AlfMenuItem widget - the perfect example would be the alfresco/headerAlfMenuItem as it does exactly this - the entire module looks like this (with the JSDoc removed)

 

define(["dojo/_base/declare",
        "alfresco/menus/AlfMenuItem"],
        function(declare, AlfMenuItem) {

   return declare([AlfMenuItem], {

      cssRequirements: [
         {cssFile:"./css/AlfMenuItem.css"}
      ]
   });
});

 

The main thing that this widget does is to provide a new CSS dependency that changes the styling of the menu item to be the style that you see in the Share header (i.e. with a black background) this image shows the difference:

 

ExtendingWidgetsBlogPost1.png

The functionality provided by alfresco/menus/AlfMenuItem is retained - all alfresco/header/AlfMenuItem does is to restyle the widget.

 

Extending Widget Functionality

The other major reason for extending is to either add or change the functionality of an existing widget. Aikau strives to not only be backwards compatible itself but to also ensure that 3rd party customizations (including extended widgets) continue to work against new releases. For that reason we will never remove a function from a widget (except when moving to a new major version and only then if the function has been previously deprecated - you can find a list of all deprecations in the release notes).

 

As well as this we use special annotations in the widgets to indicate which functions can be overridden (that is be completely replaced) or extended (be replaced but with a call to this.inherited(arguments) to call the inherited behaviour). Functions that can be overridden will be marked with @overrideable and those than can be extended will be marked with @extendable and these annotation will show up in the JSDoc as a "Support" statement as shown:

 

ExtendingWidgetsBlogPost2.png

 

As a general rule we don't automatically add these annotations to all functions - but if you have a clear requirement for a function to be supported in future releases then you should raise and issue to let us know and we will add an annotation if we feel that it is something that can be supported going forwards.

 

Extending a function in a widget is just a case of adding a function of the same name. If you want to invoke the function inherited from the extended widget then simply call this.inherited(arguments) - if you wish to change the arguments that are passed then you should simply include an array of new arguments as an extra argument, for example the DateRange widget extends DateTextBox and extends the setValue function but passes different arguments, as shown here:

 

setValue: function alfresco_forms_controls_DateRange__setValue(value) {
   if (value)
   {
      var valueTokens = value.split(this.dateSeparator);
      this.inherited(arguments, [valueTokens[0]]);
      this.toDate.setValue(valueTokens[1]);
   }
   else
   {
      this.inherited(arguments);
   }
}

 

Widget Life-cycle Functions

Aikau widgets extend from the dijit/_WidgetBase module and the main thing you need to know about this that the postMixInProperties function is called before the HTML template is processed and the postCreate function is called afterwards. These are the 2 key life-cycle functions that you will find yourself extending and in almost all cases you will want to call this.inherited(arguments) to retain the default functionality. The default functions provided by dijit/_WidgetBase are pure life-cycle hooks, but in most cases they will have been implemented by the Aikau widget that you are extending.

 

Summary (so far!)

This is an initial pass at providing some information on extending Aikau widgets - however, if there are use cases that you'd like me to elaborate on further then please let me know (via the comments section) and I will make further updates.

Filter Blog

By date: By tag: