Alfresco SDK 5.0 is a development kit that provides an easy to use approach to developing applications and out-of-process extensions for Content Services 7.x. With this SDK you can develop, package, test, run, document and release your extension project.
The following picture illustrates where SDK 5.x fits into the big picture:

The SDK is a fundamental tool provided by Alfresco to developers to build customizations and extensions for the Content Services Platform. It is based on theSpring Integration tooling library and theSpring Boot library.
Alfresco SDK 5.0 is released underApache License version 2.0 and supports Content Services Enterprise Edition as well as Community Edition. If you’re an Enterprise customer, please check theAlfresco SDK Support status for the version you’re using. If your version is in Limited or Full Support and you need help, contact ourSupport team.
The 5.0 release takes advantage of Semantic Versioning (SEMVER), which means that this new release is not directly compatible with the previous releases of the SDK.
There is no direct upgrade path from previous versions of the SDK. This is because version 5.0 is an additional SDK to support out-of-process extensions, rather than an updated 4.2 version.Alfresco SDK 4.2 is still needed for a lot of the extension points, such ascontent modelling.
If you have an existing project with business logic that could be lifted out and implemented as an external service, then the recommended approach is to create a new SDK 5.0 project and start using the event system to implement the business logic. Any business logic that is executed as a result of an action in the Repository, such as document or folder uploaded, updated, deleted,can be reimplemented as an external out-process extension utilizing the new event system.
Alfresco SDK 5.0 is a brand new development environment that brings changes oriented to assist the way out-of-process customizations are built, packaged, run and tested for Content Services 7.
This is a major release oriented to support Content Services 7, so it is not compatible with previous versions of the SDK or Content Services.
The SDK includes Java libraries for the following:
Make sure to read through thePlatform architecturebefore continuing as this section assumes familiarity with the Content Services architecture and event system.
If you are not familiar with Alfresco ReST API version 1.0, then read through thisintroduction.
The SDK has a Java library that wraps the Alfresco Event Model so it is more convenient to handle events in a Java project. This library provides the ability to work with events in a standard Java way or with Spring Integration.
The Alfresco Java Event API is consists of four main components:
The event model is a component that offers a custom model definition to clearly specify the way the Alfresco event data is organized.
This component is declared in the modulealfresco-java-event-api-model and is explained in detailhere.
The event handling library is a core component of the Alfresco Java Event API that offers a set of pre-defined event handling interfaces and the classes required to properly work with them. The idea of this library is to ease the implementation of business logic that must be executed as a response to an Alfresco repository event.
This component is defined in the modulealfresco-java-event-api-handling. The classes and interfaces of this library were designed to be as Java technology agnostic as possible. They offer the plain event handling functionality doing no assumptions about the technology used to make them work together. They’re mostly plain Java classes, so the integrator can use them in a Spring project, a Dagger project or any other technology.
The main four items in this library are explained in the following sections.
TheEventHandler interface defines the contract for an Alfresco repository event handler implementation.
This contract has been reduced to a minimum:
A hierarchy of interfaces that extendEventHandler have been defined to cover the different types of Alfresco repository events that can be triggered by the Content Services event system:
OnNodeCreatedEventHandler.OnNodeUpdatedEventHandler.OnNodeDeletedEventHandler.OnChildAssocCreatedEventHandler.OnChildAssocDeletedEventHandler.OnPeerAssocCreatedEventHandler.OnPeerAssocDeletedEventHandler.OnPermissionUpdatedEventHandler.TheEventHandlingRegistry is a class that registers theEventHandlers that must be executed in response to each repository event type.
TheEventHandlingExecutor is an interface that defines the process to execute the event handlers when events are received.
Currently, there is only one implementation (SimpleEventHandlingExecutor)of this interface. It simply uses theEventHandlingRegistryto get the list ofEventHandlers to execute when a specific repository event is triggered and executes them synchronously one by one.
Event filters can be used in theEvent handler implementations to narrow down the conditions required to execute the business logic (i.e. the code) in response to a repository event being triggered.
TheEventFilter is an interface that defines the contract that must be fulfilled by a repository event. It is basically a predicate interface that allows the developer to easily define conditions that an event must match for the code to be executed.
A number of filter implementations are offered out-of-the-box, covering the most common use cases:
AspectAddedFilter - checks if an event corresponds to a repository node that has had specified aspect added.AspectRemovedFilter - checks if an event corresponds to a repository node that has had specified aspect removed.AssocTypeFilter - checks if an event corresponds to a specific association type. This doesn’t distinguish if the event is representing a peer-peer or parent-child association.ContentAddedFilter - checks if an event represents the addition of content (i.e. a file) to an existingcm:content node in the repository.ContentChangedFilter - checks if an event represents a content update (i.e. file updated) of acm:content node in the repository.IsFileFilter - checks if an event corresponds to a repository node of typecm:content or subtype (i.e. a file).IsFolderFilter - checks if an event corresponds to a repository node of typecm:folder or subtype (i.e. a folder).MimeTypeFilter - checks if an event represents a content node (i.e.cm:content) with a specific MIME type.NodeAspectFilter - checks if an event represents a node with a specific aspect.NodeMovedFilter - checks if an event represents a node being moved in the repository.NodeTypeChangedFilter - checks if an event represents the change of the type of a node in the repository.NodeTypeFilter - checks if an event represents a node with a specific type.PropertyAddedFilter - checks if an event corresponds to the addition of a node property in the repository.PropertyChangedFilter - checks if an event corresponds to the update of a node property in the repository.PropertyCurrentValueFilter - checks if an event represents a node with a specific property with a specific current value.PropertyRemovedFilter - checks if an event corresponds to the removal of a specific property to a node in the repository.PropertyPreviousValueFilter - checks if an event represents a node with a specific property with a specific previous value.PropertyValueFilter - checks if an event represents a node with a specific property with a specific value.You can use these filters alone or combine several of them into one complex filter.
For instance, you can create a complex filter to react to an event related to the modification of the title of content typecm:content with a MIME type oftext/html:
public EventFilter getEventFilter() { return PropertyChangedFilter.of("cm:title") // If title changed .and(NodeTypeFilter.of("cm:content")) // And it's a file .and(MimeTypeFilter.of("text/html")); // And the file is a HTML file}It’s also possible to implementcustom event filters.
The Spring Integration tooling library component offers some utility classes that ease the handling of Alfresco events in the context of a Spring Integration application.
This component is defined in the modulealfresco-java-event-api-integration.
It makes use of the event handling library and the event model to offer integration features, making the assumption that the integrator is working in the context of a Spring Integration project.
The way the events are consumed from the ActiveMQ topic, where the Alfresco event system is currently publishing them, is not specified at this level of integration. This is intentionally left open to the developer’s choice. For a more opinionated integration level, take a look at theSpring Boot custom starter section.
Once the JSON events are ingested in a Spring Integration channel, this library offers a transformer to translate from the JSON schema defined by the Alfresco Event Model to the Java POJO classes defined in it (i.e.RepoEvent).
Apart from that, this module offers a wrapper of theEventFilterinterface as a Spring Integration filter (GenericSelector) to be able to easily use all the filter offering of the handling library in a Spring Integration context.
The Spring Boot custom starter component defines a personalized Spring Boot starter that will automatically configure all the beans and property defaults for an Alfresco Event system client, making it easy to implement a client for the Alfresco Java Event API. As expected, the use of this component makes the assumption that the developer is creating an integration in the context of a Spring Boot application.
This component is defined in thealfresco-java-event-api-spring-boot-starter and thealfresco-java-event-api-spring-boot modules.
The core class of this module isAlfrescoEventsAutoConfiguration.It is a Spring configuration class that automatically define the beans required to do the following actions:
RepoEvent object.alfresco.events.enableSpringIntegration is enabled.alfresco.events.enableHandlers is enabled.All this auto-configuration is enabled as soon as the dependencyorg.alfresco:alfresco-java-event-api-spring-boot-starter is added to a Spring Boot project.
There are some data mapping objects that are good to know about when working with the Event API. They wrap the JSON payload data from event messages.
When working with the Event API and folders and files there is one data object calledNodeResource that is used over and over. It’s used to get to the JSON node data returned in the JMS message payload.
Here is an example payload for a filenode updated event:
{ "specversion": "1.0", "type": "org.alfresco.event.node.Updated", "id": "ae5dac3c-25d0-438d-b148-2084d1ab05a6", "source": "/08d9b620-48de-4247-8f33-360988d3b19b", "time": "2021-01-26T10:29:42.99524Z", "dataschema": "https://api.alfresco.com/schema/event/repo/v1/nodeUpdated", "datacontenttype": "application/json", "data": { "eventGroupId": "b5b1ebfe-45fc-4f86-b71b-421996482881", "resource": { "@type": "NodeResource", "id": "d71dd823-82c7-477c-8490-04cb0e826e65", "primaryHierarchy": [ "5f355d16-f824-4173-bf4b-b1ec37ef5549", "93f7edf5-e4d8-4749-9b4c-e45097e2e19d", "c388532e-8da6-4d50-a6d2-4f3f3ac36ff7", "2fa2cde5-9d83-4460-a38c-cfe4ec9cca08" ], "name": "purchase-order-scan.pdf", "nodeType": "cm:content", "createdByUser": { "id": "admin", "displayName": "Administrator" }, "createdAt": "2021-01-21T11:14:15.695Z", "modifiedByUser": { "id": "admin", "displayName": "Administrator" }, "modifiedAt": "2021-01-26T10:29:42.529Z", "content": { "mimeType": "application/pdf", "sizeInBytes": 531152, "encoding": "UTF-8" }, "properties": { "cm:autoVersion": true, "cm:title": "Purchase Order", "cm:versionType": "MAJOR", "cm:versionLabel": "1.0", "cm:autoVersionOnUpdateProps": false, "cm:lastThumbnailModification": [ "doclib:1611227666770" ], "cm:description": "", "cm:taggable": null, "cm:initialVersion": true }, "aspectNames": [ "cm:versionable", "cm:author", "cm:thumbnailModification", "cm:titled", "rn:renditioned", "cm:auditable", "cm:taggable" ], "isFolder": false, "isFile": true }, "resourceBefore": { "@type": "NodeResource", "modifiedAt": "2021-01-21T11:14:25.223Z", "properties": { "cm:title": null, "cm:taggable": null, "cm:description": null }, "aspectNames": [ "cm:versionable", "cm:author", "cm:thumbnailModification", "cm:titled", "rn:renditioned", "cm:auditable" ] }, "resourceReaderAuthorities": [ "GROUP_EVERYONE" ], "resourceDeniedAuthorities": [] }}In an event handler, being it pure Java or Spring Integration based, we can get to the payload data via theorg.alfresco.event.sdk.model.v1.model.NodeResource object:
public class ContentUpdatedEventHandler implements OnNodeUpdatedEventHandler { public void handleEvent(final RepoEvent<DataAttributes<Resource>> repoEvent) { // Get the data for the node as it looked like before the update NodeResource beforeUpdateResource = (NodeResource) repoEvent.getData().getResourceBefore(); ZonedDateTime prevModificationDate = beforeUpdateResource.getModifiedAt(); LOGGER.info("Before this update the node was last updated {}", prevModificationDate); Set<String> beforeAspects = beforeUpdateResource.getAspectNames(); if (beforeAspects != null) { LOGGER.info("Aspects before the update: "); for (String aspectName : beforeAspects) { LOGGER.info(" {}", aspectName); } } Map<String, Serializable> beforeProperties = beforeUpdateResource.getProperties(); if (beforeProperties != null) { LOGGER.info("Properties before the update"); for (Map.Entry<String, Serializable> property : beforeProperties.entrySet()) { LOGGER.info(" {} = {}", property.getKey(), property.getValue()); } } // Get the latest data for the node NodeResource afterUpdateResource = (NodeResource) repoEvent.getData().getResource(); LOGGER.info("Node data after update:"); LOGGER.info(" ID: {}", afterUpdateResource.getId()); LOGGER.info(" Name (cm:name): {}", afterUpdateResource.getName()); LOGGER.info(" Content Model Type: {}", afterUpdateResource.getNodeType()); LOGGER.info(" Created date (cm:created): {}", afterUpdateResource.getCreatedAt()); LOGGER.info(" Created by (cm:creator): {}", afterUpdateResource.getCreatedByUser().getDisplayName()); LOGGER.info(" Modified date (cm:modified): {}", afterUpdateResource.getModifiedAt()); LOGGER.info(" Modified by (cm:modifier): {}", afterUpdateResource.getModifiedByUser().getDisplayName()); if (afterUpdateResource.getContent() != null) { LOGGER.info(" Content (cm:content): {}, {}, {} bytes", afterUpdateResource.getContent().getMimeType(), afterUpdateResource.getContent().getEncoding(), afterUpdateResource.getContent().getSizeInBytes()); } Set<String> afterAspects = afterUpdateResource.getAspectNames(); LOGGER.info("Aspects after the update"); for (String aspectName: afterAspects) { LOGGER.info(" {}", aspectName); } Map<String, Serializable> afterProperties = afterUpdateResource.getProperties(); LOGGER.info("Properties after the update"); for (Map.Entry<String, Serializable> property: afterProperties.entrySet()) { LOGGER.info(" {} = {}", property.getKey(), property.getValue()); } // Get the node location hierarchy in the repository // Use the ReST API to query for the name of the nodes List<String> nodeHierarchy = afterUpdateResource.getPrimaryHierarchy(); LOGGER.info("Node location hierarchy (immediate parent node first):"); for (String nodeID: nodeHierarchy) { LOGGER.info(" {}", nodeID); } }}The permission related propertiesresourceReaderAuthorities andresourceDeniedAuthorities will be listed as part ofresource.getProperties(). Note that these will only be present if you are running an Enterprise Edition of Alfresco version 7 or later.
The folder primary hierarchy can be resolved by using the ReST API to get the names for the different Node IDs.The first node ID in the list is the immediate parent folder for the node as in the following example:
"id": "d71dd823-82c7-477c-8490-04cb0e826e65", /app:company_home/cm:Testing/cm:Inbound/cm:purchase-order-scan.pdf (cm:content) "primaryHierarchy": [ "5f355d16-f824-4173-bf4b-b1ec37ef5549", /app:company_home/cm:Testing/cm:Inbound (cm:folder) "93f7edf5-e4d8-4749-9b4c-e45097e2e19d", /app:company_home/cm:Testing (cm:folder) "c388532e-8da6-4d50-a6d2-4f3f3ac36ff7", /app:company_home (cm:folder) "2fa2cde5-9d83-4460-a38c-cfe4ec9cca08" Store root (sys:store_root)When working with the Event API and Parent-Child associations there is one data object calledChildAssociationResource that is used over and over. It’s used to get to the JSON association data returned in the JMS message payload.
Here is an example payload for aParent-Child association created event:
{ "specversion": "1.0", "type": "org.alfresco.event.assoc.child.Created", "id": "4014bcb2-f1e6-447f-8caa-3a6219bc94ad", "source": "/08d9b620-48de-4247-8f33-360988d3b19b", "time": "2021-01-28T13:42:34.329162Z", "dataschema": "https://api.alfresco.com/schema/event/repo/v1/childAssocCreated", "datacontenttype": "application/json", "data": { "eventGroupId": "78da21cc-fa5a-47d1-afcb-03005229efa9", "resource": { "@type": "ChildAssociationResource", "assocType": "fdk:images", "parent": { "id": "a4eb7684-0ffe-4bf5-b6f7-4297a6e4ee84" }, "child": { "id": "ceb3c804-8b32-4050-b2da-b55c47f01666" } } }}In an event handler, being it pure Java or Spring Integration based, we can get to the payload data via theorg.alfresco.event.sdk.model.v1.model.ChildAssociationResource object:
public class ParentChildAssocCreatedEventHandler implements OnChildAssocCreatedEventHandler { public void handleEvent(final RepoEvent<DataAttributes<Resource>> repoEvent) { ChildAssociationResource resource = (ChildAssociationResource) repoEvent.getData().getResource(); LOGGER.info("A secondary Parent-Child association of type {} was created between nodes: {} -> {}", resource.getAssocType(), resource.getParent().getId(), resource.getChild().getId()); }}Theresource.getParent().getId() call will return the Node ID for the parent node in the association and theresource.getChild().getId() call will return the Node ID for the child node.
When working with the Event API and Peer-2-Peer associations there is one data object calledPeerAssociationResource that is used over and over. It’s used to get to the JSON association data returned in the JMS message payload.
Here is an example payload for aPeer-2-Peer association created event:
{ "specversion": "1.0", "type": "org.alfresco.event.assoc.peer.Created", "id": "8a8113a2-fa67-4914-9ecb-2ec47c456159", "source": "/08d9b620-48de-4247-8f33-360988d3b19b", "time": "2021-01-28T13:42:34.352956Z", "dataschema": "https://api.alfresco.com/schema/event/repo/v1/peerAssocCreated", "datacontenttype": "application/json", "data": { "eventGroupId": "78da21cc-fa5a-47d1-afcb-03005229efa9", "resource": { "@type": "PeerAssociationResource", "assocType": "fdk:reviews", "source": { "id": "a4eb7684-0ffe-4bf5-b6f7-4297a6e4ee84" }, "target": { "id": "f826ac49-0262-48af-8f63-f87eb7007078" } } }}In an event handler, being it pure Java or Spring Integration based, we can get to the payload data via theorg.alfresco.event.sdk.model.v1.model.PeerAssociationResource object:
public class Peer2PeerAssocCreatedEventHandler implements OnPeerAssocCreatedEventHandler { public void handleEvent(final RepoEvent<DataAttributes<Resource>> repoEvent) { PeerAssociationResource resource = (PeerAssociationResource) repoEvent.getData().getResource(); LOGGER.info("A Peer-Peer association was created: Assoc Type {}: Source {} -> Target {}", resource.getAssocType(), resource.getSource().getId(), resource.getTarget().getId()); }}Theresource.getSource().getId() call will return the Node ID for the source node in the association and theresource.getTarget().getId() call will return the Node ID for the target node.
In this section we will see how to use SDK 5 to create Alfresco event handler projects, using plain Java and using the Spring framework.
Before continuing you need an instance of Content Services version 7 running, either Community or Enterprise. In this samples section we will use Community and start it up with Docker Compose. You can get the Docker Compose file for Community version 7 from theacs-deployment GitHub project.
Put the Docker Compose YAML file in a directory and then start it all up with the following command:
$ ls -ltotal 8-rw-r--r-- 1 mbergljung staff 4006 26 Mar 09:43 docker-compose.yml$ docker-compose up...During start up you can see Apache Active MQ starting:
activemq_1 | INFO | Apache ActiveMQ 5.16.1 (localhost, ID:6906413a8893-36895-1616752022615-0:1) is startingactivemq_1 | INFO | Listening for connections at: tcp://6906413a8893:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600...Note down the ActiveMQ Broker TCP access point (i.e.tcp://localhost:61616), we will need it later when configuring the event application.
It will take 5-15 minutes to start up, depending on what Docker images you already have locally. Make sure everything has started properly by accessinghttp://localhost:8080/share and login withadmin/admin.
Before you start using any of the libraries in SDK 5 make sure you got the correct Java and Maven versions installed:
Java needs to be version 11 or above:
$ java -versionjava version "11.0.2" 2019-01-15 LTSJava(TM) SE Runtime Environment 18.9 (build 11.0.2+9-LTS)Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.2+9-LTS, mixed mode)$ javac -versionjavac 11.0.2Maven needs to be version 3.3 or above:
$ mvn -versionApache Maven 3.5.0 (ff8f5e7444045639af65f6095c62210b5713f426; 2017-04-03T20:39:06+01:00)Maven home: /Users/mbergljung/Tools/apache-maven-3.5.0Java version: 11.0.2, vendor: Oracle CorporationJava home: /Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/HomeDefault locale: en_GB, platform encoding: UTF-8OS name: "mac os x", version: "10.16", arch: "x86_64", family: "mac"The Java artifacts (i.e. JAR libs) that we will be using are located in theAlfresco Nexus repo:

Maven needs to know about the Alfresco Artifacts Repository (Nexus) so add the following to~/.m2/settings.xml:
<repositories> <repository> <id>alfresco-public</id> <url>https://artifacts.alfresco.com/nexus/content/groups/public</url> </repository> </repositories>The easiest way to get going is to use theSpring Initializr website and create a starting point project from there.
Before you start, make sure you’re familiar with Spring Boot and the Maven project structure.
Go tohttps://start.spring.io/ and fill in your project info something like this:

Make the following changes in your project:
pom.xml) so it uses the Alfresco Java SDK (i.e. SDK 5).org/alfresco/tutorial/sdk5demo/Sdk5DemoApplicationTests.java).Your project file should look like this now:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- Alfresco Java SDK 5 Parent --> <parent> <groupId>org.alfresco</groupId> <artifactId>alfresco-java-sdk</artifactId> <version>5.0.0</version> </parent> <groupId>org.alfresco.tutorial</groupId> <artifactId>sdk5-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>sdk5-demo</name> <description>Demo showing use of Alfresco SDK 5 libraries</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>Note: Skip this section if you are just creating a project for a Java ReST API client.
If you plan to create an event handler project, you’ll need to set a number of properties to:
src/main/resources/application.properties configuration file. Remember, the Active MQ server is started as part of theContent Services system.# Where is Alfresco Active MQ JMS Broker running?spring.activemq.brokerUrl=tcp://localhost:61616# This property is required if you want Spring Boot to auto-define the ActiveMQConnectionFactory, # otherwise you can define that bean in Spring configspring.jms.cache.enabled=falseNote: Skip this section if you are just creating a project for an Event handler client.
If you plan to create a project for a Java ReST API client, set the following properties to tell the ReST API client where the Content Services server is running and the endpoints for the ReST APIs:
# HTTP Basic Authentication that will be used by the APIcontent.service.security.basicAuth.username=admincontent.service.security.basicAuth.password=admin# Location of the server and API endpointscontent.service.url=http://localhost:8080content.service.path=/alfresco/api/-default-/public/alfresco/versions/1search.service.path=/alfresco/api/-default-/public/search/versions/1Now package the project and make sure it builds properly (skip the license test as none of the files have a license header):
$ mvn clean package -Dlicense.skip=true[INFO] Scanning for projects...[INFO] [INFO] ------------------------------------------------------------------------[INFO] Building sdk5-demo 0.0.1-SNAPSHOT[INFO] ------------------------------------------------------------------------[INFO] [INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ sdk5-demo ---[INFO] Deleting /Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-demo/target[INFO] [INFO] --- license-maven-plugin:3.0:check (validate-license) @ sdk5-demo ---[INFO] [INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ sdk5-demo ---[INFO] Using 'UTF-8' encoding to copy filtered resources.[INFO] Using 'UTF-8' encoding to copy filtered properties files.[INFO] Copying 1 resource[INFO] Copying 0 resource[INFO] [INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ sdk5-demo ---[INFO] Changes detected - recompiling the module![INFO] Compiling 1 source file to /Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-demo/target/classes[INFO] [INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ sdk5-demo ---[INFO] Using 'UTF-8' encoding to copy filtered resources.[INFO] Using 'UTF-8' encoding to copy filtered properties files.[INFO] skip non existing resourceDirectory /Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-demo/src/test/resources[INFO] [INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ sdk5-demo ---[INFO] Changes detected - recompiling the module![INFO] [INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ sdk5-demo ---[INFO] [INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ sdk5-demo ---[INFO] Building jar: /Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-demo/target/sdk5-demo-0.0.1-SNAPSHOT.jar[INFO] [INFO] --- spring-boot-maven-plugin:2.4.2:repackage (repackage) @ sdk5-demo ---[INFO] Replacing main artifact with repackaged archive[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time: 5.364 s[INFO] Finished at: 2021-04-07T14:12:53+01:00[INFO] Final Memory: 24M/90M[INFO] ------------------------------------------------------------------------You have successfully generated your first Spring Boot project.
Now that the JAR file has been created, in this caseevents-0.0.1-SNAPSHOT.jar, try and run the Spring Boot app:
$ java -jar target/sdk5-demo-0.0.1-SNAPSHOT.jar . ____ _ __ _ _/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \\\/ ___)| |_)| | | | | || (_| | ) ) ) )' |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/:: Spring Boot :: (v2.4.2)2021-04-07 14:15:13.514 INFO 52906 --- [ main] o.a.t.sdk5demo.Sdk5DemoApplication : Starting Sdk5DemoApplication v0.0.1-SNAPSHOT using Java 11.0.2 on MBP512-MBERGLJUNG-0917 with PID 52906 (/Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-demo/target/sdk5-demo-0.0.1-SNAPSHOT.jar started by mbergljung in /Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-demo)2021-04-07 14:15:13.520 INFO 52906 --- [ main] o.a.t.sdk5demo.Sdk5DemoApplication : No active profile set, falling back to default profiles: default2021-04-07 14:15:14.471 INFO 52906 --- [ main] o.a.t.sdk5demo.Sdk5DemoApplication : Started Sdk5DemoApplication in 1.697 seconds (JVM running for 2.257)During development it’s useful to be able to build and run the extension in one go (so you don’t forget to build…). This can be done using thespring-boot-maven-plugin as follows:
$ mvn spring-boot:run -Dlicense.skip=trueWe are now ready to add the specifics depending on what type of project we are going to develop:
Make sure you have completedprerequisites and created astarter project.
To use pure Java event handlers follow these steps:
Add the following dependency in the Maven project file (i.e.pom.xml):
<dependencies> <!-- Alfresco Java SDK 5 Java Event Handler API Spring Boot Starter --> <dependency> <groupId>org.alfresco</groupId> <artifactId>alfresco-java-event-api-spring-boot-starter</artifactId> <version>5.0.0</version> </dependency></dependencies>Remove the default Spring Boot starter dependency (i.e.<artifactId>spring-boot-starter</artifactId>).
Test it:
$ mvn clean package -Dlicense.skip=true[INFO] Scanning for projects......$ java -jar target/events-0.0.1-SNAPSHOT.jar . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.4.2)2021-03-26 09:19:45.766 INFO 73086 --- [ main] o.a.tutorial.events.EventsApplication : Starting EventsApplication v0.0.1-SNAPSHOT using Java 11.0.2 on MBP512-MBERGLJUNG-0917 with PID 73086 (/Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-pure-java-events-sample/target/events-0.0.1-SNAPSHOT.jar started by mbergljung in /Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-pure-java-events-sample)2021-03-26 09:19:45.769 INFO 73086 --- [ main] o.a.tutorial.events.EventsApplication : No active profile set, falling back to default profiles: default2021-03-26 09:19:46.593 INFO 73086 --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'errorChannel' has been explicitly defined. Therefore, a default PublishSubscribeChannel will be created.2021-03-26 09:19:46.598 INFO 73086 --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'taskScheduler' has been explicitly defined. Therefore, a default ThreadPoolTaskScheduler will be created.2021-03-26 09:19:46.606 INFO 73086 --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'integrationHeaderChannelRegistry' has been explicitly defined. Therefore, a default DefaultHeaderChannelRegistry will be created.2021-03-26 09:19:46.722 INFO 73086 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.integration.config.IntegrationManagementConfiguration' of type [org.springframework.integration.config.IntegrationManagementConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)2021-03-26 09:19:46.738 INFO 73086 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'integrationChannelResolver' of type [org.springframework.integration.support.channel.BeanFactoryChannelResolver] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)2021-03-26 09:19:46.739 INFO 73086 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'integrationDisposableAutoCreatedBeans' of type [org.springframework.integration.config.annotation.Disposables] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)2021-03-26 09:19:47.537 INFO 73086 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'2021-03-26 09:19:47.601 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the 'errorChannel' channel2021-03-26 09:19:47.602 INFO 73086 --- [ main] o.s.i.channel.PublishSubscribeChannel : Channel 'application.errorChannel' has 1 subscriber(s).2021-03-26 09:19:47.602 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean '_org.springframework.integration.errorLogger'2021-03-26 09:19:47.602 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {transformer} as a subscriber to the 'acsEventsListeningFlow.channel#0' channel2021-03-26 09:19:47.602 INFO 73086 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.acsEventsListeningFlow.channel#0' has 1 subscriber(s).2021-03-26 09:19:47.602 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsListeningFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#0'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsListeningFlow'2021-03-26 09:19:47.602 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {recipient-list-router} as a subscriber to the 'acsEventsListeningFlow.channel#1' channel2021-03-26 09:19:47.602 INFO 73086 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.acsEventsListeningFlow.channel#1' has 1 subscriber(s).2021-03-26 09:19:47.602 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsListeningFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#1'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsListeningFlow'2021-03-26 09:19:47.603 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {bridge} as a subscriber to the 'alfresco.events.si.channel' channel2021-03-26 09:19:47.603 INFO 73086 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.alfresco.events.si.channel' has 1 subscriber(s).2021-03-26 09:19:47.603 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsSpringIntegrationFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#0'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsSpringIntegrationFlow'2021-03-26 09:19:47.603 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {bridge} as a subscriber to the 'acsEventsSpringIntegrationFlow.channel#1' channel2021-03-26 09:19:47.603 INFO 73086 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.acsEventsSpringIntegrationFlow.channel#1' has 1 subscriber(s).2021-03-26 09:19:47.603 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsSpringIntegrationFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#1'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsSpringIntegrationFlow'2021-03-26 09:19:47.603 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {bridge} as a subscriber to the 'alfresco.events.handlers.channel' channel2021-03-26 09:19:47.603 INFO 73086 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.alfresco.events.handlers.channel' has 1 subscriber(s).2021-03-26 09:19:47.603 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsHandlersFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#0'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsHandlersFlow'2021-03-26 09:19:47.603 INFO 73086 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.acsEventsHandlersFlow.channel#1' has 1 subscriber(s).2021-03-26 09:19:47.604 INFO 73086 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsHandlersFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#1'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsHandlersFlow'2021-03-26 09:19:47.604 INFO 73086 --- [ main] ishingJmsMessageListener$GatewayDelegate : started org.springframework.integration.jms.ChannelPublishingJmsMessageListener$GatewayDelegate@53812a9b2021-03-26 09:19:47.784 INFO 73086 --- [ main] o.s.i.jms.JmsMessageDrivenEndpoint : started bean 'acsEventsListeningFlow.jms:message-driven-channel-adapter#0'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsListeningFlow'2021-03-26 09:19:47.796 INFO 73086 --- [ main] o.a.tutorial.events.EventsApplication : Started EventsApplication in 2.509 seconds (JVM running for 3.005)Looks ready for some event handler code.
Now, start adding your event handler code, let’s add an event handler that will be triggered when a new document/file is uploaded. To do this we need to create a class that implements theorg.alfresco.event.sdk.handling.handler.OnNodeCreatedEventHandlerevent handler interface:
package org.alfresco.tutorial.events;import org.alfresco.event.sdk.handling.handler.OnNodeCreatedEventHandler;import org.alfresco.event.sdk.model.v1.model.DataAttributes;import org.alfresco.event.sdk.model.v1.model.NodeResource;import org.alfresco.event.sdk.model.v1.model.RepoEvent;import org.alfresco.event.sdk.model.v1.model.Resource;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;/** * Sample event handler to demonstrate reacting to a document/file being uploaded to the repository. */@Componentpublic class ContentUploadedEventHandler implements OnNodeCreatedEventHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ContentUploadedEventHandler.class); public void handleEvent(final RepoEvent<DataAttributes<Resource>> repoEvent) { NodeResource nodeResource = (NodeResource) repoEvent.getData().getResource(); LOGGER.info("A file was uploaded to the repository: {}, {}, {}", nodeResource.getId(), nodeResource.getNodeType(), nodeResource.getName()); }}Add the Spring Bean class into the same directory as the Spring Boot starter class. It doesn’t have to be added to this directory, but in this case we are just testing it, so no need to organize too much.
Now stop, build and start it up again:
$ ^C...$ mvn spring-boot:run -Dlicense.skip=true...Add a file via the Share user interface, you should see the following in the logs:
2021-03-26 10:23:46.846 INFO 74020 --- [erContainer#0-1] o.a.t.e.ContentUploadedEventHandler : A file was uploaded to the repository: 13ba2bbf-2422-4152-832f-060e017ec09c, cm:content, some-file.txtNow, this event handler will actually also be triggered when a folder is created. So how can we fix so the handler is only triggered when a file is created/uploaded? By adding a so calledevent filter to the class:
package org.alfresco.tutorial.events;import org.alfresco.event.sdk.handling.filter.EventFilter;import org.alfresco.event.sdk.handling.filter.IsFileFilter;import org.alfresco.event.sdk.handling.handler.OnNodeCreatedEventHandler;import org.alfresco.event.sdk.model.v1.model.DataAttributes;import org.alfresco.event.sdk.model.v1.model.NodeResource;import org.alfresco.event.sdk.model.v1.model.RepoEvent;import org.alfresco.event.sdk.model.v1.model.Resource;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;/** * Sample event handler to demonstrate reacting to a document/file being uploaded to the repository. */@Componentpublic class ContentUploadedEventHandler implements OnNodeCreatedEventHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ContentUploadedEventHandler.class); public void handleEvent(final RepoEvent<DataAttributes<Resource>> repoEvent) { NodeResource nodeResource = (NodeResource) repoEvent.getData().getResource(); LOGGER.info("A file was uploaded to the repository: {}, {}, {}", nodeResource.getId(), nodeResource.getNodeType(), nodeResource.getName()); } public EventFilter getEventFilter() { return IsFileFilter.get(); }}Here we are using theorg.alfresco.event.sdk.handling.filter.IsFileFilter, which will make sure that the event handleris triggered only when the node type iscm:content or subtype thereof, which represents files.
For a complete list of events with sample code see theevents extension point documentation. For a complete list of Event Filters available in the SDK see thissection.
For information on how to implement a custom event filter see thissection.
For more information about how to extract all the properties from the message payload seeNodeResource info.
Make sure you have completedprerequisites and created astarter project.
To use Spring Integration based event handlers follow these steps:
Add the following dependency in the Maven project file (i.e.pom.xml):
<dependencies> <!-- Alfresco Java SDK 5 Spring Integration Event Handler API Spring Boot Starter --> <dependency> <groupId>org.alfresco</groupId> <artifactId>alfresco-java-event-api-spring-boot-starter</artifactId> <version>5.0.0</version> </dependency></dependencies>Enable Spring Integration handlers in thesrc/main/resources/application.properties configuration file (by default pure Java event handlers is expected), add the following two extra properties:
# Where is Alfresco Active MQ JMS Broker running?spring.activemq.brokerUrl=tcp://localhost:61616# This property is required if you want Spring Boot to auto-define the ActiveMQConnectionFactory,# otherwise you can define that bean in Spring configspring.jms.cache.enabled=false# Enable Spring Integration based event handlersalfresco.events.enableSpringIntegration=true# Turn off plain Java event handlersalfresco.events.enableHandlers=falseRemove the default Spring Boot starter dependency (i.e.<artifactId>spring-boot-starter</artifactId>).
Test it:
$ mvn clean package -Dlicense.skip=true[INFO] Scanning for projects......$ java -jar target/events-0.0.1-SNAPSHOT.jar . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.4.2)2021-03-29 13:44:49.441 INFO 2599 --- [ main] o.a.tutorial.events.EventsApplication : Starting EventsApplication v0.0.1-SNAPSHOT using Java 11.0.2 on MBP512-MBERGLJUNG-0917 with PID 2599 (/Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-spring-integration-events-sample/target/events-0.0.1-SNAPSHOT.jar started by mbergljung in /Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-spring-integration-events-sample)2021-03-29 13:44:49.446 INFO 2599 --- [ main] o.a.tutorial.events.EventsApplication : No active profile set, falling back to default profiles: default2021-03-29 13:44:50.606 INFO 2599 --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'errorChannel' has been explicitly defined. Therefore, a default PublishSubscribeChannel will be created.2021-03-29 13:44:50.613 INFO 2599 --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'taskScheduler' has been explicitly defined. Therefore, a default ThreadPoolTaskScheduler will be created.2021-03-29 13:44:50.621 INFO 2599 --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'integrationHeaderChannelRegistry' has been explicitly defined. Therefore, a default DefaultHeaderChannelRegistry will be created.2021-03-29 13:44:50.775 INFO 2599 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.integration.config.IntegrationManagementConfiguration' of type [org.springframework.integration.config.IntegrationManagementConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)2021-03-29 13:44:50.802 INFO 2599 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'integrationChannelResolver' of type [org.springframework.integration.support.channel.BeanFactoryChannelResolver] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)2021-03-29 13:44:50.803 INFO 2599 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'integrationDisposableAutoCreatedBeans' of type [org.springframework.integration.config.annotation.Disposables] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)2021-03-29 13:44:51.924 INFO 2599 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'2021-03-29 13:44:52.014 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the 'errorChannel' channel2021-03-29 13:44:52.015 INFO 2599 --- [ main] o.s.i.channel.PublishSubscribeChannel : Channel 'application.errorChannel' has 1 subscriber(s).2021-03-29 13:44:52.015 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean '_org.springframework.integration.errorLogger'2021-03-29 13:44:52.015 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {transformer} as a subscriber to the 'acsEventsListeningFlow.channel#0' channel2021-03-29 13:44:52.015 INFO 2599 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.acsEventsListeningFlow.channel#0' has 1 subscriber(s).2021-03-29 13:44:52.015 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsListeningFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#0'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsListeningFlow'2021-03-29 13:44:52.015 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {recipient-list-router} as a subscriber to the 'acsEventsListeningFlow.channel#1' channel2021-03-29 13:44:52.016 INFO 2599 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.acsEventsListeningFlow.channel#1' has 1 subscriber(s).2021-03-29 13:44:52.016 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsListeningFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#1'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsListeningFlow'2021-03-29 13:44:52.016 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {bridge} as a subscriber to the 'alfresco.events.si.channel' channel2021-03-29 13:44:52.016 INFO 2599 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.alfresco.events.si.channel' has 1 subscriber(s).2021-03-29 13:44:52.016 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsSpringIntegrationFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#0'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsSpringIntegrationFlow'2021-03-29 13:44:52.016 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {bridge} as a subscriber to the 'acsEventsSpringIntegrationFlow.channel#1' channel2021-03-29 13:44:52.016 INFO 2599 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.acsEventsSpringIntegrationFlow.channel#1' has 1 subscriber(s).2021-03-29 13:44:52.016 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsSpringIntegrationFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#1'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsSpringIntegrationFlow'2021-03-29 13:44:52.017 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {bridge} as a subscriber to the 'alfresco.events.handlers.channel' channel2021-03-29 13:44:52.017 INFO 2599 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.alfresco.events.handlers.channel' has 1 subscriber(s).2021-03-29 13:44:52.017 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsHandlersFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#0'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsHandlersFlow'2021-03-29 13:44:52.017 INFO 2599 --- [ main] o.s.integration.channel.DirectChannel : Channel 'application.acsEventsHandlersFlow.channel#1' has 1 subscriber(s).2021-03-29 13:44:52.018 INFO 2599 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean 'acsEventsHandlersFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#1'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsHandlersFlow'2021-03-29 13:44:52.018 INFO 2599 --- [ main] ishingJmsMessageListener$GatewayDelegate : started org.springframework.integration.jms.ChannelPublishingJmsMessageListener$GatewayDelegate@d8305c22021-03-29 13:44:52.539 INFO 2599 --- [ main] o.s.i.jms.JmsMessageDrivenEndpoint : started bean 'acsEventsListeningFlow.jms:message-driven-channel-adapter#0'; defined in: 'class path resource [org/alfresco/event/sdk/autoconfigure/AlfrescoEventsAutoConfiguration.class]'; from source: 'bean method acsEventsListeningFlow'2021-03-29 13:44:52.561 INFO 2599 --- [ main] o.a.tutorial.events.EventsApplication : Started EventsApplication in 3.849 seconds (JVM running for 4.638)Looks ready for some event handler code.
Now, start adding your event handler code, let’s add an event handler that will be triggered when a new document/file is uploaded. To do this we need to create a class that implements theorg.springframework.integration.dsl.IntegrationFlow interface, we can use a helper adapter class (i.e.IntegrationFlowAdapter) for this:
package org.alfresco.tutorial.events;import org.alfresco.event.sdk.handling.filter.EventTypeFilter;import org.alfresco.event.sdk.integration.EventChannels;import org.alfresco.event.sdk.integration.filter.IntegrationEventFilter;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.integration.dsl.IntegrationFlowAdapter;import org.springframework.integration.dsl.IntegrationFlowDefinition;import org.springframework.stereotype.Component;/** * Spring Integration based event handler that will execute code when a file is uploaded */@Componentpublic class NewContentFlow extends IntegrationFlowAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(NewContentFlow.class); // Use builder to create an integration flow based on alfresco.events.main.channel event channel @Override protected IntegrationFlowDefinition<?> buildFlow() { return from(EventChannels.MAIN) // Listen to events coming from the Alfresco events channel .filter(IntegrationEventFilter.of(EventTypeFilter.NODE_CREATED)) // Filter events and select only node created events .handle(t -> LOGGER.info("File uploaded: {}", t.getPayload().toString())); // Handle event with a bit of logging }}Add the Spring Bean class into the same directory as the Spring Boot starter class. It doesn’t have to be added to this directory, but in this case we are just testing it, so no need to organize too much.
Now stop, build and start it up again:
$ ^C...$ mvn spring-boot:run -Dlicense.skip=true...Add a file via the Share user interface, you should see the following in the logs:
2021-03-30 10:09:22.738 INFO 9603 --- [erContainer#0-1] o.a.tutorial.events.NewContentFlow : File uploaded: RepoEvent [specversion=1.0, type=org.alfresco.event.node.Created, id=12100b22-8dae-4ebe-b114-ba9dc2f9755b, source=/3bc24dba-d1ae-4c04-af60-0294a4c68a7f, time=2021-03-30T09:09:22.711117Z, dataschema=https://api.alfresco.com/schema/event/repo/v1/nodeCreated, datacontenttype=application/json, data=EventData [eventGroupId=3196f0f6-b4aa-4834-9aa2-a58eaa8f121f, resource=NodeResource [id=4e1f0830-2452-4a4c-b20a-7146402ce665, name=somefile-again.txt, nodeType=cm:content, isFile=true, isFolder=false, createdByUser=UserInfo [id=admin, displayName=Administrator], createdAt=2021-03-30T09:09:22.324Z, modifiedByUser=UserInfo [id=admin, displayName=Administrator], modifiedAt=2021-03-30T09:09:22.324Z, content=ContentInfo [mimeType=text/plain, sizeInBytes=0, encoding=UTF-8], properties={cm:title=, app:editInline=true, cm:description=}, aspectNames=[app:inlineeditable, cm:titled, cm:auditable], primaryHierarchy=[19e067a9-5d2a-43ba-ac93-d273d938050c, 30afd06d-6ec7-4434-b1d6-1f7671b6b9a7, 7a82ddff-0869-430e-8cc8-623d97b98dc4]], resourceBefore=null]]Now, this event handler will actually also be triggered when a folder is created. So how can we fix so the handler is only triggered when a file is created/uploaded? By adding a so calledevent filter to the class:
package org.alfresco.tutorial.events;import org.alfresco.event.sdk.handling.filter.EventTypeFilter;import org.alfresco.event.sdk.handling.filter.IsFileFilter;import org.alfresco.event.sdk.integration.EventChannels;import org.alfresco.event.sdk.integration.filter.IntegrationEventFilter;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.integration.dsl.IntegrationFlowAdapter;import org.springframework.integration.dsl.IntegrationFlowDefinition;import org.springframework.stereotype.Component;/** * Spring Integration based event handler that will execute code when a file is uploaded */@Componentpublic class NewContentFlow extends IntegrationFlowAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(NewContentFlow.class); // Use builder to create an integration flow based on alfresco.events.main.channel event channel @Override protected IntegrationFlowDefinition<?> buildFlow() { return from(EventChannels.MAIN) // Listen to events coming from the Alfresco events channel .filter(IntegrationEventFilter.of(EventTypeFilter.NODE_CREATED)) // Filter events and select only node created events .filter(IntegrationEventFilter.of(IsFileFilter.get())) // Filter node and make sure it is a file node .handle(t -> LOGGER.info("File uploaded: {}", t.getPayload().toString())); // Handle event with a bit of logging }}Here we are using theorg.alfresco.event.sdk.handling.filter.IsFileFilter, which will make sure that the event handleris triggered only when the node type iscm:content or subtype thereof, which represents files. To use this filter withSpring Integration we use theIntegrationEventFilter wrapper.
If you are thinking, do I really need a whole class just to process an event? No you don’t, you can include a bean definition directly in the Spring Boot app class as follows:
package org.alfresco.tutorial.events;import org.alfresco.event.sdk.handling.filter.EventTypeFilter;import org.alfresco.event.sdk.handling.filter.IsFileFilter;import org.alfresco.event.sdk.integration.EventChannels;import org.alfresco.event.sdk.integration.filter.IntegrationEventFilter;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.Bean;import org.springframework.integration.dsl.IntegrationFlow;import org.springframework.integration.dsl.IntegrationFlows;@SpringBootApplicationpublic class EventsApplication { private static final Logger LOGGER = LoggerFactory.getLogger(EventsApplication.class); public static void main(String[] args) { SpringApplication.run(EventsApplication.class, args); } @Bean public IntegrationFlow logCreateFileNode() { return IntegrationFlows.from(EventChannels.MAIN) // Listen to events coming from the Alfresco events channel .filter(IntegrationEventFilter.of(EventTypeFilter.NODE_CREATED)) // Filter events and select only node created events .filter(IntegrationEventFilter.of(IsFileFilter.get())) // Filter node and make sure it is a file node .handle(t -> LOGGER.info("File uploaded: {}", t.getPayload().toString())) // Handle event with a bit of logging .get(); }}It also makes sense to add error management code to the application class as follows:
@SpringBootApplicationpublic class EventsApplication { private static final Logger LOGGER = LoggerFactory.getLogger(EventsApplication.class); public static void main(String[] args) { SpringApplication.run(EventsApplication.class, args); } @Bean public IntegrationFlow logError() { return IntegrationFlows.from(EventChannels.ERROR).handle(t -> { LOGGER.info("Error: {}", t.getPayload().toString()); MessageHandlingException exception = (MessageHandlingException) t.getPayload(); exception.printStackTrace(); }).get(); }...}For a complete list of events with sample code see theevents extension point documentation. For a complete list of Event Filters available in the SDK see thissection.
For information on how to implement a custom event filter see thissection.
For more information about how to extract all the properties from the message payload seeNodeResource info.
Make sure you have completed theprerequisites and created astarter project.You also need to decide if you want to use pureJava event handlers orSpring Integration event handlers.
The following event filter checks if a passed in node ID is equal to a desired parent folder node ID. This event filtercan be used to check if a file or folder is located in a specific folder. To create a custom event filter you need to create a class that extends theorg.alfresco.event.sdk.handling.filter.AbstractEventFilter class and implementthetest method:
package org.alfresco.tutorial.events;import org.alfresco.event.sdk.handling.filter.AbstractEventFilter;import org.alfresco.event.sdk.model.v1.model.DataAttributes;import org.alfresco.event.sdk.model.v1.model.NodeResource;import org.alfresco.event.sdk.model.v1.model.RepoEvent;import org.alfresco.event.sdk.model.v1.model.Resource;import java.util.Objects;/** * Filter that can be used when a node needs to be in a specific folder. */public class ParentFolderFilter extends AbstractEventFilter { // The node ID for the folder we want to check against private final String parentId; // Private ctor, make sure ID is not null private ParentFolderFilter(final String parentId) { this.parentId = Objects.requireNonNull(parentId); } // When using the filter, pass in the folder node ID we want to check against public static ParentFolderFilter of(final String parentId) { return new ParentFolderFilter(parentId); } // The actual test: // get the node resource we are testing (such as a file node), // then get its primary parent folder ID and check if it matches desired folder Node ID public boolean test(RepoEvent<DataAttributes<Resource>> event) { NodeResource resource = (NodeResource) event.getData().getResource(); boolean parentFound = resource.getPrimaryHierarchy().get(0).equals(parentId); return isNodeEvent(event) && parentFound; }}This event filter can now be used in an event handler class as follows (in this case together with another filter):
@Componentpublic class ContentUploadedEventHandler implements OnNodeCreatedEventHandler { private String folderID = "5f355d16-f824-4173-bf4b-b1ec37ef5549"; ... public EventFilter getEventFilter() { // Check if uploaded file is located in desired folder return IsFileFilter.get() .and(ParentFolderFilter.of(folderID)); }The ReST API Java wrapper library in SDK 5 provides a Java object wrapper around the Alfresco ReST API. If you are not familiar with Alfresco ReST API version 1.0, then read through thisintroduction.
The ReST API wrapper classes have been generated based on the ReST APISwagger definition. The following main packages exist for the different APIs:
The following API requires theAlfresco Insight Engine to be installed:
The following APIs require theAlfresco Governance Services module to be installed:
Thealfresco-core-rest-api package contains most of the APIs that you will need. Here are some of the APIs in this package:
NodesApi – Manage nodes, such as folders and filesSitesApi – Manage sitesQueriesApi – Simple search for people, groups, nodes by termVersionsApi – Manage version historyActionsApi – Manage repository actionsAuditApi – Manage audit apps and loggingCommentsApi – Manage node commentsDownloadsApi – Download node contentThis package together with thealfresco-search-rest-api package, which contains theSearchAPI, is all you will need in most cases.
Make sure you have completedprerequisites and created astarter project.
Add the following dependency in the Maven project file (i.e.pom.xml):
<dependencies> <!-- Alfresco Java SDK 5 Java ReST API wrapper Spring Boot Starter --> <dependency> <groupId>org.alfresco</groupId> <artifactId>alfresco-java-rest-api-spring-boot-starter</artifactId> <version>5.0.0</version> </dependency> </dependencies>Remove the default Spring Boot starter dependency (i.e.<artifactId>spring-boot-starter</artifactId>).
Modify the contents of the Spring Boot application class (org/alfresco/tutorial/sdk5demo/Sdk5DemoApplication.java) by adding the followingcom.fasterxml.jackson.databind.ObjectMapper. This is required for deserializing dates:
package org.alfresco.tutorial.sdk5demo; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import javax.annotation.PostConstruct; @SpringBootApplication public class Sdk5DemoApplication { @Autowired private ObjectMapper objectMapper; @PostConstruct public void setUp() { objectMapper.registerModule(new JavaTimeModule()); } public static void main(String[] args) { SpringApplication.run(Sdk5DemoApplication.class, args); } }Test it:
$ mvn clean package -Dlicense.skip=true [INFO] Scanning for projects... ... $ java -jar target/rest-api-0.0.1-SNAPSHOT.jar . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.4.2) 2021-04-07 14:31:35.599 INFO 53273 --- [ main] o.a.tutorial.restapi.RestApiApplication : Starting RestApiApplication v0.0.1-SNAPSHOT using Java 11.0.2 on MBP512-MBERGLJUNG-0917 with PID 53273 (/Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-rest-api-java-wrapper-sample/target/rest-api-0.0.1-SNAPSHOT.jar started by mbergljung in /Users/mbergljung/IDEAProjects/docs-new/sdk5/sdk5-rest-api-java-wrapper-sample) 2021-04-07 14:31:35.605 INFO 53273 --- [ main] o.a.tutorial.restapi.RestApiApplication : No active profile set, falling back to default profiles: default 2021-04-07 14:31:36.832 INFO 53273 --- [ main] o.s.cloud.context.scope.GenericScope : BeanFactory id=55661aff-d1dc-3db8-94e2-cf0514d3118c 2021-04-07 14:31:37.443 INFO 53273 --- [ main] o.a.tutorial.restapi.RestApiApplication : Started RestApiApplication in 2.832 seconds (JVM running for 3.563)Looks ready for some ReST API code.
Now, start adding your ReST API code, let’s create a command line client that can be used to create sites, create folders, create files, and to search. First update the Spring Boot application class to look like follows, making use of theorg.springframework.boot.CommandLineRunner:
package org.alfresco.tutorial.restapi;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.CommandLineRunner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class RestApiApplication implements CommandLineRunner { private static final Logger LOGGER = LoggerFactory.getLogger(RestApiApplication.class); @Autowired CreateSiteCmd createSiteCmd; @Autowired CreateFolderCmd createFolderCmd; @Autowired CreateFileCmd createFileCmd; @Autowired SearchCmd searchCmd; public static void main(String[] args) { SpringApplication.run(RestApiApplication.class, args); } public void run(String... args) throws Exception { for (int i = 0; i < args.length; ++i) { LOGGER.info("args[{}]: {}", i, args[i]); } String command = args[0]; switch (command) { case "create-site": createSiteCmd.execute(args[1]); break; case "create-folder": // siteId, folderName createFolderCmd.execute(args[1], args[2]); break; case "create-file": // parentFolderNodeId, filename createFileCmd.execute(args[1], args[2]); break; case "search": // siteId, term searchCmd.execute(args[1], args[2]); break; default: LOGGER.error("Command {} is not available", command); } }}This command line runner uses a number of beans to support creating different things in the Alfresco Repository, such as sites and folders. Start by creating theCreateSiteCmd bean that will facilitate creating sites via the ReST API Java wrapper, in the same package as the Spring Boot application class create the following class:
package org.alfresco.tutorial.restapi;import org.alfresco.core.handler.SitesApi;import org.alfresco.core.model.Site;import org.alfresco.core.model.SiteBodyCreate;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.io.IOException;import java.util.Objects;@Componentpublic class CreateSiteCmd { static final Logger LOGGER = LoggerFactory.getLogger(CreateSiteCmd.class); @Autowired SitesApi sitesApi; public void execute(String siteId) throws IOException { Site site = Objects.requireNonNull(sitesApi.createSite( new SiteBodyCreate() .id(siteId) .title("title-" + siteId) .description("description-" + siteId) .visibility(SiteBodyCreate.VisibilityEnum.PUBLIC), null, null, null).getBody()).getEntry(); LOGGER.info("Created site: {}", site); }}To use one of the ReST API Java wrapper services, such asSitesApi, auto wire it into the component as in the above class. Creating stuff in the repository usually mean making a HTTP POST in the background. In these cases there is always a body class that we can use to fill in POST data, such asSiteBody in this case. A successful API call will return a populated result object calledSite.
In a similar way we add the other three command beans in the same directory as follows, starting with theCreateFolderCmd:
package org.alfresco.tutorial.restapi;import org.alfresco.core.handler.NodesApi;import org.alfresco.core.handler.SitesApi;import org.alfresco.core.model.Node;import org.alfresco.core.model.NodeBodyCreate;import org.alfresco.core.model.SiteContainer;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.io.IOException;import java.util.Objects;@Componentpublic class CreateFolderCmd { static final Logger LOGGER = LoggerFactory.getLogger(CreateFolderCmd.class); @Autowired SitesApi sitesApi; @Autowired NodesApi nodesApi; public void execute(String siteId, String folderName) throws IOException { SiteContainer docLibContainer = Objects.requireNonNull(sitesApi.getSiteContainer(siteId, "documentLibrary", null).getBody()).getEntry(); LOGGER.info("Creating folder in site DocumentLibrary folder Node ID: {}", docLibContainer.getId()); Node folderNode = Objects.requireNonNull(nodesApi.createNode(docLibContainer.getId(), new NodeBodyCreate() .nodeType("cm:folder") .name(folderName), null, null, null, null, null).getBody()).getEntry(); LOGGER.info("Created folder: {}", folderNode.toString()); }}TheNodesApi is one of the main APIs that we will use a lot to manipulate folders and files. We use it here to create a folder node in the site’s document library.
Next we create theCreateFileCmd as follows in the same directory:
package org.alfresco.tutorial.restapi;import org.alfresco.core.handler.NodesApi;import org.alfresco.core.model.Node;import org.alfresco.core.model.NodeBodyCreate;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.io.IOException;import java.util.Objects;@Componentpublic class CreateFileCmd { static final Logger LOGGER = LoggerFactory.getLogger(CreateFileCmd.class); @Autowired NodesApi nodesApi; public void execute(String parentFolderId, String fileName) throws IOException { // Get the parent folder where file should be stored Node parentFolderNode = Objects.requireNonNull(nodesApi.getNode(parentFolderId, null, null, null).getBody()).getEntry(); LOGGER.info("Got parent folder node: {}", parentFolderNode.toString()); // Create the file node metadata Node fileNode = Objects.requireNonNull(nodesApi.createNode(parentFolderNode.getId(), new NodeBodyCreate().nodeType("cm:content").name(fileName), null, null, null, null, null).getBody()).getEntry(); // Add the file node content Node updatedFileNode = Objects.requireNonNull(nodesApi.updateNodeContent(fileNode.getId(), "Some text for this file...".getBytes(), true, null, null, null, null).getBody()).getEntry(); LOGGER.info("Created file with content: {}", updatedFileNode.toString()); }}You might notice that it requires two calls to create a file with content. The ReST API does provide a way to do this with one call as can be seenhere.However, the generated Java wrapping classes does not yet provide functionality for this (it is scheduled to be supported in a future version of SDK 5).
Add also the finalSearchCmd class as follows:
package org.alfresco.tutorial.restapi;import org.alfresco.search.handler.SearchApi;import org.alfresco.search.model.RequestQuery;import org.alfresco.search.model.ResultSetPaging;import org.alfresco.search.model.SearchRequest;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Component;import java.io.IOException;@Componentpublic class SearchCmd { static final Logger LOGGER = LoggerFactory.getLogger(SearchCmd.class); @Autowired SearchApi searchApi; public void execute(String siteId, String term) throws IOException { ResponseEntity<ResultSetPaging> result = searchApi.search(new SearchRequest() .query(new RequestQuery() .language(RequestQuery.LanguageEnum.AFTS) .query("(SITE:\"" + siteId + "\" AND TEXT:\"" + term + "\" )"))); LOGGER.info("Search result: {}", result.getBody().getList().getEntries()); }}Now, stop and build it again:
$ ^C...$ mvn clean package -Dlicense.skip=true...Create an Alfresco Share site with idtest as follows:
$ java -jar target/rest-api-0.0.1-SNAPSHOT.jar create-site test...2021-04-08 13:16:49.239 INFO 62074 --- [ main] o.a.tutorial.restapi.RestApiApplication : args[0]: create-site2021-04-08 13:16:49.241 INFO 62074 --- [ main] o.a.tutorial.restapi.RestApiApplication : args[1]: test2021-04-08 13:16:52.989 INFO 62074 --- [ main] o.a.tutorial.restapi.CreateSiteCmd : Created site: class Site { id: test guid: 59dc57a1-ad07-4715-8844-005cc7fc59d7 title: title-test description: description-test visibility: PUBLIC preset: site-dashboard role: SiteManager}Then create a folder calledfolder1 in the site with idtest:
$ java -jar target/rest-api-0.0.1-SNAPSHOT.jar create-folder test folder1...2021-04-08 13:19:23.264 INFO 62106 --- [ main] o.a.tutorial.restapi.RestApiApplication : args[0]: create-folder2021-04-08 13:19:23.266 INFO 62106 --- [ main] o.a.tutorial.restapi.RestApiApplication : args[1]: test2021-04-08 13:19:23.267 INFO 62106 --- [ main] o.a.tutorial.restapi.RestApiApplication : args[2]: folder12021-04-08 13:19:23.560 INFO 62106 --- [ main] o.a.tutorial.restapi.CreateFolderCmd : Creating folder in site DocumentLibrary folder Node ID: aa02f5eb-f45d-4ab4-bf21-9eeb8c243d512021-04-08 13:19:24.166 INFO 62106 --- [ main] o.a.tutorial.restapi.CreateFolderCmd : Created folder: class Node { id: 3e16d079-2fdc-4d64-ad76-c65c233165f4 name: folder1 nodeType: cm:folder isFolder: true isFile: false isLocked: false modifiedAt: 2021-04-08T12:19:23.876Z modifiedByUser: class UserInfo { displayName: Administrator id: admin } createdAt: 2021-04-08T12:19:23.876Z createdByUser: class UserInfo { displayName: Administrator id: admin } parentId: aa02f5eb-f45d-4ab4-bf21-9eeb8c243d51 isLink: null isFavorite: null content: null aspectNames: [cm:auditable] properties: null allowableOperations: null path: null permissions: null definition: null}Create a file calledsomefile.txt in the folder calledfolder1 (3e16d079-2fdc-4d64-ad76-c65c233165f4):
$ java -jar target/rest-api-0.0.1-SNAPSHOT.jar create-file 3e16d079-2fdc-4d64-ad76-c65c233165f4 somefile.txt...2021-04-08 13:21:55.972 INFO 62152 --- [ main] o.a.tutorial.restapi.RestApiApplication : args[0]: create-file2021-04-08 13:21:55.973 INFO 62152 --- [ main] o.a.tutorial.restapi.RestApiApplication : args[1]: 3e16d079-2fdc-4d64-ad76-c65c233165f42021-04-08 13:21:55.973 INFO 62152 --- [ main] o.a.tutorial.restapi.RestApiApplication : args[2]: somefile.txt2021-04-08 13:21:56.211 INFO 62152 --- [ main] o.a.tutorial.restapi.CreateFileCmd : Got parent folder node: class Node { id: 3e16d079-2fdc-4d64-ad76-c65c233165f4 name: folder1 nodeType: cm:folder isFolder: true isFile: false isLocked: false modifiedAt: 2021-04-08T12:19:23.876Z modifiedByUser: class UserInfo { displayName: Administrator id: admin } createdAt: 2021-04-08T12:19:23.876Z createdByUser: class UserInfo { displayName: Administrator id: admin } parentId: aa02f5eb-f45d-4ab4-bf21-9eeb8c243d51 isLink: null isFavorite: null content: null aspectNames: [cm:auditable] properties: null allowableOperations: null path: null permissions: null definition: null}2021-04-08 13:21:56.896 INFO 62152 --- [ main] o.a.tutorial.restapi.CreateFileCmd : Created file with content: class Node { id: 1187b449-258e-4843-997f-991b7995b665 name: somefile.txt nodeType: cm:content isFolder: false isFile: true isLocked: false modifiedAt: 2021-04-08T12:21:56.697Z modifiedByUser: class UserInfo { displayName: Administrator id: admin } createdAt: 2021-04-08T12:21:56.265Z createdByUser: class UserInfo { displayName: Administrator id: admin } parentId: 3e16d079-2fdc-4d64-ad76-c65c233165f4 isLink: null isFavorite: null content: class ContentInfo { mimeType: text/plain mimeTypeName: Plain Text sizeInBytes: 26 encoding: ISO-8859-1 } aspectNames: [cm:versionable, cm:auditable] properties: {cm:versionLabel=1.0, cm:versionType=MAJOR} allowableOperations: null path: null permissions: null definition: null}Finally, search for content matching textfile in site with idtest:
$ java -jar target/rest-api-0.0.1-SNAPSHOT.jar search test file...2021-04-08 14:40:51.379 INFO 63261 --- [ main] o.a.tutorial.restapi.RestApiApplication : args[0]: search2021-04-08 14:40:51.381 INFO 63261 --- [ main] o.a.tutorial.restapi.RestApiApplication : args[1]: test2021-04-08 14:40:51.381 INFO 63261 --- [ main] o.a.tutorial.restapi.RestApiApplication : args[2]: file2021-04-08 14:40:52.493 INFO 63261 --- [ main] org.alfresco.tutorial.restapi.SearchCmd : Search result: [class ResultSetRowEntry { entry: class ResultNode { id: 1187b449-258e-4843-997f-991b7995b665 name: somefile.txt nodeType: cm:content isFolder: false isFile: true isLocked: false modifiedAt: 2021-04-08T12:21:59.077Z modifiedByUser: class UserInfo { displayName: Administrator id: admin } createdAt: 2021-04-08T12:21:56.265Z createdByUser: class UserInfo { displayName: Administrator id: admin } parentId: 3e16d079-2fdc-4d64-ad76-c65c233165f4 isLink: null content: class ContentInfo { mimeType: text/plain mimeTypeName: Plain Text sizeInBytes: 26 encoding: ISO-8859-1 mimeTypeGroup: null } aspectNames: null properties: null allowableOperations: null path: null search: class SearchEntry { score: 1.0 highlight: null } archivedByUser: null archivedAt: null versionLabel: null versionComment: null }}]This sample has shown us that it’s is easy to interact with the Alfresco Repository from a Java client with the help of SDK 5 Java ReST API services.
For more information see theReST API Java wrapper extension point documentation.
Make sure you have completedprerequisites and then create astarter project with configuration properties set for both event handling and ReST API.
Theapplication.properties file should look something like this:
# Where is Alfresco Active MQ JMS Broker running?spring.activemq.brokerUrl=tcp://localhost:61616# This property is required if you want Spring Boot to auto-define the ActiveMQConnectionFactory,# otherwise you can define that bean in Spring configspring.jms.cache.enabled=false# HTTP Basic Authentication that will be used by the APIcontent.service.security.basicAuth.username=admincontent.service.security.basicAuth.password=admin# Location of the server and API endpointscontent.service.url=http://localhost:8080content.service.path=/alfresco/api/-default-/public/alfresco/versions/1search.service.path=/alfresco/api/-default-/public/search/versions/1Note: The configuration will look slightly different if you want to use the Spring Integration for event handling.
Add the following dependencies in the Maven project file (i.e.pom.xml):
<dependencies> <!-- Alfresco Java SDK 5 Java Event Handler API Spring Boot Starter --> <dependency> <groupId>org.alfresco</groupId> <artifactId>alfresco-java-event-api-spring-boot-starter</artifactId> <version>5.0.0</version> </dependency> <!-- Alfresco Java SDK 5 Java ReST API wrapper Spring Boot Starter --> <dependency> <groupId>org.alfresco</groupId> <artifactId>alfresco-java-rest-api-spring-boot-starter</artifactId> <version>5.0.0</version> </dependency> </dependencies>Remove the default Spring Boot starter dependency (i.e.<artifactId>spring-boot-starter</artifactId>).
Modify the contents of the Spring Boot application class (org/alfresco/tutorial/sdk5demo/Sdk5DemoApplication.java) by adding the followingcom.fasterxml.jackson.databind.ObjectMapper. This is required for deserializing dates:
package org.alfresco.tutorial.sdk5demo; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import javax.annotation.PostConstruct; @SpringBootApplication public class Sdk5DemoApplication { @Autowired private ObjectMapper objectMapper; @PostConstruct public void setUp() { objectMapper.registerModule(new JavaTimeModule()); } public static void main(String[] args) { SpringApplication.run(Sdk5DemoApplication.class, args); } }Test it:
$ mvn spring-boot:run -Dlicense.skip=true ... 2021-04-08 15:21:17.392 INFO 63958 --- [ main] o.a.t.sdk5demo.Sdk5DemoApplication : Started Sdk5DemoApplication in 2.531 seconds (JVM running for 3.0)We can now add event handling and ReST API code. Here is an example of an event handler that is triggered when a file is uploaded. It then calls back to the repository via the ReST API to get the file content:
package org.alfresco.tutorial.sdk5demo;import org.alfresco.core.handler.NodesApi;import org.alfresco.event.sdk.handling.filter.*;import org.alfresco.event.sdk.handling.handler.OnNodeCreatedEventHandler;import org.alfresco.event.sdk.model.v1.model.DataAttributes;import org.alfresco.event.sdk.model.v1.model.NodeResource;import org.alfresco.event.sdk.model.v1.model.RepoEvent;import org.alfresco.event.sdk.model.v1.model.Resource;import org.apache.commons.io.IOUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.io.InputStream;import java.nio.charset.StandardCharsets;/** * Sample event handler to demonstrate reacting to a document/file being uploaded to the repository. * It then uses the Java ReST API to call back for the file content from the repository. */@Componentpublic class ContentUploadedEventHandler implements OnNodeCreatedEventHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ContentUploadedEventHandler.class); @Autowired NodesApi nodesApi; public void handleEvent(final RepoEvent<DataAttributes<Resource>> repoEvent) { NodeResource nodeResource = (NodeResource) repoEvent.getData().getResource(); LOGGER.info("A file was uploaded to the repository: {}, {}, {}", nodeResource.getId(), nodeResource.getNodeType(), nodeResource.getName()); try { InputStream fileInputStream = nodesApi.getNodeContent( nodeResource.getId(), true, null, null) .getBody() .getInputStream(); String result = IOUtils.toString(fileInputStream, StandardCharsets.UTF_8.toString()); LOGGER.info("A file '{}' was uploaded with the following content: {}", nodeResource.getName(), result); } catch (Exception ex) { LOGGER.error("An error occurred trying to download the content of the file", ex); } } public EventFilter getEventFilter() { return IsFileFilter.get(); }}Running this and uploading a text file to the Repository gives logging as follows:
$ mvn spring-boot:run -Dlicense.skip=true...2021-04-08 15:36:46.441 INFO 64121 --- [erContainer#0-1] o.a.t.s.ContentUploadedEventHandler : A file was uploaded to the repository: 9e99d999-ef8f-4f6f-9582-ac5f52c2bf8d, cm:content, some.txt2021-04-08 15:36:47.134 INFO 64121 --- [erContainer#0-1] o.a.t.s.ContentUploadedEventHandler : A file 'some.txt' was uploaded with the following content: This is a file with some textDebugging an extension project is most likely going to be something you will have to do to see what’s going on. This iseasy with a Spring Boot App. Configure for example the Spring Boot Maven plugin as follows:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <jvmArguments> -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 </jvmArguments> </configuration> </plugin> </plugins></build>Then you can attach remotely and debug from, for example, IntelliJ IDEA:

You can also configure debug on the command line (no maven plugin config needed):
mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005" -Dlicense.skip=true