Triggering a Workflow using Event Listeners in AEM

Triggering a Workflow using Event Listeners in AEM

AEM Version: 6.2
Target Audience: AEM Developers

Introduction

In AEM 6.2 Workflows, we can trigger a workflow when a DAM Asset is created, modified, or deleted within a given path. In this article, we will explore triggering workflows from our code based on events in the JCR.

Suppose you have a workflow that creates custom renditions of assets in addition to the default AEM renditions, when the asset is under “/content/dam/ProjectName/images/”. You would have set up two launchers for triggering this workflow: one with event type as “Node Create” and one with “Node Modified”. We can also achieve the same functionality through our code, without touching the GUI.

Requirement

When assets are moved into a certain folder structure in DAM, trigger a workflow that creates a 100px X 100px thumbnail of our image.

No-rendition-outside-imgs

Fig 1: Before Moving the asset, no custom thumbnail. Fig 2: Desired result after moving the asset, the new thumbnail.

Analysis

The intuitive thought is that when an asset is moved, a new node is created in the new location and the old one is deleted. However, experience shows that AEM does not create a new node in the destination folder on Node Move. We know this because the ‘jcr:Created’ property does not change. AEM does not even change the last modified date.

Creation Timestamp Before Moving the Asset Creation Timestamp is the same after moving

Fig 3: Creation Timestamp Before Moving the Asset. Fig 4: Creation Timestamp is the same after moving

Modification Timestamp Before Moving the Asset. Modification Timestamp is the same after moving

Fig 5: Modification Timestamp Before Moving the Asset. Fig 6: Modification Timestamp is the same after moving.

What if we copy the asset?

On copying the asset, a new version of the same is created. This triggers the Node Creation launcher.

No versions before copying the asset Version created after copy-pasting the asset

Fig. 7: No versions before copying the asset. Fig. 8: Version created after copy-pasting the asset.

Approach

Event Listeners

AEM supports observation, which enables us to receive notifications of persistent changes to the workspace. A persisted change to the workspace is represented by a set of one or more events. Each event reports a single simple change to the structure of the persistent workspace in terms of an item added, changed, moved or removed. There are thus 7 possible events at the JCR level, viz:

  1. Node Added
  2. Node Moved
  3. Node Modified
  4. Node Removed
  5. Property Added
  6. Property Removed
  7. Property Changed

We connect with the observation mechanism by registering an event listener with the workspace. An event listener is a class implementing the EventListener interface, that responds to the stream of events to which it has been subscribed. An event listener is added to a workspace with:

void ObservationManager. 
addEventListener(EventListener listener, 
int eventTypes, 
String absPath,
boolean isDeep, 
String[] uuid, 
String[] nodeTypeName, 
boolean noLocal)

(A detailed explanation of each parameter is given with the code example in the package as well as the at the end of this article) As defined by the EventListener interface, listener must provide an implementation of the onEvent method:

void EventListener.onEvent(EventIterator events)

When an event occurs that falls within the scope of the listener, the repository calls the onEvent method invoking our logic which processes/responds to the event. In our case, we will register an event listener to listen for “Node Moved” events under “/content/dam/images” so that when an asset is moved to that folder, our workflow can be triggered.

Implementation

When the component is activated, the activate(…) method is called. It contains a call to ObservationManager.addEventListener(…) for registering the event listener. The deactivate(…) method contains logic for deregistering the event listener, and is triggered when the bundle is being stopped.

When the relevant event occurs, the onEvent(…) method is called, which contains logic for processing the event. In our case, we trigger a workflow.

The following is the relevant code from ThumbnailNodeMovedListener.java:

 
protected void activate(ComponentContext ctx) { 

try { 
. 
. 
. 
// Building the parameters for adding the event listener 


// Whether the subfolders of the given path should also be watched 

boolean isDeep = true; 

// Only events whose associated node has one of the UUIDs in this list will be 

// received. If this parameter is null then no UUID-related restriction is 

// placed on events received. 

String[] uuid = null; 

// Only events whose associated node has one of the node types (or a subtype of 

// one of the node types) in this list will be received. If this parameter is 

// null then no node type-related restriction is placed on events received. 

String[] nodeTypeName = null; 
 

// If noLocal is true, then events generated by the session through which the 

// listener was registered are ignored. Otherwise, they are not ignored. 

boolean noLocal = true; 

// Registering the event listener 

observationManager.addEventListener(this, Event.NODE_MOVED, ASSET_UPDATE_PATH, isDeep, uuid, nodeTypeName, 

noLocal); 

} 

} 

 

public void onEvent(EventIterator itr) { 

while (itr.hasNext()) { 

Event currentEvent = itr.nextEvent(); 

try { 
. 
. 
.  

// Create a workflow session 

WorkflowSession wfSession = workflowService.getWorkflowSession(localSession); 

// Get the workflow model 

WorkflowModel wfModel = wfSession.getModel(THUMBNAIL_WORKFLOW_PATH); 


// Get the Workflow data. The first parameter in the newWorkflowData method is 

// the payloadType. Just a fancy name to let it know what type of workflow it is 

// working with. 

WorkflowData wfData = wfSession.newWorkflowData(JCR_PATH, currentEvent.getPath()+ORIGINAL_RENDITION_RELATIVE_PATH); 

// Start the Workflow. 

wfSession.startWorkflow(wfModel, wfData); 

} 
. 
. 
.

Download this code (including the workflow):

Build it using

mvn clean install -PautoInstallPackage

N.B: Creating a workflow is not part of this tutorial, and therefore a ready workflow has been provided in the code package. However, if you want to learn to create workflows, here is an excellent resource: ->

https://www.argildx.com/workflow-in-aem/

References

Adobe Consulting Services. (2018, March 20). acs-aem-samples/SampleJcrEventListener.java at master · Adobe-Consulting-Services/acs-aem-samples. Retrieved from Github: https://github.com/Adobe-Consulting-Services/acs-aem-samples/blob/master/bundle/src/main/java/com/adobe/acs/samples/events/impl/SampleJcrEventListener.java

Day Software AG. (2018, March 20). JCR 2.0: 12 Observation (Content Repository for Java Technology API v2.0). Retrieved from Adobe Docs: https://docs.adobe.com/docs/en/spec/jcr/2.0/12_Observation.html

How to Change Data Type (Typecast) in AEM, Use @TypeHint

How to Change Data Type (Typecast) in AEM, Use @TypeHint
Problem Statement:

How to convert a variable from one data type to another data type in AEM.

For a use case where a number field is used in dialog and further data will be utilized in Sightly (HTL) for numeric comparison operations. Problem can come up as data will be stored in String format and comparison can only be made on same data type elements.

Solution:

@TypeHint: It is used to forcefully define the data type of a property. It can be used as following:

Scenario:

I have a number field in my dialog with name ‘sponsoredPosition’, by default it’s value is stored in String format and I wanted it to be stored in Long format instead of String.

Steps:

In the component add a node, parallel to the ‘sponsoredPosition’ node (The one of which data type needs to be changed) of the type nt:unstructured.

  1. In the new node add following properties:
  2. ignoreData{Boolean} = true
  3. value{String} = Long
  4. Name{String} [email protected]
  5. sling:resourceType{String} =   granite/ui/components/foundation/form/hidden.

Here,

(a) ignoreData, as the name suggests tells value of this field should not be stored.

(b) In value field you must define the data type in which you want your data to be stored.

(c) In Name field add ‘@TypeHint’ suffix to the property name of original node whose value was stored in string(by default) .

(d) Resource type hidden is used for hiding it in dialog.

<sponsoredPosition
  jcr:primaryType="nt:unstructured"
  sling:resourceType="granite/ui/components/foundation/form/numberfield"
  fieldLabel="Sponsored Content Position"
  max="{Long}3"
  min="{Long}2"
  value="3"
  name="./sponsoredPosition"/>
  <sponsoredPositionTypeHint
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/foundation/form/hidden"
    ignoreData="{Boolean}true"
    name="./[email protected]"
    value="Long"/>

 

BLOG SUMMARY & BUSINESS GOAL

This blog is intended to provide technical AEM users a solution to an asked question and tactical training on the topic: How to Typecast in AEM.

Conclusion

Adding @TypeHint solved the issue, now value is being stored in Long format instead of String.

Interested in more training and support for your organisation using AEM CMS? Request a consultation to discuss Argil DX managed services.

Sling Models with Sightly Part – IV (Key Annotations – II)

In this post, I will explain some important questions related to OSGi Services and Sling Models. These questions had been asked by some of my blog readers on the basis of my last blog.

So the Problem Statement is-

According to them, they have one interface with multiple service implementations and want to choose any one of these service implementations according to their need. So their questions were-

1. How to choose the desired service implementation from these service implementations in another services or servlets?
2. How to choose the desired service implementation from these service implementations in Sling Model Class?

I have created a demo implementation according to their requirement and for doing that I have created an interface named as TestService with a dummy method named as test(). Here is the code-

package services;
public interface TestService {
    void test();
}

Now, I have created two dummy service implementations of this interface. Here is the code for the first implementation named as TestServiceFirstImpl.

@Service
@Component(enabled = true,immediate = true,metatype = true)
public class TestServiceFirstImpl implements TestService {
    public void test() {
        System.out.println("inside first");
    }
}

Here is the code for my second implementation named as TestServiceSecondImpl.

@Service
@Component(enabled = true,immediate = true,metatype = true)
public class TestServiceSecondImpl  implements TestService {
    public void test() {
        System.out.println("inside second");
    }
}

Answer for the first question-
It is a two-step process. These steps are explained below-

Step 1
Add a new property that have a unique value for each and every service implementation. This property could be anything you want to add. For example, I am using service.label property for these services and on the basis of this property I will choose from these implementations.

Here are my new definitions for TestServiceFirstImpl class.

@Service
@Component(enabled = true,immediate = true,metatype = true)
@Property(name = "service.label", value = "first")
public class TestServiceFirstImpl implements TestService {
    public void test() {
        System.out.println("inside first");
    }
}

Code For TestServiceSecondImpl class.

@Service
@Component(enabled = true,immediate = true,metatype = true)
@Property(name = "service.label", value = "second")
public class TestServiceSecondImpl  implements TestService {

    public void test() {
        System.out.println("inside second");
    }
}

Step:- 2

Use @Reference annotation in another servlet or in OSGi Service with an extra attribute named as “target”. Just redefine this line as shown below-

@SlingServlet(paths="/bin/testService",extensions = "html",generateComponent = false)
@Component(enabled = true,immediate = true)
public class TestServlet extends SlingSafeMethodsServlet {

    @Reference(target = "(service.label=second)")
    TestService testService;

    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)throws ServletException, IOException{
        testService.test();
        response.getWriter().print(" CHECK YOUR LOGS ");
    }
}

Try to run your code, you will get the desired output.

Q2. How to choose these services in Sling Model class?

In Sling Model class you can call an external service using two annotations. These are-
1. @OSGiService
2. @Inject with @Source annotation

If you are using @OSGiService annotation then you have an attribute “filter”, here add your condition and you will get the desired implementation as shown below-

@OSGiService(filter = "(service.label=first)")
TestService testService;

If you are using @Inject with @Source annotation then you need to add one more annotation @Filter. Here is the new code-

    @Inject @Source("osgi-services")
    @Filter("(service.label=second)")
    TestService testService1;

Now here my complete working code that will show you the use of both of these two approaches.

@Model(adaptables = Resource.class)
public class ServiceResolver {


    @OSGiService(filter = "(service.label=first)")
    TestService testService;

    @Inject @Source("osgi-services")
    @Filter("(service.label=second)")
    TestService testService1;

    @PostConstruct
    public void activate(){
        System.out.println("Inside Post Constructor Method");
        testService.test();
        testService1.test();
    }

    @Inject
    @Default(values = "Ankur Chauhan")
    private String firstName;

    public String getFirstName() {
        return firstName;
    }
}
Q3. How am I testing these annotations in Sling Model class?

I have created a dummy component and that component calls these Sling Model classes. Sightly code snippet is-

<div data-sly-use.serviceResolver="sling.models.ServiceResolver">
   ${serviceResolver.firstName}
</div>

For complete working code, I am sharing the Git repository link.
https://bitbucket.org/accunitysoft/accunity-blog-snippets

Happy Coding..!!

Ankur Chauhan
Tech Lead

Sling Model with Sightly Part – I

Sling-Model-with-Sightly

In this post, I will explain, how to use sling models with Sightly in AEM6.x?

For doing this, I have created a project using maven archetype for AEM6. If you want to use any existing project, then you need to check two things-

1. Dependency for Sling models in your project’s pom.xml file. You can find this maven dependency in you AEM instance package finder tab as shown in fig-

1.1

Now search for this dependency in your Maven project parent pom.xml file. If it’s already there then it’s fine else add this dependency into your project.

2. In whatever java packages, you want to add your Sling Model classes, add these java packages information into your maven-bundle-plugin.
For example, I am using two java package for adding my Sling Model classes, these packages are-
sling.models and com.blog.sling.models, so I have to place these package information into my maven-bundle-plugin, as shown below-

<plugin>
    <groupId>org.apache.felix</groupId>
    <artifactId>maven-bundle-plugin</artifactId>
    <extensions>true</extensions>
    <configuration>
        <instructions>
            <Sling-Model-Packages>
                sling.models,
                com.blog.sling.models
            </Sling-Model-Packages>
            <Bundle-Category>sling-model-demo</Bundle-Category>
        </instructions>
    </configuration>
</plugin>
Q1). What will happen, If I don’t add these entries?

If you don’t add these entries, then maven will not add
“Sling-Model-Packages: sling.models, com.blog.sling.models”
header entry in your bundle Manifest file. So that these classes will not behave as sling models and will work as simple java classes and, If you try to run your code without this entry, then you will not get desired output as well as there will be no error message in error.log file.

In this post, I will create a very basic example where I will get a name from the component dialog and print that value using Sling Model. So Let’s start-

Create a Sling model class.

package sling.models;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Optional;

import javax.annotation.PostConstruct;
import javax.inject.Inject;

@Model(adaptables = Resource.class)
public class ResourceModel {

    @Inject
    private String firstName;
    private String value;

    @PostConstruct
    public void activate() {
        value = "Hi! " + firstName;
    }

    public String getValue() {
        return value;
    }
}

Here I am using @Inject annotation on “firstName” property, it means that this property will be looked up from the resource (after first adapting it to a ValueMap) and it is injected.
@PostConstructor annotation defines that this method will run after injecting all field (having @Inject annotation) from the resource.

Create the component as shown below-

1.2
In slingModel.html file add these lines.

<h4> Sling Model Examples </h4>
<div data-sly-test="${properties.firstName}" data-sly-use.slingModel="sling.models.ResourceModel">
     ${slingModel.value}
</div>

My dialog properties are as shown below: –

Dialogue Properties

 

 

Build your maven project and drop this component on your page. You will see a screen as-
1.3

Open the dialog of this component and add any entry (For example “Ankur Chauhan”) you will see the desired output as-
1.5

Q2). Why are we creating Sling Model class for this example even we can directly use ${properties.firstName} in our slingModel.html file?

You are right, We can do that or you can say, we must do that. But as I already mention that, in this post I am going to show you a very basic use of Sling Model. So I think this is the simplest example for this post. You will see must better examples in my coming blogs.

I am also sharing the Git repository link.
Git repository link is –

https://bitbucket.org/argildx/accunity-blog-snippets/src/master/

Happy Coding..!!

Ankur Chauhan
Tech Lead