Skip navigation
All Places > Alfresco Content Services (ECM) > Blog > 2017 > April
2017

It’s been a while since our last release of Community RM, but 2.5.b is now available.

 

What's in 2.5.b

 

The main driver for this release is compatibility with Alfresco Community 201702 (i.e. Alfresco 5.2.x). This involved some work to integrate with the shiny new site creation page as well as a few less visible compatibility fixes. The release also contains a lot of bug fixes, particularly around retention schedules, audit and rules.

 

Create site dialog with RM 2.5.b

 

Internally the main innovation has been to create a build which just tests Community behaviour. We have a lot of tests for our Enterprise artefacts (which include the community code too), but in order to allow us to make Community releases more frequently we have set up a new build to look for regressions in our community code.

 

We have also set up a publicly accessible Travis build to make it easier for anyone to see the status the current code, as well as any pull requests they submit.

 

What’s coming up next?

 

On master you’ll see all our current community development, which includes a large focus on the first release of the governance services v1 REST API. It’s been through a couple of significant refactorings since we started work on it, but we think it’s nearing completion now. The current plan is to release it in 2.6.a, which should be in roughly a month if all goes well.

 

You might also have noticed a new module in the project – rm-benchmark. We’ve recently been investigating our performance at scale, and the first part of that is to benchmark the performance of our core capabilities. See RM-3953 for more details.

 

So please let us know what you think of 2.5.b and what you'd like to see in 2.6.a and beyond.

 

Links

 

Our journey through the v1 REST APIs in 5.2 is nearly at an end, last time we looked at the trashcan API, in this part we're going to look at arguably one of the most important APIs, the discovery API.

 

As there is only one GET endpoint in this API there is no Postman collection for this post as you can follow along with this post using just a browser.

 

The discovery API is available at http://localhost:8080/alfresco/api/discovery and as the name suggests it provides information on the repository, such as version number, feature status and which modules are installed.

 

We recommend that clients call this API as early as possible in their lifecycle so that it's features can be sensitive to the repository's capabilities. A good example is the ability the manage shared links, this feature can be disabled on the server, in this scenario the /shared-links endpoints will return a 501 status code, if this is not handled by the UI it does not provide a very good user experience!

 

Calling this API on my repository (installed using the Community installer) returns the following response:

{
  "entry": {
    "repository": {
      "edition": "Community",
      "version": {
        "major": "5",
        "minor": "2",
        "patch": "0",
        "hotfix": "0",
        "schema": 10005,
        "label": "r135134-b14",
        "display": "5.2.0.0 (r135134-b14) schema 10005"
      },
      "status": {
        "isReadOnly": false,
        "isAuditEnabled": true,
        "isQuickShareEnabled": true,
        "isThumbnailGenerationEnabled": true
      },
      "modules": [
        {
          "id": "alfresco-aos-module",
          "title": "Alfresco Office Services Module",
          "description": "Allows applications that can talk to a SharePoint server to talk to your Alfresco installation",
          "version": "1.1.5",
          "installDate": "2017-02-20T10:52:26.618+0000",
          "installState": "INSTALLED",
          "versionMin": "5.1",
          "versionMax": "999"
        },
        {
          "id": "org.alfresco.integrations.google.docs",
          "title": "Alfresco / Google Docs Integration",
          "description": "The Repository side artifacts of the Alfresco / Google Docs Integration.",
          "version": "3.0.3",
          "installDate": "2017-02-20T10:52:27.288+0000",
          "installState": "INSTALLED",
          "versionMin": "5.0.0",
          "versionMax": "5.99.99"
        },
        {
          "id": "alfresco-share-services",
          "title": "Alfresco Share Services AMP",
          "description": "Module to be applied to alfresco.war, containing APIs for Alfresco Share",
          "version": "5.2.0",
          "installDate": "2017-02-20T10:52:27.779+0000",
          "installState": "INSTALLED",
          "versionMin": "5.1",
          "versionMax": "999"
        },
        {
          "id": "alfresco-trashcan-cleaner",
          "title": "alfresco-trashcan-cleaner project",
          "description": "The Alfresco Trash Can Cleaner (Alfresco Module)",
          "version": "2.2",
          "installState": "UNKNOWN",
          "versionMin": "0",
          "versionMax": "999"
        },
        {
          "id": "fdk-custom-model-module",
          "title": "FDK Custom Model Module",
          "description": "Packages the FDK custom model as a simple module",
          "version": "1.0-SNAPSHOT",
          "installState": "UNKNOWN",
          "versionMin": "0",
          "versionMax": "999"
        }
      ]
    }
  }
}

 

Although this has been the shortest post in this series so far, hopefully you can see the value and importance of this small, simple API.

 

There is one final post in this series to come where we'll cover some topics that apply to all APIs and highlight some capabilities of the API you might not have known were there!

We will now follow Gavin Cornwell  v1 REST API - Part 11 - Trashcan   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 File 

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", "content-to-be-deleted.txt", requestBody);
RequestBody fileRequestBody = multipartBuilder.build();
NodeRepresentation nodeToDelete = client.getNodesAPI().createNodeCall(NodesAPI.FOLDER_MY, fileRequestBody).execute().body();

Delete File 

client.getNodesAPI().deleteNodeCall(nodeToDelete.getId()).execute();
        Assert.assertFalse(client.getNodesAPI().getNodeCall(nodeToDelete.getId()).execute().isSuccessful());

 

List deleted nodes  

TrashcanAPI trashcanAPI = client.getTrashcanAPI();
ResultPaging<DeletedNodeRepresentation> deletedNodes = trashcanAPI.listDeletedNodesCall().execute().body();
Assert.assertTrue(deletedNodes.getCount() > 10);

 

Deleted node details 

DeletedNodeRepresentation deletedNodeInfo = trashcanAPI.getDeletedNodeCall(nodeToDelete.getId()).execute().body();
Assert.assertEquals(deletedNodeInfo.getId() , nodeToDelete.getId());

 

Restore node 

NodeRepresentation restoredNode = trashcanAPI.restoreDeletedNodeCall(nodeToDelete.getId(), null).execute().body();
Assert.assertEquals(restoredNode.getId() , nodeToDelete.getId());
Assert.assertTrue(client.getNodesAPI().getNodeCall(nodeToDelete.getId()).execute().isSuccessful());

 

Permanently delete node  

client.getNodesAPI().deleteNodeCall(nodeToDelete.getId()).execute();
Response<Void> purgedNodeResponse = trashcanAPI.purgeDeletedNodeCall(restoredNode.getId()).execute();
Assert.assertTrue(purgedNodeResponse.isSuccessful());
Assert.assertFalse(client.getNodesAPI().getNodeCall(nodeToDelete.getId()).execute().isSuccessful());

 

Alfresco Java Client SDK Series

We will now follow Gavin Cornwell  v1 REST API - Part 10 - People   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 person 

//Create Person
PeopleAPI peopleAPI = client.getPeopleAPI();

PersonBodyCreate bodyCreate = new PersonBodyCreate("jdoe").firstName("John").lastName("Doe")
          .email("john.doe@example.com").password("jdoe").skypeId("johndoe_skype").jobTitle("Software Engineer");

PersonRepresentation personRepresentation = peopleAPI.createPersonCall(bodyCreate).execute().body();
Assert.assertEquals(personRepresentation.getId(), "jdoe");
Assert.assertEquals(personRepresentation.getFirstName(), "John");
Assert.assertEquals(personRepresentation.getLastName(), "Doe");
Assert.assertEquals(personRepresentation.getEmail(), "john.doe@example.com");
Assert.assertEquals(personRepresentation.getSkypeId(), "johndoe_skype");
Assert.assertEquals(personRepresentation.getJobTitle(), "Software Engineer");

 

List people

ResultPaging<PersonRepresentation> personList = peopleAPI.listPeopleCall().execute().body();
Assert.assertEquals(personList.getCount(), 7);

 

Find people

ResultPaging<PersonRepresentation> searchPersonList = client.getQueriesAPI().findPeopleCall("jdoe").execute().body();
Assert.assertEquals(searchPersonList.getCount(), 1);
Assert.assertEquals(searchPersonList.getList().get(0).getId(), "jdoe");

 

Person Details

PersonRepresentation jdoeDetails = peopleAPI.getPersonCall("jdoe").execute().body();
Assert.assertEquals(jdoeDetails.getId(), "jdoe");
Assert.assertEquals(jdoeDetails.getFirstName(), "John");
Assert.assertEquals(jdoeDetails.getLastName(), "Doe");
Assert.assertEquals(jdoeDetails.getEmail(), "john.doe@example.com");
Assert.assertEquals(jdoeDetails.getSkypeId(), "johndoe_skype");
Assert.assertEquals(jdoeDetails.getJobTitle(), "Software Engineer");

 

Update person details

PersonBodyUpdate bodyUpdate = new PersonBodyUpdate().firstName("Johnathon").mobile("07000 123456");

PersonRepresentation updatedPerson = peopleAPI.updatePersonCall("jdoe", bodyUpdate, null).execute().body();
Assert.assertEquals(updatedPerson.getMobile(), "07000 123456");
Assert.assertEquals(updatedPerson.getFirstName(), "Johnathon");

 

Change password

PersonBodyUpdate changePassword = new PersonBodyUpdate().oldPassword("jdoe").password("my-new-password");

PersonRepresentation updatedPPerson = peopleAPI.updatePersonCall("jdoe", changePassword, null).execute().body();

 

Disable person

PersonBodyUpdate disablePersonBody = new PersonBodyUpdate().enabled(false);

PersonRepresentation disablePerson = peopleAPI.updatePersonCall("jdoe", disablePersonBody, null).execute().body();
Assert.assertEquals(disablePerson.isEnabled(), Boolean.FALSE);

 

 

 

Alfresco Java Client SDK Series

We will now follow Gavin Cornwell  v1 REST API - Part 9 - Queries & Search   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 Public Site

//Create a public Site
SitesAPI sitesAPI = client.getSitesAPI();

//Create public site
SiteBodyCreate siteBodyCreate = new SiteBodyCreate("queriesSearchSite", "Queries and Search Site", "Site created for queries and search blog post", SiteVisibilityEnum.PUBLIC);
Response<SiteRepresentation> siteRepresentationResponse = sitesAPI.createSiteCall(siteBodyCreate).execute();
SiteRepresentation siteRepresentation = siteRepresentationResponse.body();
Assert.assertEquals(siteRepresentation.getId(), "queriesSearchSite");
Assert.assertEquals(siteRepresentation.getVisibilityEnum(), SiteVisibilityEnum.PUBLIC);

 

Retrieve document library container

Response<SiteContainerRepresentation> doclibContainerResponse = sitesAPI.getSiteContainerCall("queriesSearchSite", "documentLibrary").execute();
SiteContainerRepresentation doclibContainer = doclibContainerResponse.body();
Assert.assertEquals(doclibContainer.getFolderId(), "documentLibrary");

 

Upload File (text)

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();
client.getNodesAPI().createNodeCall(doclibContainer.getId(), fileRequestBody).execute();

 

Upload File (Image)

file = new File("W:\\image.png");
requestBody = RequestBody.create(MediaType.parse("image/png"), file);
multipartBuilder = new MultipartBody.Builder();
multipartBuilder.addFormDataPart("filedata", "image.png", requestBody);
fileRequestBody = multipartBuilder.build();
client.getNodesAPI().createNodeCall(doclibContainer.getId(), fileRequestBody).execute();

 

Nodes Query

ResultPaging<NodeRepresentation> loremResult = client.getQueriesAPI().findNodesCall("lorem").execute().body();
Assert.assertTrue(loremResult.getCount() >= 7);

 

Sites Query

ResultPaging<SiteRepresentation> siteResult = client.getQueriesAPI().findSitesCall("queries").execute().body();
Assert.assertEquals(siteResult.getCount(), 1);
Assert.assertEquals(siteResult.getList().get(0).getId(), "queriesSearchSite");

 

People Query

ResultPaging<PersonRepresentation> personResult = client.getQueriesAPI().findPeopleCall("jackson").execute().body();
Assert.assertEquals(personResult.getCount(), 1);
Assert.assertEquals(personResult.getList().get(0).getId(), "mjackson");

Basic Search

SearchAPI searchAPI = client.getSearchAPI();

QueryBody body = new QueryBody().query(new RequestQuery().query("lorem"));
ResultSetRepresentation<ResultNodeRepresentation> loremSearchResult = searchAPI.searchCall(body).execute().body();
Assert.assertTrue(loremSearchResult.getCount() >= 7);

 

Basic CMIS search

RequestQuery cmisQuery = new RequestQuery().query("select * from cmis:document WHERE cmis:name LIKE 'test.%'")
     .language(RequestQuery.LanguageEnum.CMIS);
QueryBody cmisbody = new QueryBody().query(cmisQuery);
ResultSetRepresentation<ResultNodeRepresentation> cmisResult = searchAPI.searchCall(cmisbody).execute().body();
Assert.assertTrue(cmisResult.getCount() >= 1);

 

Basic Lucene search

RequestQuery luceneQuery = new RequestQuery().query("+@cm\\:modified:[NOW/DAY-7DAYS TO NOW/DAY+1DAY] +TYPE:\"cm:content\"")
   .language(RequestQuery.LanguageEnum.LUCENE);
QueryBody lucenebody = new QueryBody().query(luceneQuery);
ResultSetRepresentation<ResultNodeRepresentation> luceneResult = searchAPI.searchCall(lucenebody).execute().body();
Assert.assertTrue(luceneResult.getCount() == 100);

 

Search with paging and sorting

RequestQuery pagingQuery = new RequestQuery().query("+TYPE:\"cm:content\"").language(RequestQuery.LanguageEnum.AFTS);

List<RequestSortDefinition> sortDefinitions = Arrays.asList(
          new RequestSortDefinition().type(RequestSortDefinition.TypeEnum.FIELD).field("cm:name").ascending(false));

RequestPagination pagination = new RequestPagination().skipCount(10).maxItems(25);

QueryBody pagingQueryBody = new QueryBody().query(pagingQuery).sort(sortDefinitions).paging(pagination);
ResultSetRepresentation<ResultNodeRepresentation> pagingResult = searchAPI.searchCall(pagingQueryBody).execute().body();
Assert.assertTrue(pagingResult.getCount() == 25);
Assert.assertEquals(pagingResult.getPagination().getMaxItems(), 25);
Assert.assertEquals(pagingResult.getPagination().getSkipCount(), 10);

 

Search with facets

RequestQuery facetQuery = new RequestQuery().query("(name:\"test*\" OR title:\"test*\") AND TYPE:\"cm:content\"");

List<RequestFacetQuery> facetsQuery = Arrays.asList(
          new RequestFacetQuery().query("content.size:[0 TO 10240]").label("Small Files"),
          new RequestFacetQuery().query("content.mimetype:'text/plain'").label("Plain Text"),
          new RequestFacetQuery().query("content.mimetype:'image/jpeg' OR content.mimetype:'image/png' OR content.mimetype:'image/gif'").label("Images"),
          new RequestFacetQuery().query("content.mimetype:'application/msword' OR content.mimetype:'application/vnd.ms-excel'").label("Office")
          );

RequestFacetFields facetFields = new RequestFacetFields().facets(Arrays.asList(new RequestFacetFieldsFacets().field("creator")));

QueryBody facetQueryBody = new QueryBody().query(facetQuery).facetQueries(facetsQuery).facetFields(facetFields);
ResultSetRepresentation<ResultNodeRepresentation> facetResult = searchAPI.searchCall(facetQueryBody).execute().body();
Assert.assertTrue(facetResult.getCount() >= 3);
Assert.assertEquals(facetResult.getContext().getFacetQueries().size(), 4);
Assert.assertEquals(facetResult.getContext().getFacetFields().get(0).getBuckets().size(), 2);
Assert.assertEquals(facetResult.getList().size(), 3);

 

Search with term hightlighting

//Search with term highlighting
RequestQuery highlightQuery = new RequestQuery().query("(name:\"test*\" OR title:\"test*\") AND TYPE:\"cm:content\"");

List<RequestHighlightField> highlightField = Arrays.asList(
          new RequestHighlightField().field("cm:name").prefix("(").postfix(")"),
          new RequestHighlightField().field("{http://www.alfresco.org/model/content/1.0}title")
);

RequestHighlight highlight = new RequestHighlight().fields(highlightField);

QueryBody highlightQueryBody = new QueryBody().query(highlightQuery).highlight(highlight);
ResultSetRepresentation<ResultNodeRepresentation> highlightResult = searchAPI.searchCall(highlightQueryBody).execute().body();
Assert.assertTrue(highlightResult.getCount() >= 3);
Assert.assertEquals(highlightResult.getList().size(), 3);
Assert.assertEquals(highlightResult.getList().get(0).getSearch().getHighlight().size(), 2);

 

 

Alfresco Java Client SDK Series

We will now follow Gavin Cornwell  v1 REST API - Part 8 - Sites   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 Test user

// Select PeopleAPI
PeopleAPI peopleAPI = client.getPeopleAPI();

//Create Test user
PersonBodyCreate personBodyCreate = new PersonBodyCreate("test", "Test", "User", "test@alfresco.com", "test");
Response<PersonRepresentation> personRepresentationResponse = peopleAPI.createPersonCall(personBodyCreate).execute();
PersonRepresentation personRepresentation = personRepresentationResponse.body();
Assert.assertEquals(personRepresentation.getId(), "test");

 

Create public site

// Create new Client and connect with new user test/test user
client = new AlfrescoClient.Builder().httpLogging(HttpLoggingInterceptor.Level.BODY)
     .connect("http://localhost:8080/alfresco", "test", "test").build();

//Create a public Site
SitesAPI sitesAPI = client.getSitesAPI();

//Create public site
/*SiteBodyCreate siteBodyCreate = new SiteBodyCreate("publicSite", "Public Site", "Public site created for blog post", SiteVisibilityEnum.PUBLIC);
Response<SiteRepresentation> siteRepresentationResponse = sitesAPI.createSiteCall(siteBodyCreate).execute();
SiteRepresentation siteRepresentation = siteRepresentationResponse.body();
Assert.assertEquals(siteRepresentation.getId(), "publicSite");
Assert.assertEquals(siteRepresentation.getVisibilityEnum(), SiteVisibilityEnum.PUBLIC);
Assert.assertEquals(siteRepresentation.getRole(), "SiteManager");

 

Retrieve document library container

//Retrieve document library container
Response<SiteContainerRepresentation> doclibContainerResponse = sitesAPI.getSiteContainerCall("publicSite", "documentLibrary").execute();
SiteContainerRepresentation doclibContainer = doclibContainerResponse.body();
Assert.assertEquals(doclibContainer.getFolderId(), "documentLibrary");

 

Update site description

//Update site description
SiteBodyUpdate update = new SiteBodyUpdate("Public site created for blog post - part 8");
Response<SiteRepresentation> siteRepresentationResponse = sitesAPI.updateSiteCall("publicSite", update).execute();
SiteRepresentation siteRepresentation = siteRepresentationResponse.body();
Assert.assertEquals(siteRepresentation.getId(), "publicSite");
Assert.assertEquals(siteRepresentation.getDescription(), "Public site created for blog post - part 8");

 

Create moderated site

SiteBodyCreate siteBodyCreate = new SiteBodyCreate("moderatedSite", "Moderated Site",
     "Moderated site created for blog post", SiteVisibilityEnum.MODERATED);
Response<SiteRepresentation> siteRepresentationResponse = sitesAPI.createSiteCall(siteBodyCreate).execute();
SiteRepresentation siteRepresentation = siteRepresentationResponse.body();
Assert.assertEquals(siteRepresentation.getId(), "moderatedSite");
Assert.assertEquals(siteRepresentation.getVisibilityEnum(), SiteVisibilityEnum.MODERATED);
Assert.assertEquals(siteRepresentation.getRole(), "SiteManager");

 

Create 2nd test user

PersonBodyCreate personBodyCreate = new PersonBodyCreate("test2", "Test", "User2", "test2@alfresco.com",
          "test2");
Response<PersonRepresentation> personRepresentationResponse = peopleAPI.createPersonCall(personBodyCreate)
          .execute();
PersonRepresentation personRepresentation = personRepresentationResponse.body();
Assert.assertEquals(personRepresentation.getId(), "test2");

 

Join public site

// Create new Client and connect with new user test2/test2 user
AlfrescoClient client2 = new AlfrescoClient.Builder().httpLogging(HttpLoggingInterceptor.Level.BODY)
          .connect("http://localhost:8080/alfresco", "test2", "test2").build();

// Join public site
SiteMembershipRequestBodyCreate requestBodyCreate = new SiteMembershipRequestBodyCreate("publicSite");
Response<SiteMembershipRequestRepresentation> publicSiteRepresentationResponse = client2.getSitesAPI()
          .createSiteMembershipRequestForPersonCall("test2", requestBodyCreate, null).execute();
SiteMembershipRequestRepresentation requestRepresentation = publicSiteRepresentationResponse.body();
Assert.assertEquals(requestRepresentation.getSite().getRole(), "SiteConsumer");
Assert.assertEquals(requestRepresentation.getSite().getId(), "publicSite");
Assert.assertEquals(requestRepresentation.getId(), "publicSite");

 

Request to join moderated site

SiteMembershipRequestBodyCreate requestModeratedBodyCreate = new SiteMembershipRequestBodyCreate(
          "moderatedSite", "I would like to join this site as it looks interesting", null);
Response<SiteMembershipRequestRepresentation> moderatedSiteRepresentationResponse = client2.getSitesAPI()
          .createSiteMembershipRequestForPersonCall("test2", requestModeratedBodyCreate, null).execute();
SiteMembershipRequestRepresentation moderatedRequestRepresentation = moderatedSiteRepresentationResponse.body();
Assert.assertNull(moderatedRequestRepresentation.getSite().getRole());
Assert.assertEquals(moderatedRequestRepresentation.getSite().getId(), "moderatedSite");
Assert.assertEquals(moderatedRequestRepresentation.getId(), "moderatedSite");

 

Review site membership requests

ResultPaging<SiteMembershipRequestRepresentation> siteMembershipRequestPaging = client2.getSitesAPI()
          .listSiteMembershipRequestsForPersonCall("test2").execute().body();
Assert.assertEquals(siteMembershipRequestPaging.getCount(), 1);
Assert.assertEquals(siteMembershipRequestPaging.getList().get(0).getId(), "moderatedSite");*/

 

List site members

ResultPaging<SiteMemberRepresentation> siteMembers = client2.getSitesAPI().listSiteMembershipsCall("publicSite").execute().body();
Assert.assertEquals(siteMembers.getCount(), 2);
Assert.assertEquals(siteMembers.getList().size(), 2);

 

List my sites

ResultPaging<SiteRoleRepresentation> mySites = client2.getSitesAPI().listSiteMembershipsForPersonCall("test2").execute().body();
Assert.assertEquals(mySites.getCount(), 1);
Assert.assertEquals(mySites.getList().get(0).getRole(), "SiteConsumer");

 

List all sites

ResultPaging<SiteRepresentation> allSites = client2.getSitesAPI().listSitesCall().execute().body();
Assert.assertEquals(allSites.getCount(), 3);

 

Find site

ResultPaging<SiteRepresentation> findSites = client2.getQueriesAPI().findSitesCall("public").execute().body();
Assert.assertEquals(findSites.getCount(), 1);
Assert.assertEquals(findSites.getList().get(0).getId(), "publicSite");

 

Leave site

Response<Void> leaveSiteResponse = client2.getSitesAPI().deleteSiteMembershipForPersonCall("test2", "publicSite").execute();
Assert.assertEquals(leaveSiteResponse.isSuccessful(), true);
Assert.assertEquals(client2.getSitesAPI().listSiteMembershipsForPersonCall("test2").execute().body().getCount(), 0);

 

Add site member

SiteMembershipBodyCreate siteMembershipBodyCreate = new SiteMembershipBodyCreate("test2", "SiteContributor");
SiteMemberRepresentation siteMember = client.getSitesAPI()
          .createSiteMembershipCall("publicSite", siteMembershipBodyCreate, null).execute().body();
Assert.assertEquals(siteMember.getRole(), "SiteContributor");
Assert.assertEquals(siteMember.getId(), "test2");
Assert.assertEquals(client2.getSitesAPI().listSiteMembershipsForPersonCall("test2").execute().body().getCount(), 1);

 

Change member role

SiteMembershipBodyUpdate siteMembershipBodyUpdate = new SiteMembershipBodyUpdate("SiteManager");
SiteMemberRepresentation siteMemberUpdated = client.getSitesAPI()
     .updateSiteMembershipCall("publicSite", "test2", siteMembershipBodyUpdate, null).execute().body();
Assert.assertEquals(siteMemberUpdated.getRole(), "SiteManager");
Assert.assertEquals(siteMemberUpdated.getId(), "test2");
Assert.assertEquals(client2.getSitesAPI().listSiteMembershipsForPersonCall("test2").execute().body().getCount(), 1);

 

Delete site

Response<Void> deleteSiteResponse = client.getSitesAPI().deleteSiteCall("publicSite").execute();
Assert.assertTrue(deleteSiteResponse.isSuccessful());

 

 

Alfresco Java Client SDK Series

We will now follow Gavin Cornwell  v1 REST API - Part 7 - Collaboration  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 Content

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

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

 

Add a comment

//Setting up Comments API
CommentsAPI commentsAPI = client.getCommentsAPI();

//Add a comment
String commentValue = "This is my comment";
CommentBody commentBody = new CommentBody(commentValue);
Response<CommentRepresentation> commentResponse = commentsAPI.createCommentCall(emptyNode.getId(), commentBody).execute();
CommentRepresentation commentRepresentation = commentResponse.body();
Assert.assertEquals(commentRepresentation.getContent(), commentValue);
Assert.assertEquals(commentRepresentation.getCreatedBy().getId(), "admin");

 

Retrieve comments

//Retrieve Comment
Response<ResultPaging<CommentRepresentation>> commentsListResponse = commentsAPI.listCommentsCall(emptyNode.getId()).execute();
ResultPaging<CommentRepresentation> commentListing = commentsListResponse.body();
Assert.assertEquals(commentListing.getCount(), 1);
Assert.assertEquals(commentListing.getList().get(0), commentRepresentation);
Assert.assertEquals(commentListing.getList().get(0).getCanEdit(), Boolean.TRUE);
Assert.assertEquals(commentListing.getList().get(0).getCanDelete(), Boolean.TRUE);

 

Update comments

//Update Comment
String updatedCommentValue = "Updated comment";
CommentBody updatedCommentBody = new CommentBody(updatedCommentValue);
Response<CommentRepresentation> updatedCommentResponse = commentsAPI.updateCommentCall(emptyNode.getId(), commentRepresentation.getId(), updatedCommentBody).execute();
CommentRepresentation updatedCommentRepresentation = updatedCommentResponse.body();
Assert.assertEquals(updatedCommentRepresentation.getContent(), updatedCommentValue);

 

Delete comment

//Delete Comment
Response<Void> deleteResponse = commentsAPI.deleteCommentCall(emptyNode.getId(), commentRepresentation.getId()).execute();
Assert.assertEquals(deleteResponse.isSuccessful(), true);
Assert.assertEquals(commentsAPI.listCommentsCall(emptyNode.getId()).execute().body().getCount(), 0);

 

Like node

//Setting up Ratings API
RatingsAPI ratingsAPI = client.getRatingsAPI();

//Like a node
RatingBody likeBody = new RatingBody(RatingsAPI.LIKES, true);
Response<RatingRepresentation> likeResponse = ratingsAPI.rateNodeCall(emptyNode.getId(), likeBody).execute();
Assert.assertEquals(likeResponse.body().getId(), RatingsAPI.LIKES);
Assert.assertEquals(likeResponse.body().getMyRating(), Boolean.TRUE);
Assert.assertEquals(likeResponse.body().getAggregate().getNumberOfRatings(), (Integer) 1);

 

Retrieve ratings

Response<ResultPaging<RatingRepresentation>> ratingListingResponse = ratingsAPI.listRatingsCall(emptyNode.getId()).execute();
ResultPaging<RatingRepresentation> ratingListing = ratingListingResponse.body();
Assert.assertEquals(ratingListing.getCount(), 2);
Assert.assertEquals(ratingListing.getList().get(0).getId(),RatingsAPI.FIVE_STAR);
Assert.assertEquals(ratingListing.getList().get(1).getId(),RatingsAPI.LIKES);

 

Unlike Nodes

//Unlike a node
Response<Void> unlikeResponse = ratingsAPI.deleteRatingCall(emptyNode.getId(), RatingsAPI.LIKES).execute();
Assert.assertEquals(unlikeResponse.isSuccessful(), true);

 

Retrieve Like ratings

//Retrieve Like Rating
Response<RatingRepresentation> ratingRepresentationResponse = ratingsAPI.getRatingCall(emptyNode.getId(), RatingsAPI.LIKES).execute();
Assert.assertNull(ratingRepresentationResponse.body().getMyRating());
Assert.assertEquals(ratingRepresentationResponse.body().getAggregate().getNumberOfRatings(), (Integer) 0);

 

Add Blog Tag

//Setting up Tagging API
TagsAPI tagsApi = client.getTagsAPI();

//Add Blog Tag
String blog = "blog";
TagBody tagBody = new TagBody(blog);
Response<TagRepresentation> tagRepresentationResponse = tagsApi.createTagForNodeCall(emptyNode.getId(), tagBody).execute();
TagRepresentation tagBlog = tagRepresentationResponse.body();
Assert.assertEquals(tagBlog.getTag(), blog);

Add Post Tag

//Add Post Tag
String post = "post";
TagBody tag2Body = new TagBody(post);
Response<TagRepresentation> tag2RepresentationResponse = tagsApi.createTagForNodeCall(emptyNode.getId(), tag2Body).execute();
Assert.assertEquals(tag2RepresentationResponse.body().getTag(), post);

 

Retrieve node tags

//Retrieve node Tags
Response<ResultPaging<TagRepresentation>> tagListResponse = tagsApi.listTagsForNodeCall(emptyNode.getId()).execute();
ResultPaging<TagRepresentation> tagList = tagListResponse.body();
Assert.assertEquals(tagList.getCount(), 2);
Assert.assertEquals(tagList.getList().get(0).getTag(), blog);
Assert.assertEquals(tagList.getList().get(1).getTag(), post);

 

Retrieve repository tags

//Retrieve Repository Tags
Response<ResultPaging<TagRepresentation>> tagRepoListResponse = tagsApi.listTagsCall().execute();
ResultPaging<TagRepresentation> tagRepoList = tagRepoListResponse.body();
Assert.assertEquals(tagRepoList.getCount(), 2);
Assert.assertEquals(tagRepoList.getList().get(0).getTag(), blog);
Assert.assertEquals(tagRepoList.getList().get(1).getTag(), post);

 

Delete tag from node

//Delete Tag from Node
Response<Void> deleteTagResponse = tagsApi.deleteTagFromNodeCall(emptyNode.getId(), tagBlog.getId()).execute();
Assert.assertEquals(deleteTagResponse.isSuccessful(), true);
Assert.assertEquals(tagsApi.listTagsForNodeCall(emptyNode.getId()).execute().body().getCount(), 1);

 

 

Alfresco Java Client SDK Series

In the last post we discussed the people API, this time I'm going to show you how you can manage deleted nodes via the trashcan API.

 

As usual, there is a Postman collection to go with this post, click the "Run in Postman" button below to import it into your client.

 

 

Before we can start looking at the trashcan API we need to do a little bit of setup by deleting a node. Firstly, make sure your repository has the test user we created back in part 8. The 1st and 2nd request of the Postman collection creates and deletes a file, respectively. 

 

To list the nodes that have been deleted we can use http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/deleted-nodes (3rd request in the Postman collection), the response will look similar to the one below:

{
  "list": {
    "pagination": {
      "count": 1,
      "hasMoreItems": false,
      "totalItems": 1,
      "skipCount": 0,
      "maxItems": 100
    },
    "entries": [
      {
        "entry": {
          "createdAt": "2017-04-11T13:53:02.359+0000",
          "archivedAt": "2017-04-11T13:55:56.432+0000",
          "isFolder": false,
          "isFile": true,
          "createdByUser": {
            "id": "test",
            "displayName": "Test User"
          },
          "modifiedAt": "2017-04-11T13:53:02.359+0000",
          "modifiedByUser": {
            "id": "test",
            "displayName": "Test User"
          },
          "name": "content-to-be-deleted.txt",
          "archivedByUser": {
            "id": "test",
            "displayName": "Test User"
          },
          "id": "1860a21b-b6d2-4cde-aadd-e0bd521787cf",
          "nodeType": "cm:content",
          "content": {
            "mimeType": "text/plain",
            "mimeTypeName": "Plain Text",
            "sizeInBytes": 0,
            "encoding": "UTF-8"
          }
        }
      }
    ]
  }
}

 

The response should look familiar, it's based on the response of /nodes/{{nodeId}}/children with a couple of additions. There is an additional archivedAt property and an archivedByUser property providing details of when the node was archived and by whom. By default, the list is ordered by the archivedAt property, with the most recently deleted being first in the list.

 

As with the nodes API some information is omitted for performance reasons, see the API Explorer for the additional information you can request via the include query parameter.

 

Calling this endpoint as a normal user will only return the nodes you have deleted, however, if you are an administrator, all deleted nodes in the system are returned.

 

To get details of an individual deleted node http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/deleted-nodes/{{nodeId}} (4th request in the Postman collection) can be used. This endpoint returns a little more information about the deleted node (see below) by default but again there is some optional information that can be added via the include query parameter, see the API Explorer for details.

{
  "entry": {
    "isFile": true,
    "createdByUser": {
      "id": "test",
      "displayName": "Test User"
    },
    "modifiedAt": "2017-04-11T13:53:02.359+0000",
    "nodeType": "cm:content",
    "content": {
      "mimeType": "text/plain",
      "mimeTypeName": "Plain Text",
      "sizeInBytes": 0,
      "encoding": "UTF-8"
    },
    "aspectNames": [
      "rn:renditioned",
      "cm:ownable",
      "cm:auditable",
      "cm:thumbnailModification"
    ],
    "createdAt": "2017-04-11T13:53:02.359+0000",
    "archivedAt": "2017-04-11T13:55:56.432+0000",
    "isFolder": false,
    "modifiedByUser": {
      "id": "test",
      "displayName": "Test User"
    },
    "name": "content-to-be-deleted.txt",
    "archivedByUser": {
      "id": "test",
      "displayName": "Test User"
    },
    "id": "1860a21b-b6d2-4cde-aadd-e0bd521787cf",
    "properties": {
      "cm:lastThumbnailModification": [
        "doclib:1491918788304"
      ],
      "cm:owner": {
        "id": "test",
        "displayName": "Test User"
      }
    }
  }
}

 

As you would expect, the API also allows you to restore a deleted node which removes it from the list of deleted nodes and puts it back in the "live" store. You can do this by POSTing to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/deleted-nodes/{{nodeId}}/restore (5th request in the Postman collection). For this endpoint there is no body to send. If the node was successfully restored you will receive a 200 OK response with a representation of the "live" node.  

 

Just to make sure you can list the deleted nodes again using http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/deleted-nodes (3rd request in the Postman collection) and you should see an empty list as shown below:

{
  "list": {
    "pagination": {
      "count": 0,
      "hasMoreItems": false,
      "totalItems": 0,
      "skipCount": 0,
      "maxItems": 100
    },
    "entries": []
  }
}

 

The opposite or restoring a node is to permanently delete it, once this is done it's final, there is no way to get the node back! To try this out we need to repeat the setup process so re-run the 1st and 2nd request in the Postman collection.

 

To permanently delete a node send a DELETE request to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/deleted-nodes/{{nodeId}} (6th request in the Postman collection), if the deletion was successful you will receive an empty response with a 204 status code.

 

List the deleted nodes (3rd request in the Postman collection) and you will see an empty list once again.

 

If you want to bypass the trashcan completely you can permanently delete a "live" node by using the permanent query parameter, see the API Explorer for details.

 

Hopefully that's given you a good overview of the trashcan API and what's possible, next time we're going to look at the only remaining API we haven't covered yet, the discovery API.

In the last post we looked at the queries and search APIs, today we're going to look at the people API. A couple of the endpoints have been available since 4.2 and we've added a few new ones in the 5.2 release.

 

To keep with tradition this post has a Postman collection you can use to follow along, click the "Run in Postman" button below to import it into your client.

 

 

Let's start by creating a new person using one of the new endpoints added in the 5.2 release. We can POST the body below to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/people (1st request in the Postman collection) to create a person with a username (id) of "jdoe". Any of the properties defined for the out-of-the-box cm:person type can be provided, for full details please refer to the API Explorer.

{
  "id": "jdoe",
  "firstName": "John",
  "lastName": "Doe",
  "email": "john.doe@example.com",
  "password": "jdoe",
  "skypeId": "johndoe_skype",
  "jobTitle": "Software Engineer"
}

 

Sending the request above results in the following response:

{
  "entry": {
    "firstName": "John",
    "lastName": "Doe",
    "skypeId": "johndoe_skype",
    "jobTitle": "Software Engineer",
    "emailNotificationsEnabled": true,
    "company": {},
    "id": "jdoe",
    "enabled": true,
    "email": "john.doe@example.com"
  }
}

 

Some customers extend the out-of-the-box cm:person object so we have added support for custom properties too, for example to create a person with a custom property called mycompany:employeeId the following body could be used (presuming the property has been defined in the content model):

{
  "id": "jdoe",
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane.doe@example.com",
  "password": "jdoe",
  "skypeId": "janedoe_skype",
  "jobTitle": "Software Engineer",
  "properties": {
    "mycompany:employeeId": "abc-123"
  }
}

 

Another capability added in 5.2 is the ability to retrieve a list of people in the repository by using http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/people (2nd request in the Postman collection) resulting in a response similar to the one below, which shows the person we just created.

{
  "list": {
    "pagination": {
      "count": 8,
      "hasMoreItems": false,
      "totalItems": 8,
      "skipCount": 0,
      "maxItems": 100
    },
    "entries": [
      {
        "entry": {
          "lastName": "Beecher",
          "userStatus": "Helping to design the look and feel of the new web site",
          "jobTitle": "Graphic Designer",
          "statusUpdatedAt": "2011-02-15T20:20:13.432+0000",
          "mobile": "0112211001100",
          "emailNotificationsEnabled": true,
          "description": "Alice is a demo user for the sample Alfresco Team site.",
          "telephone": "0112211001100",
          "enabled": false,
          "firstName": "Alice",
          "skypeId": "abeecher",
          "avatarId": "198500fc-1e99-4f5f-8926-248cea433366",
          "location": "Tilbury, UK",
          "company": {
            "organization": "Moresby, Garland and Wedge",
            "address1": "200 Butterwick Street",
            "address2": "Tilbury",
            "address3": "UK",
            "postcode": "ALF1 SAM1"
          },
          "id": "abeecher",
          "email": "abeecher@example.com"
        }
      },
      {
        "entry": {
          "firstName": "John",
          "lastName": "Doe",
          "skypeId": "johndoe_skype",
          "jobTitle": "Software Engineer",
          "emailNotificationsEnabled": true,
          "company": {},
          "id": "jdoe",
          "enabled": true,
          "email": "john.doe@example.com"
        }
      },
      ...
    ]
  }
}

 

You may recall way back in part 2 when we discussed the /nodes API, the properties and aspect names are not present by default but can be included via the include query parameter, the same holds true here as well. If you want to see any custom properties or aspects applied you can add include=properties,aspectNames to the URL. The results can also be sorted by id (username), firstName and lastName.

 

Unfortunately, we ran out of time in the 5.2 release to add filtering capabilities to this endpoint, however, as discussed in the last post, the /queries/people endpoint (3rd request in the Postman collection) or the /search endpoint can be used to look for people and achieve the same thing.

 

To retrieve the full details of the person we created earlier we can use http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/people/jdoe which returns the following response (4th request in the Postman collection):

{
  "entry": {
    "firstName": "John",
    "lastName": "Doe",
    "skypeId": "johndoe_skype",
    "jobTitle": "Software Engineer",
    "emailNotificationsEnabled": true,
    "company": {},
    "id": "jdoe",
    "enabled": true,
    "email": "john.doe@example.com"
  }
}

 

It's also possible to update the details of a person (non administrator users can only update their own details for obvious reasons). The example below shows how we can update the details of the person we created earlier by PUTting the following body to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/people/jdoe (5th request in the Postman collection):

{
  "firstName": "Johnathon",
  "mobile": "07000 123456"
}

 

Using the same endpoint it's also possible to change a person's password. For security reasons you can only change your own password (unless you're an administrator) and when you do you have to provide the old password together with the new password. The example below (6th request in the Postman collection) changes the password of the person we created earlier to "my-new-password":

{
  "oldPassword": "jdoe",
  "password": "my-new-password"
}

 

The last capability of the people API we're going to cover today is the ability to disable and enable people, this is obviously something only administrators can do. You may have noticed the enabled flag for each person, this can be toggled to set the state of the person. It's also possible to create a person in the disabled state by setting the enabled property to false when the person is created.

 

The example below demonstrates how we can disable the person we created earlier. Again, we use PUT against http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/people/jdoe to achieve this (7th request in the Postman collection):

{
  "enabled": false
}

 

Now try and make a request using "jdoe" (remember to update the credentials to use the newer password), you'll get a 401 error as disabled users are locked out of the repository.

 

At this time there is no way to remove a person from the system via the v1 REST API, there are several nuances that need further discussion, once these discussions have concluded you can expect to see this capability added to a future release.

 

That concludes our coverage of the people API, I hope you'll join me again next time when we look at the trashcan (deleted nodes) API.

The aim of this blog is to introduce you to Enterprise Integration Patterns and to show you how to create an application to integrate Alfresco with an external application…in this case we will be sending documents on request from Alfresco to Box based on CMIS queries. We will store both the content and the metadata in Box.

 

1.    Enterprise Integration Patterns

EIP (Enterprise Integration Patters) defines a language consisting of 65 integration patterns (http://www.enterpriseintegrationpatterns.com/patterns/messaging/toc.html) to establish a technology-independent vocabulary and a visual notation to design and document integration solutions.

Why EIP? Today's applications rarely live in isolation. Architecting integration solutions is a complex task.

The lack of a common vocabulary and body of knowledge for asynchronous messaging architectures make it difficult to avoid common pitfalls.

For example the following diagram shows how content from one application is routed and transformed to be delivered to another application. Each step can be further detailed with specific annotations.

 

 

  • Channel Patterns describe how messages are transported across a Message Channel. These patterns are implemented by most commercial and open source messaging systems.
  • Message Construction Patterns describe the intent, form and content of the messages that travel across the messaging system.
  • Routing Patterns discuss how messages are routed from a sender to the correct receiver. Message routing patterns consume a message from one channel and republish it message, usually without modification, to another channel based on a set of conditions.
  • Transformation Patterns change the content of a message, for example to accommodate different data formats used by the sending and the receiving system. Data may have to be added, taken away or existing data may have to be rearranged.
  • Endpoint Patterns describe how messaging system clients produce or consume messages.
  • System Management Patterns describe the tools to keep a complex message-based system running, including dealing with error conditions, performance bottlenecks and changes in the participating systems.

 

The following example shows how to maintain the overall message flow when processing a message consisting of multiple elements, each of which may require different processing.

               

 

2.    Apache Camel

Apache Camel (http://camel.apache.org/) is an integration framework whose main goal is to make integration easier. It implements many of the EIP patterns and allows you to focus on solving business problems, freeing you from the burden of plumbing.

At a high level, Camel is composed of components, routes and processors. All of these are contained within the CamelContext .

 

The CamelContext provides access to many useful services, the most notable being components, type converters, a registry, endpoints, routes, data formats, and languages.

 

Service

Description

Components

A Component is essentially a factory of Endpoint instances. To date, there are over 80 components in the Camel ecosystem that range in function from data transports, to DSL s, data formats, and so on i.e. cmis, http, box, salesforce, ftp, smtp, etc

Endpoints

An endpoint is the Camel abstraction that models the end of a channel through which a system can send or receive messages. Endpoints are usually created by a Component and Endpoints are usually referred to in the DSL via their URIs i.e. cmis://cmisServerUrl[?options]

Routes

The steps taken to send a message from one end point to another end point.

Type Converters

Camel provides a built-in type-converter system that automatically converts between well-known types. This system allows Camel components to easily work together without having type mismatches.

Data Formats

Allow messages to be marshaled to and from binary or text formats to support a kind of Message Translator i.e. gzip, json, csv, crypto, etc

Registry

Contains a registry that allows you to look up beans i.e. use a bean that defines the jdbc data source

Languages

To wire processors and endpoints together to form routes, Camel defines a DSL. DSL include among other Java, Groovy, Scala, Spring XML.

 

3.    Building an Integration Application

 

The aim of the application is to send documents on request from Alfresco to Box. We will store both the content and the metadata in Box.

To build an EIP Application we are going to use:

  • Maven to build the application
  • Spring-boot to run the application
  • Apache Camel to integrate Alfresco and Box

 

The full source code is available on GitHub: https://github.com/miguel-rodriguez/Alfresco-Camel

 

The basic message flow is as follows:

 


 

 

3.1          Maven

Apache Maven (https://maven.apache.org/) is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a project's build, reporting and documentation from a central piece of information.

 

3.1.1    Maven Pom.xml

For our project the pom.xml brings the required dependencies such as Camel and ActiveMQ. The pom.xml file looks like this:

 

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

   

    <groupId>support.alfresco</groupId>

    <artifactId>camel</artifactId>

    <name>Spring Boot + Camel</name>

    <version>0.0.1-SNAPSHOT</version>

    <description>Project Example.</description>

 

    <!-- Using Spring-boot 1.4.3 -->

    <parent>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-parent</artifactId>

        <version>1.4.3.RELEASE</version>

    </parent>

 

    <!-- Using Camel version 2.18.1 -->

    <properties>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <camel-version>2.18.1</camel-version>

        <app.version>1.0-SNAPSHOT</app.version>

    </properties>

 

    <!-- Spring -->

    <dependencies>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

        </dependency>

 

        <!-- The Core Camel Java DSL based router -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-core</artifactId>

            <version>${camel-version}</version>

        </dependency>

 

        <!-- Camel Spring support -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-spring</artifactId>

            <version>${camel-version}</version>

        </dependency>

 

        <!-- Camel Metrics based monitoring component -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-metrics</artifactId>

            <version>${camel-version}</version>

        </dependency>

 

        <!-- Camel JMS support -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-jms</artifactId>

            <version>${camel-version}</version>

        </dependency>

 

        <!-- ActiveMQ component for Camel -->

        <dependency>

            <groupId>org.apache.activemq</groupId>

            <artifactId>activemq-camel</artifactId>

        </dependency>

 

        <!-- Camel CMIS which is based on Apache Chemistry support -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-cmis</artifactId>

            <version>2.14.1</version>

        </dependency>

 

        <!-- Camel Stream (System.in, System.out, System.err) support -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-stream</artifactId>

            <version>${camel-version}</version>

        </dependency>

 

        <!-- Camel JSON Path Language -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-jsonpath</artifactId>

            <version>${camel-version}</version>

        </dependency>

 

       <!-- Apache HttpComponents HttpClient - MIME coded entities -->

        <dependency>

            <groupId>org.apache.httpcomponents</groupId>

            <artifactId>httpmime</artifactId>

        </dependency>

 

        <!-- Camel HTTP (Apache HttpClient 4.x) support -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-http4</artifactId>

            <version>${camel-version}</version>

        </dependency>

 

        <!-- Camel SQL support -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-sql</artifactId>

            <version>${camel-version}</version>

        </dependency>

 

        <!-- Camel Zip file support -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-zipfile</artifactId>

            <version>${camel-version}</version>

        </dependency>

 

        <!-- Support for PostgreSQL database -->

        <dependency>

            <groupId>org.postgresql</groupId>

            <artifactId>postgresql</artifactId>

            <exclusions>

                <exclusion>

                    <groupId>org.slf4j</groupId>

                    <artifactId>slf4j-simple</artifactId>

                </exclusion>

            </exclusions>

        </dependency>

 

        <!-- Camel Component for Box.com -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-box</artifactId>

            <version>${camel-version}</version>

        </dependency>

 

        <!-- Camel script support -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-script</artifactId>

            <version>${camel-version}</version>

        </dependency>

 

        <!-- A simple Java toolkit for JSON -->

        <dependency>

            <groupId>com.googlecode.json-simple</groupId>

            <artifactId>json-simple</artifactId>

            <version>1.1.1</version>

            <!--$NO-MVN-MAN-VER$-->

        </dependency>

 

        <!-- XStream is a Data Format which to marshal and unmarshal Java objects to and from XML -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-xstream</artifactId>

            <version>2.9.2</version>

        </dependency>

 

        <!-- Jackson XML is a Data Format to unmarshal an XML payload into Java objects or to marshal Java objects into an XML payload -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-jackson</artifactId>

            <version>2.9.2</version>

        </dependency>

 

        <!-- test -->

        <dependency>

            <groupId>org.apache.camel</groupId>

            <artifactId>camel-test</artifactId>

            <version>${camel-version}</version>

            <scope>test</scope>

        </dependency>

 

        <!-- logging -->

        <dependency>

            <groupId>commons-logging</groupId>

            <artifactId>commons-logging</artifactId>

            <version>1.1.1</version>

        </dependency>

 

        <dependency>

            <groupId>org.apache.logging.log4j</groupId>

            <artifactId>log4j-api</artifactId>

            <scope>test</scope>

        </dependency>

 

        <dependency>

            <groupId>org.apache.logging.log4j</groupId>

            <artifactId>log4j-core</artifactId>

            <scope>test</scope>

        </dependency>

 

        <dependency>

            <groupId>org.apache.logging.log4j</groupId>

            <artifactId>log4j-slf4j-impl</artifactId>

            <scope>test</scope>

        </dependency>

 

        <!--  monitoring -->

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-remote-shell</artifactId>

        </dependency>

 

        <dependency>

            <groupId>org.jolokia</groupId>

            <artifactId>jolokia-core</artifactId>

        </dependency>

 

    </dependencies>

    <build>

        <plugins>

            <plugin>

                <groupId>org.springframework.boot</groupId>

                <artifactId>spring-boot-maven-plugin</artifactId>

            </plugin>

        </plugins>

    </build>

</project>

 

 

3.2          Spring Boot

 

Spring Boot (https://projects.spring.io/spring-boot/) makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run". Most Spring Boot applications need very little Spring configuration.

 

Features

  • Create stand-alone Spring applications
  • Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
  • Provide opinionated 'starter' POMs to simplify your Maven configuration
  • Automatically configure Spring whenever possible
  • Provide production-ready features such as metrics, health checks and externalized configuration

 

3.2.1       Spring Boot applicationContext.xml

We use the applicationContext.xml to define the java beans used by our application. Here we define the beans for connecting to Box, Database connectivity, ActiveMQ and Camel. For the purpose of this application we only need ActiveMQ and Box connectivity.

 

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd

        http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd">

   

 <!-- Define configuration file application.properties -->

    <bean id="placeholder" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

        <property name="locations">

            <list>

                <value>classpath:application.properties</value>

            </list>

        </property>

        <property name="ignoreResourceNotFound" value="false" />

        <property name="searchSystemEnvironment" value="true" />

        <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE" />

    </bean>

   

    <!--  Bean for Box authentication. Please note you need a Box developer account -->

    <bean id="box" class="org.apache.camel.component.box.BoxComponent">

        <property name="configuration">

            <bean class="org.apache.camel.component.box.BoxConfiguration">

                <property name="userName" value="${box.userName}" />

                <property name="userPassword" value="${box.userPassword}" />

                <property name="clientId" value="${box.clientId}" />

                <property name="clientSecret" value="${box.clientSecret}" />

            </bean>

        </property>

    </bean>

 

    <!-- Define database connectivity -->

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">

        <property name="driverClassName" value="org.postgresql.Driver" />

        <property name="url" value="jdbc:postgresql://localhost:5432/alfresco" />

        <property name="username" value="alfresco" />

        <property name="password" value="admin" />

    </bean>

   

    <!-- Configure the Camel SQL component to use the JDBC data source -->

    <bean id="sql" class="org.apache.camel.component.sql.SqlComponent">

        <property name="dataSource" ref="dataSource" />

    </bean>

   

    <!-- Create a connection to ActiveMQ -->

    <bean id="jmsConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">

        <property name="brokerURL" value="tcp://localhost:61616" />

    </bean>

   

    <!-- Create Camel context -->

    <camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring" autoStartup="true">

        <routeBuilder ref="myRouteBuilder" />

    </camelContext>

   

    <!-- Bean defining Camel routes -->

    <bean id="myRouteBuilder" class="support.alfresco.Route" />

</beans>

 

3.2.2       Application.java

The Application class is used to run our Spring application

 

package support.alfresco;

import org.springframework.boot.SpringApplication;

import org.springframework.context.annotation.ImportResource;

 

@ImportResource("applicationContext.xml")

public class Application {

                    public static void main(String[] args) {

                                        SpringApplication.run(Application.class, args);

                    }

}

 

3.2.3       Route.java

In the Route.java file we define the Camel routes to send traffic from Alfresco to Box.

The code below shows the routes to Execute cmis query, download content and properties, compress it and upload it to Box

 

                                   //////////////////////////////////////

                                        // Download Alfresco documents  //

                                        //////////////////////////////////////

                                        from("jms:alfresco.downloadNodes")

                                        .log("Running query: ${body}")

                                        .setHeader("CamelCMISRetrieveContent", constant(true))

                                        .to(alfrescoSender + "&queryMode=true")

                                        // Class FileContentProcessor is used to store the files in the filesystem together with the metadata

                                        .process(new FileContentProcessor());

                                       

                                       

                                        ///////////////////////////////////////////////

                                        // Move documents and metadata to Box  //

                                        //////////////////////////////////////////////

                                        from("file:/tmp/downloads?antInclude=*")

                                        .marshal().zipFile()

                                        .to("file:/tmp/box");

                                       

                                        from("file:/tmp/metadata?antInclude=*")

                                        .marshal().zipFile()

                                        .to("file:/tmp/box");

                                       

                                        from("file:/tmp/box?noop=false&recursive=true&delete=true")

                                        .to("box://files/uploadFile?inBody=fileUploadRequest");

 

Let’s break it down…

 

1. We read requests messages with a CMIS query from an ActiveMQ queue

from("jms:alfresco.downloadNodes")

 

For example a CMIS query to get the nodes on a specific folder looks like…

SELECT * FROM cmis:document WHERE IN_FOLDER ('workspace://SpacesStore/56c5bc2e-ea5c-4f6a-b817-32f35a7bb195') and cmis:objectTypeId='cmis:document'

 

 For testing purposes we can fire the message requests directly from the ActiveMQ admin UI (http://127.0.0.1:8161/admin/

 

 

2. We send the CMIS query to Alfresco defined as “alfrescoSender”

.to(alfrescoSender + "&queryMode=true")

 

3. Alfresco sender is defined in application.properties as

 

and mapped to “alfrescoSender” variable in Route.java

public static String alfrescoSender;

@Value("${alfresco.sender}")

public void setAlfrescoSender(String inSender) {

        alfrescoSender = inSender;

}

   

4. We store the files retrieved by the CMIS query in the filesystem using class FileContentProcessor for that job

.process(new FileContentProcessor());

 

5. Zip the content file and the metadata file 

from("file:/tmp/downloads?antInclude=*")

.marshal().zipFile()

.to("file:/tmp/box");

                                       

from("file:/tmp/metadata?antInclude=*")

.marshal().zipFile()

.to("file:/tmp/box");

 

6. And finally upload the content to Box 

from("file:/tmp/box?noop=false&recursive=true&delete=true")

.to("box://files/uploadFile?inBody=fileUploadRequest");

 

 

 4.    Building and Running the application

To build the application using maven we execute the following command: 

mvn clean install

 

To run the application execute the following command:

mvn spring-boot:run

5.    Monitoring with Hawtio

Hawtio (http://hawt.io) is a pluggable management console for Java stuff which supports any kind of JVM, any kind of container (Tomcat, Jetty, Karaf, JBoss, Fuse Fabric, etc), and any kind of Java technology and middleware.

Hawtion can help you to visualize Routes with real-time updates on messages metrics.

 

 

You can get statistical data for each individual route.

 

 

I hope this basic introduction to EIP and Apache Camel gives you some idea on how to integrate different applications using the existing end points provided by Apache Camel.

In the last post we covered the Sites APIs, this time we're going to take a look at ways to find things in the repository. There are two main APIs to do this, /queries and /search.

 

As always there is a Postman collection to accompany this post, click the "Run in Postman" button below to import it into your client.

 

 

The first request uses one of the APIs we learnt about last time to create a public site named "queriesSearchSite", the second request retrieves the document library container id and stores it in a global variable. The third request can be used to upload documents to the site, to get the most out of this post upload a text file containing some lorem ipsum text, some image files and some Office documents.

 

The /queries endpoints are designed to be very simple to use and usable in "live search" scenarios i.e. they can executed upon each key press so clients can show results as the user types. The actual query used behind the scenes is hard-coded, if complex or custom queries are required the /search API should be used, which we'll look at shortly.

 

Let's first take a look at the endpoint to find nodes. The http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/queries/nodes endpoint returns nodes (files and folders) that match a simple term provided via a query parameter. The type of nodes returned can be restricted via the nodeType query parameter, for example passing my:type as the value will only return nodes of that type and any of it's subtypes. The query will look in the name, title and description properties, in the content and in tags for a match. Take a look at the API Explorer for the other options available for this endpoint.

 

The 4th request in the Postman collection shows an example of looking for the term "lorem". The number of results you get will depend on the content in your repository, some of the sample site content contains the word "lorem" so you should get a few results! The response format (shown below) is also consistent with the /nodes API so if you've been following the series it should look familiar.

{
  "list": {
    "pagination": {
      "count": 7,
      "hasMoreItems": false,
      "totalItems": 7,
      "skipCount": 0,
      "maxItems": 100
    },
    "entries": [
      {
        "entry": {
          "createdAt": "2017-04-10T09:12:32.761+0000",
          "isFolder": false,
          "isFile": true,
          "createdByUser": {
            "id": "test",
            "displayName": "Test User"
          },
          "modifiedAt": "2017-04-10T09:12:32.761+0000",
          "modifiedByUser": {
            "id": "test",
            "displayName": "Test User"
          },
          "name": "test-lorem-ipsum.txt",
          "id": "3379e95a-fa24-418e-a1df-7d7ef9192516",
          "nodeType": "cm:content",
          "content": {
            "mimeType": "text/plain",
            "mimeTypeName": "Plain Text",
            "sizeInBytes": 3186,
            "encoding": "ISO-8859-1"
          },
          "parentId": "d32682f0-cfd9-43da-ab74-ba78fc59a01a"
        }
      },
      ...
    ]
  }
}

 

To find sites the http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/queries/sites endpoint can be used. The 5th request in the Postman collection shows how to look for sites that have the term "queries" in the site id, title or description. Again, take a look at the API Explorer for other options, including how to order the results.

{
  "list": {
    "pagination": {
      "count": 1,
      "hasMoreItems": false,
      "totalItems": 1,
      "skipCount": 0,
      "maxItems": 100
    },
    "entries": [
      {
        "entry": {
          "role": "SiteManager",
          "visibility": "PUBLIC",
          "guid": "763588b4-9c6f-4b34-af41-c92a6102711f",
          "description": "Site created for queries and search blog post",
          "id": "queriesSearchSite",
          "preset": "site-dashboard",
          "title": "Queries and Search Site"
        }
      }
    ]
  }
}

 

Finally, to find people (users) the http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/queries/people endpoint can be used. The 6th request in the Postman collection shows how to look for people that have "jackson" in their username (id), first name or last name. As my repository has the sample site loaded the sample user "Mike Jackson" is returned:

{
  "list": {
    "pagination": {
      "count": 1,
      "hasMoreItems": false,
      "totalItems": 1,
      "skipCount": 0,
      "maxItems": 100
    },
    "entries": [
      {
        "entry": {
          "lastName": "Jackson",
          "userStatus": "Working on a new web design for the corporate site",
          "jobTitle": "Web Site Manager",
          "statusUpdatedAt": "2011-02-15T20:13:09.649+0000",
          "mobile": "012211331100",
          "emailNotificationsEnabled": true,
          "description": "Mike is a demo user for the sample Alfresco Team site.",
          "telephone": "012211331100",
          "enabled": false,
          "firstName": "Mike",
          "skypeId": "mjackson",
          "avatarId": "3fbde500-298b-4e80-ae50-e65a5cbc2c4d",
          "location": "Threepwood, UK",
          "company": {
            "organization": "Green Energy",
            "address1": "100 Cavendish Street",
            "address2": "Threepwood",
            "address3": "UK",
            "postcode": "ALF1 SAM1"
          },
          "id": "mjackson",
          "email": "mjackson@example.com"
        }
      }
    ]
  }
}

 

As with the other queries endpoints the options are intentionally limited, see the API Explorer for details. We will also be looking at the people API in a lot more depth in the next instalment of this series so stay tuned!

 

As mentioned earlier, if the pre-canned queries do not provide what you need you have the option to use the rich and powerful /search API, at the cost of a little more complexity.

 

Due to the number of options and functionality available via the search API, it is a little different than most of the other APIs we've looked at so far in the series. Firstly, the API is defined under the "search" namespace so it's base URL is slightly different. Secondly, the /search endpoint does not accept any query parameters and is therefore completely controlled via the POST body as we'll see in the examples that follow.

 

We'll start by executing a simple search, the 7th request in the Postman collection shows how POSTing the following body to http://localhost:8080/alfresco/api/-default-/public/search/versions/1/search searches for the term "lorem":

{
  "query": {
    "query": "lorem"
  }
}

 

The results should look familiar though, for the most part they are the same as the results from /queries and from /nodes/{id}/children

{
  "list": {
    "pagination": {
      "count": 7,
      "hasMoreItems": false,
      "totalItems": 7,
      "skipCount": 0,
      "maxItems": 100
    },
    "entries": [
      {
        "entry": {
          "isFile": true,
          "createdByUser": {
            "id": "mjackson",
            "displayName": "Mike Jackson"
          },
          "modifiedAt": "2011-03-03T10:31:31.651+0000",
          "nodeType": "cm:content",
          "content": {
            "mimeType": "application/vnd.ms-powerpoint",
            "mimeTypeName": "Microsoft PowerPoint",
            "sizeInBytes": 2898432,
            "encoding": "UTF-8"
          },
          "parentId": "38745585-816a-403f-8005-0a55c0aec813",
          "createdAt": "2011-03-03T10:31:30.596+0000",
          "isFolder": false,
          "search": {
            "score": 1.6137421
          },
          "modifiedByUser": {
            "id": "mjackson",
            "displayName": "Mike Jackson"
          },
          "name": "Project Overview.ppt",
          "location": "nodes",
          "id": "99cb2789-f67e-41ff-bea9-505c138a6b23"
        }
      },
      ...
    ]
  }
}

 

There are a couple of differences though, the search API returns two additional properties, search and location. The search property (line 29) adds extra context for the individual result, in this case, the score.

 

Explaining the full details is beyond the scope of this post but it is possible to search across "live" nodes, deleted nodes and versioned nodes, the location property (line 37) shows from which area the result came from. By default only "live" nodes are included.

 

The example above used the default search language afts (Alfresco Full Text Search), however, cmis and lucene are also supported. The example body below shows how to execute a simple CMIS query (8th request in the Postman collection) to find all content with a name starting with "test.":

{
  "query": {
    "query": "select * from cmis:document WHERE cmis:name LIKE 'test.%'",
    "language": "cmis"
  }
}

 

For completeness, the example body below shows how to execute a simple Lucene query (9th request in the Postman collection) to find all the content modified in the last week:

{
  "query": {
    "query": "+@cm\\:modified:[NOW/DAY-7DAYS TO NOW/DAY+1DAY] +TYPE:\"cm:content\"",
    "language": "lucene"
  }
}

 

As with all the v1 REST APIs paging can also be controlled, it's just done via the body rather than a query parameter. The results can also be sorted. The example body below shows how to execute a search (10th request in the Postman collection) to find all content ordered by the name property, only show 25 results rather than the default of 100 and skip the first 10 results:

{
  "query": {
    "query": "+TYPE:\"cm:content\"",
    "language": "afts"
  },
  "paging": {
    "maxItems": "25",
    "skipCount": "10"
  },
  "sort": [{"type":"FIELD", "field":"cm:name", "ascending":"false"}]
}

 

Now we've covered the basics let's look at a couple of the more interesting features of the search API, faceting and term highlighting.

 

There are two types of facets; queries and fields. A query facet returns the count of results for the given query, you can provide multiple facet queries in one request. A field facet returns a number of "buckets" for a field, providing the count of results that fit into each bucket.

 

It's much easier to understand with an example, the body below shows a search request (11th request in the Postman collection) that will look for content nodes that have a name or title starting with "test". We also specify that we want to know how many of the results are small files, how many are plain text files, how many are images and how many are Office files. Additionally, we are also asking for the creator facet field to be included, which will indicate how many of the results were created by each user:

{
  "query": {
    "query": "(name:\"test*\" OR title:\"test*\") AND TYPE:\"cm:content\""
  },
  "facetQueries": [
    {"query": "content.size:[0 TO 10240]", "label": "Small Files"},
    {"query": "content.mimetype:'text/plain'", "label": "Plain Text"},
    {"query": "content.mimetype:'image/jpeg' OR content.mimetype:'image/png' OR content.mimetype:'image/gif'", "label": "Images"},
    {"query": "content.mimetype:'application/msword' OR content.mimetype:'application/vnd.ms-excel'", "label": "Office"}
  ],
  "facetFields": {"facets": [{"field": "creator"}]}
}

 

The response to this request is shown below:

{
  "list": {
    "pagination": {
      "count": 8,
      "hasMoreItems": false,
      "totalItems": 8,
      "skipCount": 0,
      "maxItems": 100
    },
    "context": {
      "facetQueries": [
        {
          "label": "Office",
          "count": 2
        },
        {
          "label": "Small Files",
          "count": 4
        },
        {
          "label": "Plain Text",
          "count": 1
        },
        {
          "label": "Images",
          "count": 3
        }
      ],
      "facetsFields": [
        {
          "label": "creator",
          "buckets": [
            {
              "label": "test",
              "count": 6,
              "display": "Test User"
            },
            {
              "label": "System",
              "count": 2,
              "display": "System"
            }
          ]
        }
      ]
    },
    "entries": [
      {
        "entry": {
          "isFile": true,
          "createdByUser": {
            "id": "test",
            "displayName": "Test User"
          },
          "modifiedAt": "2017-04-10T09:21:44.499+0000",
          "nodeType": "cm:content",
          "content": {
            "mimeType": "image/gif",
            "mimeTypeName": "GIF Image",
            "sizeInBytes": 3039,
            "encoding": "UTF-8"
          },
          "parentId": "d32682f0-cfd9-43da-ab74-ba78fc59a01a",
          "createdAt": "2017-04-10T09:20:41.665+0000",
          "isFolder": false,
          "search": {
            "score": 2.0050006
          },
          "modifiedByUser": {
            "id": "test",
            "displayName": "Test User"
          },
          "name": "test.gif",
          "location": "nodes",
          "id": "4ba71ad8-8812-4c1a-9d0b-30643dc39c51"
        }
      },
      ...
    ]
  }
}

 

As well as the expected list of nodes, the response also contains a facetQueries and a facetsFields object containing the counts we requested. The facetQueries object has an entry for each query supplied in the result whereas the facetsFields object contains an entry for each requested field which in turn contains the count for each bucket.

 

The last example we're going to look at in this post is term highlighting. The example body below shows a search request (12th request in the Postman collection) that will look for content nodes that have a name or title starting with "test", if the match occurs in either the cm:name or cm:title property the location of the match will be returned in the results. By default, the matched term is highlighted by surrounded by an em tag, to surround the match with something else the prefix and postfix properties can be used as shown in the example below:

{
  "query": {
    "query": "(name:\"test*\" OR title:\"test*\") AND TYPE:\"cm:content\""
  },
  "highlight": {
    "fields": [
      {
        "field": "cm:name",
        "prefix": "(",
        "postfix": ")"
      },
      {
        "field": "{http://www.alfresco.org/model/content/1.0}title"
      }
    ]
  }
}

 

As the highlighting is specific to each individual result the search object we saw earlier is used to return the result as shown below:

{
  "list": {
    "pagination": {
      "count": 8,
      "hasMoreItems": false,
      "totalItems": 8,
      "skipCount": 0,
      "maxItems": 100
    },
    "entries": [
      {
        "entry": {
          "isFile": true,
          "createdByUser": {
            "id": "System",
            "displayName": "System"
          },
          "modifiedAt": "2017-02-20T10:57:28.407+0000",
          "nodeType": "cm:content",
          "content": {
            "mimeType": "application/x-javascript",
            "mimeTypeName": "JavaScript",
            "sizeInBytes": 2271,
            "encoding": "UTF-8"
          },
          "parentId": "a4e9e481-89b5-43da-9389-21314dbb6046",
          "createdAt": "2017-02-20T10:57:28.407+0000",
          "isFolder": false,
          "search": {
            "score": 1.1892484,
            "highlight": [
              {
                "field": "cm:name",
                "snippets": [
                  "example (test) script.js.sample"
                ]
              },
              {
                "field": "{http://www.alfresco.org/model/content/1.0}title",
                "snippets": [
                  "Example <em>Test</em> Script"
                ]
              }
            ]
          },
          "modifiedByUser": {
            "id": "System",
            "displayName": "System"
          },
          "name": "example test script.js.sample",
          "location": "nodes",
          "id": "7e02b810-4bce-4ed6-aff0-3f2f88a5ff82"
        }
      },
      ...
    ]
  }
}

 

As we specified in the request, the match in the name property is surrounded by brackets (line 35) and the em tag surrounds the match in the title property (line 41).

 

We've only just scratched the surface of the capabilities of the search API in this post so I would highly recommend you take a look at the API Explorer and select "Search API" from the drop-down menu to get more details of what's possible.

 

If you're using Community via the installer as instructed in the previous post you will have been using SOLR 4. You may have heard that we also released support for SOLR 6 with 5.2. To learn more please read the SOLR 6 blog posts on this site or visit our documentation site.

 

Next time we're going to take a look at the people API.

Load balancing a network protocol is something quite common nowadays. There are loads of ways to do that for HTTP for instance, and generally speaking all "single flow" protocols can be load-balanced quite easily. However, some protocols are not as simple as HTTP and require several connections. This is exactly what is FTP.

 

Reminder: FTP modes

Let's take a deeper look at the FTP protocol, in order to better understand how we can load-balance it. In order for an FTP client to work properly, two connections must be opened between the client and the server:

  • A control connection
  • A data connection

The control connection is initiated by the FTP client to the TCP port 21 on the server. On the other end, the data connection can be created in different ways. The first way is the through an "active" FTP session. In this mode the client sends a "PORT" command which randomly opens one of its network port and instruct the server to connect to it using port 20 as source port. This mode is usually discouraged or even server configuration prevent it for security reasons (the server initiate the data connection to the client). The second FTP mode is the "passive" mode. When using the passive mode a client sends a "PASV" command to the server. As a response the server opens a TCP port and sends the number and IP address as part of the PASV response so the client knows what socket to use. Modern FTP clients usually use this mode first if supported by the server. There is a third mode which is the "extended opassive" mode. It is very similar to the "passive" mode but the client sends an "EPSV" command (instead of "PASV") and the server respond with only the number of the TCP port that has been chosen for data connection (without sending the IP address).

 

Load balancing concepts

So now that we know how FTP works we also know that load-balancing FTP requires balancing both the control connections and the data connections. The load balancer must also make sure that data connections are sent the right backend server, the one which replied to the client command.

 

Alfresco configuration

From your ECM side, there is not much to do but there are some pre-requisites:

  • Alfresco nodes must belong to the same (working) cluster
  • Alfresco nodes must be reachable from the load balancer on the FTP ports
  • No FTP related properties should have been persisted in database

The Alfresco configuration presented bellow is valid for both load balancing method presented later. Technically every bit of this Alfresco configuration is not required, depending on the method you choose, but applying the config as shown will work on both cases.

First of all you should prefer setting FTP options in the alfresco-global.properties file as Alfresco cluster nodes have different settings, which you may not set using either the admin-console or the JMX interface.

If you have already set FTP parameters using JMX (or the admin-console), those parameters are persisted in the database and need to be remove from there (using the "revert" action in JMX for example).

Add the following to your alfresco-global.properties and restart Alfresco:

 

### FTP Server Configuration ###
ftp.enabled=true
ftp.port=2121
ftp.dataPortFrom=20000
ftp.dataPortTo=20009

 

ftp.dataPortFrom and ftp.dataPortTo properties need to be different on all servers. So if there were 2 Alfresco nodes alf1 and alf2, the properties for alf2 could be:

ftp.dataPortFrom=20010
ftp.dataPortTo=20019

 

Load balancing with LVS/Keepalived

 

Keepalived is a Linux based load-balancing system. It wraps the IPVS (also called LVS) software stack from the Linux-HA project and offer additional features like backend monitoring and VRRP redundancy. The schema bellow shows how Keepalived proceed with FTP load-balancing. It tracks control connection on port 21 and dynamically handles the data connections using a Linux kernel module called "ip_vs_ftp" which inspect the control connection in order to be aware of the port that will be used to open the data connection.

 

 

Configuration steps are quite simple.

 

First install the software:

sudo apt-get install keepalived

Then create a configuration file using the sample:

sudo cp /usr/share/doc/keepalived/samples/keepalived.conf.sample /etc/keepalived/keepalived.conf

Edit the newly created file in order to add a new virtual server and the associated backend servers: virtual_server

 

192.168.0.39 21 {

    delay_loop 6

    lb_algo rr

    lb_kind NAT

    protocol TCP

    real_server 10.1.2.101 2121 {

        weight 1

        TCP_CHECK {

            connect_port 2121

            connect_timeout 3

        }

    }

    real_server 10.1.2.102 2121 {

        weight 1

        TCP_CHECK {

            connect_port 2121

            connect_timeout 3

        }

    }

}

In a production environment you will most certainly want to use an additional VRRP instance to ensure a highly available load balancer. Please refer to the Keepalived documentation in order to set that up or just use the example given in the distribution files.

The example above defines a virtual server that listen on socket 192.168.0.39:21. Connections sent to this socket are redirected to backend servers using round-robin algorithm (others are available) and after masquerading source IP address. Additionally we need to load the FTP helper in order to track FTP data connections:

 

echo 'ip_vs_ftp' >> /etc/modules

It is important to note that this setup leverage the ftp kernel helper which reads the content of FTP frames. This means that it doesn't work when FTP is secured using SSL/TLS

 

Secure FTP load-balancing

 

Before you go any further:

 

This method has a huge advantage: it can handle FTPs (SSL/TLS). However, it also have a big disadvantage: it doesn't work when the load balancer behaves as a NAT gateway (which is basically what HAProxy does).
This is mainly because at the moment Alfresco doesn't comply with the necessary pre-requisites for secure FTP to work.

 

Some FTP clients may work even with this limitation. It may happen to work if server is using ipv6 or for clients using the "Extended Passive Mode" on ipv4 (which is normally used for ipv6 only). To better understand how, please see FTP client and passive session behing a NAT.

 

This means that what's bellow will mainly only work with macOSX ftp command line and probably no other FTP client!

Don't spend time on it and use previous method if you need other FTP clients or if you have no control over what FTP client your users have.

 

Load balancing with HAProxy

 

This method can also be adapted to Keepalived using iptables mangling and "fwmark" (see Keepalived secure FTP), but you should only need it if you are bound to FTPs as normal FTP is much better handled by previous method.

HAProxy is a modern and widely used load balancer. It provides similar features as Keepalived and much more. Nevertheless HAProxy is not able to track data connections as related to the global FTP session. For this reason we have to trick the FTP protocol in order to provide connection consistency within the session. Basically we will split the load balancing in several parts:

  • control connection load-balancing
  • data connection load balancing or each backend server

So if we have 2 backend servers - as shown in the schema bellow - we will create 3 load balancing connection pools (let's called it like this for now).

 

 

First install the software:

sudo apt-get install haproxy

HAProxy has the notion of "frontends" and "backends". Frontends allow to define specific sockets (or set of sockets) each of which can be linked to different backends. So we can use the configuration bellow:

frontend alfControlChannel

    bind *:21

    default_backend alfPool

frontend alf1DataChannel

    bind *:20000-20009

    default_backend alf1

frontend alf2DataChannel

    bind *:20010-20019

    default_backend alf2

backend alfPool

    server alf1 10.1.2.101:2121 check port 2121 inter 20s

    server alf2 10.1.2.102:2121 check port 2121 inter 20s

backend alf1

    server alf1 10.1.2.101:2121 check port 2121 inter 20s

backend alf2

    server alf2 10.1.2.102:2121 check port 2121 inter 20s

 

So in this case the frontend that handle the control connection load-balancing (alfControlChannel) alternatively sends requests to all backend server (alfPool). Each server (alf1 & alf2) will negotiate a data transfer socket on a different frontend (alf1DataChannel & alf2DataChannel). Each of this frontend will only forward data connection to the only corresponding backend (alf1 or alf2), thus making the load balancing sticky. And... job done!

Filter Blog

By date: By tag: