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 – III (Key Annotations – I)

Sling Models with Sightly Part III

The title of this blog is “Key Annotations – I” because In this post, I will explain two important annotations related to Sling Models and continue explaining other annotations in my coming posts. So for explaining two annotations, I have selected following two questions for this post.

1). How to include OSGI Services in Sling Model?
2). What is the use of @ResoucePath annotation?

I am assuming that you are using the latest versions of Sling Model APIs that I described in my previous blog.
For Answering first question, I have created an OSGi Service that have a dummy method as follows-

@Component(immediate = true,enabled = true,metatype = false)
@Service (DemoService.class)
public class DemoService {

    private Logger logger = LoggerFactory.getLogger(DemoService.class);

    public void dummyMethod(){
        logger.info("Inside Dummy Method, its working fine. ");
    }
}
Now, You can include OSGi Services in your Sling Models using two annotations, these are-

1. @OSGiService Annotations
2. @Inject Annotation

Here is the Sling Model class, that shows how to use these annotations?

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

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

    @Inject @Source("osgi-services")
    DemoService demoService;

    @OSGiService
    DemoService demoServiceX;

    @PostConstruct
    public void activate(){
        demoService.dummyMethod();
        demoServiceX.dummyMethod();
    }

    public String getFirstName() {
        return firstName;
    }
}

When you call this Model class then you will see that both of these two annotations working in the same manner.

Q1. What is the use of @ResoucePath annotations?

@ResoucePath annotation is a very handy annotation provide by Sling and using this annotation, you can convert a path into its resource object without writing any code. Let’s suppose you have a predefined path (e.g. /content/geometrixx/en) of the resource and want to convert that path into resource object then you can use @ResourcePath annotation.

Q2. How to use @ResoucePath annotation?

Here the code snippet that shows you the use of this annotation-

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

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

    @Inject @ResourcePath(path = "/content/geometrixx/en")
    Resource tempRes;

    @PostConstruct
    public void activate(){
        System.out.println( tempRes + " = Resource Path");
    }

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

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

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

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

Happy Coding..!!

Ankur Chauhan
Tech Lead

New Features in AEM 6.1 Sightly

New Features in AEM 6.1 Sightly

In this blog, I will explain new features introduced in AEM6.1 Sightly. These features will make your sightly code more shorter and simple. These features are listed below.

  1. Introduction of <sly> tag.
  2. data-sly-repeat
  3. URL manipulation
  4. local for i18n

I will explain these features in the form of questionnaires, so let’s start.

Q1). What is the use of <sly> tag?

This tag is used to remove extra divs those are generated when we include a resource in our component’s HTML file using Sightly code.

Q2). We can do that using data-sly-unwrap, why <sly>?

For explaining this, Let’s consider we have to include a parsys component in our HTML without extra divs using data-sly-unwrap then your code line will be-

<div data-sly-resource="${@ path='par', resourceType='foundation/components/parsys'}" data-sly-unwrap="true" ></div> or
<div data-sly-resource="${@ path='par', resourceType='foundation/components/parsys'}" data-sly-unwrap ></div>

It will work fine but in this case, you have to first define a <div> tag to use data-sly-resource attribute and you also have to write data-sly-unwrap to remove this extra <div> tag.
Isn’t it simple if you don’t have to write this extra <div> tag and data-sly-unwrap.

Q3). How to use <sly> tag? or what is the syntax of <sly> tag?

If you want to write the same line using <sly> tag then code will be-

<sly data-sly-resource="${@ path='par', resourceType='foundation/components/parsys'}" /> or
<sly data-sly-resource="${@ path='par', resourceType='foundation/components/parsys'}"></sly>

Here, you can see the difference between old approach and <sly> tag.

Q4). will <sly> tag not generate any extra divs?

Yes, it will not generate any extra <div> tag or any HTML DOM element, you can think it as a replacement of data-sly-unwrap Sightly tag.

Q5). is data-sly-unwrap is deprecated or removed from AEM6.1 Sightly?

No, you can use it as well but as you have a much better <sly> tag then I think data-sly-unwrap is not required. I think, It is available only to support the AEM6.0 Sightly code.

Q6). What is the data-sly-repeat tag in Sightly?

This tag is used to iterate any of the list element.

Q7). Can we do that using data-sly-list in Sightly?

Yes, you can do that by using data-sly-list but the only difference is that, if you use data-sly-repeat, you will not have to provide any container element.

Q8). Not getting your point, could you please elaborate on your statement i.e. any container element?

Let us consider you want to iterate all child pages of currentPage object then first use data-sly-list you will write the code as –

<div data-sly-list="${ currentPage.listChildren }">
      ${item.name}
</div>

Output will be generated as –

test1 test2 test3 test4

And when you view the HTML element structure in you debugger then it will be –

<div>
   Test1
   Test2
   Test3
   Test4
<div>

Now use data-sly-repeat

<div data-sly-repeat="${ currentPage.listChildren }">
      ${item.name}
</div>

Output will be –

test1
test2
test3
test4

And when you view the HTML element structure in you debugger then it will be –

<div>
      test1
</div>
<div>
      test2
</div>
<div>
      test3
</div>
<div>
      test4
</div>

You can see these generated DOM elements in debugger’s Elements tab.

Q9). We can achieve same DOM structure using data-sly-list, is it required to use data-sly-repeat for the same?

No, it is not required but using data-sly-repeat your HTML code will become shorter. For ex. If you want to achieve

<div>
      test1
</div>
<div>
      test2
</div>
<div>
      test3
</div>
<div>
      test4
</div>

output then the code you have to write with data-sly-list will be –

<div data-sly-list="${ currentPage.listChildren }" data-sly-unwrap>
       <div> ${item.name} </div>
</div>

Here you have to add data-sly-unwrap and <div> tag for each element. But by using data-sly-repeat, These tags will not be required and your HTML looks more refined, simple and less complex.

Q10). What is URL manipulation in AEM6.1 Sightly?

In AEM 6.1 you can do some URL manipulations which are not available in AEM6.0 Sightly. Some of the examples are –
Adding extension, code is –

${'content/geometrixx/en'@ extension='json'}

Output will be –

content/geometrixx/en.json

Adding scheme (http or https) if required –

 ${'content/geometrixx/en'@ scheme='http', extension='json'}

Output will be –

http://content/geometrixx/en.json

Adding Selectors –

${'content/geometrixx/en'@  selectors='s1', extension='html'}

Output will be –

 content/geometrixx/en.s1.html

Adding Fragments –

 ${currentPage.path @ selectors='s1.s2', extension='html', fragment='blog'}

Output will be –

content/geometrixx/en.s1.s2.html#blog
Q11). What is the locale for i18n?

This locale is newly introduced object in AEM6.1. When we work with i18n in Sightly we can use this object to set custom locale. It can be used as –

${'Hello World' @ i18n, locale=request.locale}
Note –  This object is not present in AEM6.

I am also sharing the Git repository link where you can find demo examples for these properties.
Git repository link is –

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

Happy Coding..!!

Ankur Chauhan
Tech Lead