Best Practices for Creating Transformers in Adobe Experience Manager (AEM)

Problem statement:

What is the Adobe recommended way to create a TransformerFactory in AEM?

Requirement:

This article provides best practices for creating Transformers in Adobe Experience Manager (AEM). It discusses the recommended way to create a TransformerFactory and outlines the steps to create a powerful mechanism that rewrites the output generated by the Sling rendering process. The article also includes sample code for creating a Component Service with TransformerFactory class and implementing a TransformerFactory class.

Create the Transformer to rewrite the output with powerful mechanisms

Introduction:

The TransformerFactory is a service that creates Transformers on demand. The created transformers form the middle part of the rewriter pipeline. The factories themselves are not chained but the resulting transformers are. On each pipeline call new instances are created. The factory is referenced using a service property named ‘pipeline. type’. Each factory should have a unique value for this property. With the optional property ‘pipeline. mode’ set to the value ‘global’ the transformer is used for each and every pipeline regardless of the actual configuration for this pipeline. All available global transformers with a service ranking below zero are chained right after the generator. All available global transformers with a service ranking higher or equal to zero are chained right before the serializer. Therefore the property “service.ranking” should be used for the factory in combination with ‘pipeline.mode’. To be compatible with possible future uses of the ‘pipeline.mode’ property, it should only be used with the value ‘global’.

This is a powerful mechanism that rewrites the output (typically HTML markup) generated by the Sling rendering process. It is part of the Apache Sling Rewriter module, which uses SAX event-based pipelines as shown here.

Every pipeline consists of three components, and each component has a corresponding Java interface and factory:

Create Component Service with TransformerFactory class and implement TransformerFactory class

Provide a pipeline for the action

Override createTransformer method

package com.mysite.core.filters;

import org.apache.sling.rewriter.Transformer;
import org.apache.sling.rewriter.TransformerFactory;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link TransformerFactory} defined to create new {@link ContentVariableTransformer} objects and pass in the reference to
 * the service used to aggregate properties.
 */
@Component(service = TransformerFactory.class, property = {
        "pipeline.type=ccvar-transformer"
})
public class ContentVariableTransformerFactory implements TransformerFactory {
    private static final Logger LOG = LoggerFactory.getLogger(ContentVariableTransformerFactory.class);


    @Override
    public Transformer createTransformer() {
        LOG.trace("Content Variable Transformer");
        return new ContentVariableTransformer();
    }
}

Create the transformer implementation and override the init() method

package com.mysite.core.filters;

import java.io.IOException;
import java.util.Map;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.rewriter.ProcessingComponentConfiguration;
import org.apache.sling.rewriter.ProcessingContext;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import com.adobe.acs.commons.rewriter.ContentHandlerBasedTransformer;

/**
 * {@link org.apache.sling.rewriter.Transformer} used to process HTML requests and replace content tokens found in the
 * rendered HTML.
 */
public class ContentVariableTransformer extends ContentHandlerBasedTransformer {

    private Map<String, Object> contentVariableReplacements;

    public ContentVariableTransformer() {
    }

    @Override
    public void init(ProcessingContext processingContext, ProcessingComponentConfiguration processingComponentConfiguration) throws IOException {
        SlingHttpServletRequest request = processingContext.getRequest();
    }

    public void startElement(String uri, String localName, String quaName, Attributes atts) throws SAXException {
    }

    public void characters(char[] ch, int start, int length) throws SAXException {
    }
}

References:

https://github.com/Adobe-Consulting-Services/acs-aem-commons/blob/master/bundle/src/main/java/com/adobe/acs/commons/rewriter/impl/ContentVariableTransformerFactory.java

https://github.com/Adobe-Consulting-Services/acs-aem-commons/blob/master/bundle/src/main/java/com/adobe/acs/commons/rewriter/impl/ContentVariableTransformer.java

AEM Filter – Best Practices for Creating Filters

Problem statement:

What are the recommended ways to create filters in AEM?

Requirement:

This article discusses the best practices for creating filters in Adobe Experience Manager (AEM). Filters are objects that perform filtering tasks on requests or responses from a resource. The article covers creating a component service with a filter class, providing a service description, ranking, and vendor for the filter, and overriding the doFilter method to chain requests and add logs.
Create the Filters to chain the requests and add logs

Introduction:

  • A filter is an object that performs filtering tasks on either the request to a resource (a servlet or static content), the response from a resource, or both.
  • Filters perform filtering in the doFilter method. Every Filter has access to a FilterConfig object from which it can obtain its initialization parameters, and a reference to the ServletContext which it can use, for example, to load resources needed for filtering tasks.
  • Filters are configured in the deployment descriptor of a web application.

Create a Component Service with a Filter class and implement a Filter class

Provide service description, ranking, and vendor for the filter

Override the doFilter method and chain the requests

package com.mysite.core.filters;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.engine.EngineConstants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.propertytypes.ServiceDescription;
import org.osgi.service.component.propertytypes.ServiceRanking;
import org.osgi.service.component.propertytypes.ServiceVendor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Simple servlet filter component that logs incoming requests.
 */
@Component(service = Filter.class,
           property = {
                   EngineConstants.SLING_FILTER_SCOPE + "=" + EngineConstants.FILTER_SCOPE_REQUEST,
           })
@ServiceDescription("Demo to filter incoming requests")
@ServiceRanking(-700)
@ServiceVendor("Adobe")
public class LoggingFilter implements Filter {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response,
                         final FilterChain filterChain) throws IOException, ServletException {

        final SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;
        logger.debug("request for {}, with selector {}", slingRequest
                .getRequestPathInfo().getResourcePath(), slingRequest
                .getRequestPathInfo().getSelectorString());

        filterChain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }
}

Creating a Listener in AEM: The Recommended Way

Problem statement:

What is the Adobe Experience Manager recommended way to create a Listener?

Requirement:

Adobe provides a Framework service registry that allows EventHandler objects to be registered and notified when an event is sent or posted. This article explains the recommended way to create a Listener to handle events on the property in Adobe.

Introduction:

  • EventHandler objects are registered with the Framework service registry and are notified with an Event object when an event is sent or posted.
  • EventHandler objects can inspect the received Event object to determine its topic and properties.
  • EventHandler objects must be registered with a service property EventConstants.EVENT_TOPIC whose value is the list of topics in which the event handler is interested.

Listener:

Create Component Service with Event Handler class and implement Event Handler class Provide event topic for the action

Override handle event method

package com.mysite.core.listeners;

import org.apache.sling.api.SlingConstants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.propertytypes.ServiceDescription;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(service = EventHandler.class,
           immediate = true,
           property = {
                   EventConstants.EVENT_TOPIC + "=org/apache/sling/api/resource/Resource/*"
           })
@ServiceDescription("Demo to listen on changes in the resource tree")
public class SimpleResourceListener implements EventHandler {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    public void handleEvent(final Event event) {
        logger.debug("Resource event: {} at: {}", event.getTopic(), event.getProperty(SlingConstants.PROPERTY_PATH));
    }
}

AEM Query Builder Optimization using Java Streams and Resource Filter

Problem statement:

Can Java Streams and Resource Filter be used as an alternative to Query Builder queries in AEM for filtering pages and resources based on specific criteria?

Requirement:

The query for the pages whose resurcetype = “wknd/components/page” and get child resources which have an Image component (“wknd/components/image”) and get the file reference properties into a list

Query builder query would be like this:

@PostConstruct
private void initModel() {
  Map < String, String > map = new HashMap < > ();
  map.put("path", resource.getPath());
  map.put("property", "jcr:primaryType");
  map.put("property.value", "wknd/components/page");

  PredicateGroup predicateGroup = PredicateGroup.create(map);
  QueryBuilder queryBuilder = resourceResolver.adaptTo(QueryBuilder.class);

  Query query = queryBuilder.createQuery(predicateGroup, resourceResolver.adaptTo(Session.class));
  SearchResult result = query.getResult();

  List < String > imagePath = new ArrayList < > ();

  try {
    for (final Hit hit: result.getHits()) {
      Resource resultResource = hit.getResource();
      @NotNull
      Iterator < Resource > children = resultResource.listChildren();
      while (children.hasNext()) {
        final Resource child = children.next();
        if (StringUtils.equalsIgnoreCase(child.getResourceType(), "wknd/components/image")) {
          Image image = modelFactory.getModelFromWrappedRequest(request, child, Image.class);
          imagePath.add(image.getFileReference());
        }
      }
    }
  } catch (RepositoryException e) {
    LOGGER.error("error occurered while getting result resource {}", e.getMessage());
  }
}

Introduction

This article discusses the use of Java Streams and Resource Filter in optimizing AEM Query Builder queries. The article provides code examples for using Resource Filter Streams to filter pages and resources and using Java Streams to filter and map child resources based on specific criteria. The article also provides optimization strategies for AEM tree traversal to reduce memory consumption and improve performance.

Resource Filter bundle provides a number of services and utilities to identify and filter resources in a resource tree.

Resource Filter Stream:

ResourceFilterStream combines the ResourceStream functionality with the ResourcePredicates service to provide an ability to define a Stream<Resource> that follows specific child pages and looks for specific Resources as defined by the resources filter script. The ResourceStreamFilter is accessed by adaptation.

ResourceFilterStream rfs = resource.adaptTo(ResourceFilterStream.class);
rfs
  .setBranchSelector("[jcr:primaryType] == 'cq:Page'")
  .setChildSelector("[jcr:content/sling:resourceType] != 'apps/components/page/folder'")
  .stream()
  .collect(Collectors.toList());

Parameters

The ResourceFilter and ResourceFilteStream can have key-value pairs added so that the values may be used as part of the script resolution. Parameters are accessed by using the dollar sign ‘$’

rfs.setBranchSelector("[jcr:content/sling:resourceType] != $type").addParam("type","apps/components/page/folder");

Using Resource Filter Stream the example code would look like below:

@PostConstruct
private void initModel() {
  ResourceFilterStream rfs = resource.adaptTo(ResourceFilterStream.class);
  List < String > imagePaths = rfs.setBranchSelector("[jcr:primaryType] == 'cq:Page'")
    .setChildSelector("[jcr:content/sling:resourceType] == 'wknd/components/page'")
    .stream()
    .filter(r -> StringUtils.equalsIgnoreCase(r.getResourceType(), "wknd/components/image"))
    .map(r -> modelFactory.getModelFromWrappedRequest(request, r, Image.class).getFileReference())
    .collect(Collectors.toList());
}

Optimizing Traversals

Similar to indexing in a query there are strategies that you can do within a tree traversal so that traversals can be done in an efficient manner across a large number of resources. The following strategies will assist in traversal optimization.

Limit traversal paths

In a naive implementation of a tree traversal, the traversal occurs across all nodes in the tree regardless of the ability of the tree structure to support the nodes that are being looked for. An example of this is a tree of Page resources that has a child node of jcr:content which contains a subtree of data to define the page structure. If the jcr:content node is not capable of having a child resource of type Page and the goal of the traversal is to identify Page resources that match specific criteria then the traversal of the jcr:content node can not lead to additional matches. Using this knowledge of the resource structure, you can improve performance by adding a branch selector that prevents the traversal from proceeding down a nonproductive path

Limit memory consumption

The instantiation of a Resource object from the underlying ResourceResolver is a nontrivial consumption of memory. When the focus of a tree traversal is obtaining information from thousands of Resources, an effective method is to extract the information as part of the stream processing or utilize the forEach method of the ResourceStream object which allows the resource to be garbage collected in an efficient manner.

References:

https://sling.apache.org/documentation/bundles/resource-filter.html

Efficiently Iterating Child Nodes in Adobe Experience Manager (AEM)

Problem statement:

How can I iterate child nodes and get certain properties? Specifically, the requirement is to get child resources of the current resource and get all image component file reference properties into a list.

Requirement:

Get child resources of the current resource and get all image component file reference properties into a list

Can I use Java 8 Streams?

Introduction: Using while or for loop:

@PostConstruct
private void initModel() {
  List < String > imagePath = new ArrayList < > ();
  Iterator < Resource > children = resource.listChildren();
  while (children.hasNext()) {
    final Resource child = children.next();
    if (StringUtils.equalsIgnoreCase(child.getResourceType(), "wknd/components/image")) {
      Image image = modelFactory.getModelFromWrappedRequest(request, child, Image.class);
      imagePath.add(image.getFileReference());
    }
  }
}

Introduction Abstract Resource Visitor:

Sling provides AbstractResourceVisitor API, which performs traversal through a resource tree, which helps in getting child properties.

Create the class which extends AbstractResourceVisitor abstract class

Override accept, traverseChildren and visit methods as shown below

Call visit inside accepts method instead of super. visit, I have observed it was traversing twice if I use super hence keep this in mind

package utils;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.AbstractResourceVisitor;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import com.day.cq.wcm.foundation.Image;
import com.drew.lang.annotations.NotNull;

public class ExampleResourceVisitor extends AbstractResourceVisitor {
	
	private static final String IMAGE_RESOURCE_TYPE = "wknd/components/image";
	private static final String TEXT_RESOURCE_TYPE = "wknd/components/text";
	
	private static final ArrayList<String> ACCEPTED_PRIMARY_TYPES = new ArrayList<>();
	static {
		ACCEPTED_PRIMARY_TYPES.add(IMAGE_RESOURCE_TYPE);
		ACCEPTED_PRIMARY_TYPES.add(TEXT_RESOURCE_TYPE);
	}
	
	private final List<String> imagepaths = new ArrayList<>();	
	
	public List<String> getImagepaths() {
		return imagepaths;
	}

	@Override
	public final void accept(final Resource resource) {
		if (null != resource) {
			final ValueMap properties = resource.adaptTo(ValueMap.class);
			final String primaryType = properties.get(ResourceResolver.PROPERTY_RESOURCE_TYPE, StringUtils.EMPTY);
			if(ACCEPTED_PRIMARY_TYPES.contains(primaryType)){
				visit(resource);
			}
			this.traverseChildren(resource.listChildren());
		}
	}

	@Override
	protected void traverseChildren(final @NotNull Iterator<Resource> children) {
		while (children.hasNext()) {
			final Resource child = children.next();
			accept(child);
		}
	}

	@Override
	protected void visit(@NotNull Resource resource) {
		final ValueMap properties = resource.adaptTo(ValueMap.class);
		final String primaryType = properties.get(ResourceResolver.PROPERTY_RESOURCE_TYPE, StringUtils.EMPTY);
		if (StringUtils.equalsIgnoreCase(primaryType, IMAGE_RESOURCE_TYPE)) {
			imagepaths.add(properties.get(Image.PN_REFERENCE, StringUtils.EMPTY));
		}
	}
}

Call the ExampleResourceVisitor and pass the resource and call the getImagepaths() to get the list of image paths

@PostConstruct
private void initModel() {
  ExampleResourceVisitor exampleResourceVisitor = new ExampleResourceVisitor();
  exampleResourceVisitor.accept(resource);
  List < String > imageVisitorPaths = exampleResourceVisitor.getImagepaths();
}

Introduction Resource Filter Stream:

Resource Filter bundle provides a number of services and utilities to identify and filter resources in a resource tree.

Resource Filter Stream:

ResourceFilterStream combines the ResourceStream functionality with the ResourcePredicates service to provide an ability to define a Stream<Resource> that follows specific child pages and looks for specific Resources as defined by the resources filter script. The ResourceStreamFilter is accessed by adaptation.

ResourceFilterStream rfs = resource.adaptTo(ResourceFilterStream.class);
rfs.stream().collect(Collectors.toList());

Example code for our problem statement would be like this:

@PostConstruct
private void initModel() {
  ResourceFilterStream rfs = resource.adaptTo(ResourceFilterStream.class);
  List < String > imagePaths = rfs.stream()
    .filter(r -> StringUtils.equalsIgnoreCase(r.getResourceType(), "wknd/components/image"))
    .map(r -> modelFactory.getModelFromWrappedRequest(request, r, Image.class).getFileReference())
    .collect(Collectors.toList());
}

Optimizing Traversals

Similar to indexing in a query there are strategies that you can do within a tree traversal so that traversals can be done in an efficient manner across a large number of resources. The following strategies will assist in traversal optimization.


Limit traversal paths

In a naive implementation of a tree traversal, the traversal occurs across all nodes in the tree regardless of the ability of the tree structure to support the nodes that are being looked for. An example of this is a tree of Page resources that has a child node of jcr:content which contains a subtree of data to define the page structure. If the jcr:content node is not capable of having a child resource of type Page and the goal of the traversal is to identify Page resources that match specific criteria then the traversal of the jcr:content node can not lead to additional matches. Using this knowledge of the resource structure, you can improve performance by adding a branch selector that prevents the traversal from proceeding down a nonproductive path


Limit memory consumption

The instantiation of a Resource object from the underlying ResourceResolver is a nontrivial consumption of memory. When the focus of a tree traversal is obtaining information from thousands of Resources, an effective method is to extract the information as part of the stream processing or utilize the forEach method of the ResourceStream object which allows the resource to be garbage collected in an efficient manner.

Best Practices for Creating OSGi Scheduler in AEM

Problem statement:

The article aims to provide guidelines for creating OSGi schedulers in Adobe Experience Manager, addressing the following queries:

  • What is new with the AEM scheduler with OSGi R7/8 annotations?
  • How to use the scheduler service?
  • Why is enable/disable important on a scheduler?

Requirement:

The article describes the best practices for creating OSGi Schedulers in Adobe Experience Manager (AEM). A scheduler is used to schedule time/cron-based jobs in AEM, and it executes the jobs. The article provides information on creating a scheduler, providing an enabled boolean attribute to start or stop the scheduler, and using the @Activate and @Modified methods. The article also provides a template to create an AEM scheduler.

Introduction:

A scheduler to schedule time/cron based jobs. A job is an object that is executed/fired by the scheduler. The object should either implement the Job interface or the Runnable interface. A job can be scheduled either by creating a ScheduleOptions instance through one of the scheduler methods and then calling schedule(Object, ScheduleOptions) or by using the whiteboard pattern and registering a Runnable service with either the PROPERTY_SCHEDULER_EXPRESSION or PROPERTY_SCHEDULER_PERIOD property. If both properties are specified, only PROPERTY_SCHEDULER_PERIOD is considered for scheduling. Services registered by the whiteboard pattern can by default run concurrently, which usually is not wanted. Therefore it is advisable to also set the PROPERTY_SCHEDULER_CONCURRENT property with Boolean.FALSE. Jobs started through the scheduler API are not persisted and are not restarted after a bundle restart. If the client bundle is stopped, the scheduler will stop all jobs started by this bundle as well. However, the client bundle does not need to keep a reference to the scheduler service.

Create Scheduler Config – OCD

Create a package for config for adding Scheduler related OCD
Creating separate configs will help in the long run if more configs are required for the scheduler

Things to keep in mind:
  1. Always provide an enabled boolean attribute to start or stop the scheduler (sometimes the scheduler takes a long time to run hence this helps to remove those schedulers)
  2. Add the scheduler based on the condition in @Activate, @Modified method
package com.mysite.core.schedulers.config;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(name="A scheduled task", description = "Simple demo for cron-job like task with properties")
public @interface SimpleScheduledTaskConfig {

    @AttributeDefinition(name = "Cron-job expression")
    String schedulerExpression() default "*/30 * * * * ?";

    @AttributeDefinition(name = "Concurrent task", description = "Whether or not to schedule this task concurrently")
    boolean schedulerConcurrent() default false;

    @AttributeDefinition(name = "A parameter", description = "Can be configured in /system/console/configMgr")
    String myParameter() default "";

    @AttributeDefinition(name = "Enabled", description = "True, if scheduler service is enabled", type = AttributeType.BOOLEAN)
    public boolean enabled() default true;
}

Creates Scheduler

Create a scheduler using OSGi Component Service DS with a service that has runnable

Reference Scheduler service and sling setting to make sure the scheduler runs only in the author is recommended and override the run method

Make sure the scheduler runs in

  • author mode during @Activate @Modified method
  • get the class’s simple name and use it as a scheduler ID
@Activate
@Modified
protected void activate(SimpleScheduledTaskConfig simpleScheduledTaskConfig) {
  if (isAuthor()) {
    /**
     * Creating the scheduler id
     */
    this.schedulerJobName = this.getClass().getSimpleName();
    addScheduler(simpleScheduledTaskConfig);
    this.myParameter = simpleScheduledTaskConfig.myParameter();
  }
}

Add the scheduler to the scheduler service

private void addScheduler(SimpleScheduledTaskConfig simpleScheduledTaskConfig) {
  /**
   * Check if the scheduler is enabled
   */
  if (simpleScheduledTaskConfig.enabled()) {

    /**
     * Scheduler option takes the cron expression as a parameter and run accordingly
     */
    ScheduleOptions scheduleOptions = scheduler.EXPR(simpleScheduledTaskConfig.schedulerExpression());

    /**
     * Adding some parameters
     */
    scheduleOptions.name(schedulerJobName);
    scheduleOptions.canRunConcurrently(simpleScheduledTaskConfig.schedulerConcurrent());

    /**
     * Scheduling the job
     */
    scheduler.schedule(this, scheduleOptions);

    logger.info("{} Scheduler added", schedulerJobName);
  } else {
    logger.info("Scheduler is disabled");
    removeScheduler();
  }
}

Remove the scheduler if the scheduler is disabled

/**
 * This method removes the scheduler
 */
private void removeScheduler() {
  logger.info("Removing scheduler: {}", schedulerJobName);
  /**
   * Unscheduling/removing the scheduler
   */
  scheduler.unschedule(String.valueOf(schedulerJobName));
}

Use the below template to create the AEM scheduler

package com.mysite.core.schedulers;

import org.apache.sling.commons.scheduler.ScheduleOptions;
import org.apache.sling.commons.scheduler.Scheduler;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mysite.core.schedulers.config.SimpleScheduledTaskConfig;
import com.mysite.core.services.ExampleService;

/**
 * A simple demo for cron-job like tasks that get executed regularly.
 * It also demonstrates how property values can be set. Users can
 * set the property values in /system/console/configMgr
 */
@Component(service=Runnable.class)
@Designate(ocd= SimpleScheduledTaskConfig.class)
public class SimpleScheduledTask implements Runnable {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * Id of the scheduler based on its name
     */
    private String schedulerJobName;

    @Reference
    private Scheduler scheduler;

    @Reference
    private SlingSettingsService slingSettings;

    @Reference
    private ExampleService exampleService;

    private String myParameter;
    
    @Override
    public void run() {
        logger.debug("SimpleScheduledTask is now running, myParameter='{}'", myParameter);
        exampleService.generateContentList(myParameter);
    }

    @Activate
    @Modified
    protected void activate(SimpleScheduledTaskConfig simpleScheduledTaskConfig) {
    	if (isAuthor()) {
            /**
             * Creating the scheduler id
             */
            this.schedulerJobName = this.getClass().getSimpleName();
            addScheduler(simpleScheduledTaskConfig);
            this.myParameter = simpleScheduledTaskConfig.myParameter();
        }
    }

    private void addScheduler(SimpleScheduledTaskConfig simpleScheduledTaskConfig) {
        /**
         * Check if the scheduler is enabled
         */
        if (simpleScheduledTaskConfig.enabled()) {
            /**
             * Scheduler option takes the cron expression as a parameter and run accordingly
             */
            ScheduleOptions scheduleOptions = scheduler.EXPR(simpleScheduledTaskConfig.schedulerExpression());
            /**
             * Adding some parameters
             */
            scheduleOptions.name(schedulerJobName);
            scheduleOptions.canRunConcurrently(simpleScheduledTaskConfig.schedulerConcurrent());
            /**
             * Scheduling the job
             */
            scheduler.schedule(this, scheduleOptions);
            logger.info("{} Scheduler added", schedulerJobName);
        } else {
            logger.info("Scheduler is disabled");
            removeScheduler();
        }
    }

    /**
     * This method removes the scheduler
     */
    private void removeScheduler() {
        logger.info("Removing scheduler: {}", schedulerJobName);
        /**
         * Unscheduling/removing the scheduler
         */
        scheduler.unschedule(String.valueOf(schedulerJobName));
    }

    /**
     * It is use to check whether AEM is running in Publish mode or not.
     * @return Returns true is AEM is in publish mode, false otherwise
     */
    public boolean isAuthor() {
        return this.slingSettings.getRunModes().contains("author");
    }
}

Using Lombok to Simplify Sling Models, Java Beans, and Avoid Boilerplate Code in AEM

Problem Statement:

The problem addressed in this article is the need to avoid writing boilerplate code in Sling models. Writing boilerplate code such as getters and setters can be time-consuming and can lead to code duplication, which can make the code difficult to maintain. Therefore, developers need a way to simplify the process of writing Sling models by removing the boilerplate code.

Requirement:

Avoid boilerplate code like getters and setters and make sling models look a lot simpler.

Introduction:

You can annotate any field with @Getter and/or @Setter, to let Lombok generate the default getter/setter automatically.

A default getter simply returns the field and is named getFoo if the field is called foo (or isFoo if the field’s type is boolean). A default setter is named setFoo if the field is called foo, returns void, and takes 1 parameter of the same type as the field. It simply sets the field to this value.

The generated getter/setter method will be public unless you explicitly specify an AccessLevel, as shown in the example below. Legal access levels are PUBLIC, PROTECTED, PACKAGE, and PRIVATE.

You can also put a @Getter and/or @Setter annotation on a class. In that case, it’s as if you annotate all the non-static fields in that class with the annotation.

Use Lombok in sling model to avoid writing getters and setters and make the class look simpler and remove all the boilerplate code from the Java class

Most of the time we usually create Sling models to grab resource properties and use them on sightly for representation. In other cases, we usually do some custom changes on these injected properties.

Using Lombok on the Sling model removes all the boilerplate code related to all the injected fields and increases maintainability.

Lombok @Getter and @Setter

Create LombokExample Interface:

package com.mysite.core.models;

import org.osgi.annotation.versioning.ConsumerType;

@ConsumerType
public interface LombokExample {
    default String getName() {
        throw new UnsupportedOperationException();
    }
}

Create LombokExampleImpl – Sling model

Lombok @Getter – creates the get method and @Setter creates the set method

In Sightly call useObject.name/path / jcrTitle you will get resource properties

the following class increases the readability and maintainability of the model

package com.mysite.core.models.impl;

import javax.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.models.factory.ModelFactory;
import com.adobe.cq.export.json.ExporterConstants;
import com.mysite.core.bean.ExampleBean;
import com.mysite.core.bean.ExampleConstBean;
import com.mysite.core.models.LombokExample;
import lombok.Getter;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {
		LombokExample.class }, resourceType = WithoutLombokImpl.RESOURCE_TYPE)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class LombokExampleImpl implements LombokExample {
	public static final String RESOURCE_TYPE = "mysite/components/content/lombok";

	@Self
	private SlingHttpServletRequest request;

	@SlingObject
	private Resource currentResource;

	@SlingObject
	private ResourceResolver resourceResolver;

	@ValueMapValue
	@Getter
	private String name;

	@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
	@Getter
	private String path;

	@ValueMapValue(name = "jcr:title", injectionStrategy = InjectionStrategy.OPTIONAL)
	@Default(values = StringUtils.EMPTY)
	@Getter
	private String jcrTitle;

	@OSGiService
	private ModelFactory modelFactory;

	@PostConstruct
	private void init() {
	}
}

Lombok – @Data

Use @Data as a sling model when you are trying to adapt the current resource or some resource result into a different sling model (which doesn’t have any OOTB sling script variable or services like SlingHttpServletRequest, resource, etc.)

package com.mysite.core.models.impl;

import com.adobe.cq.wcm.core.components.models.contentfragment.ContentFragment;
import com.drew.lang.annotations.Nullable;
import lombok.Data;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import javax.annotation.PostConstruct;

@Model(adaptables = SlingHttpServletRequest.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Data
public class CFModelImpl{

    @ValueMapValue(name = ContentFragment.PN_PATH, injectionStrategy = InjectionStrategy.OPTIONAL)
    @Nullable
    private String fragmentPath;

    @ValueMapValue(name = ContentFragment.PN_ELEMENT_NAMES, injectionStrategy = InjectionStrategy.OPTIONAL)
    @Nullable
    private String[] elementNames;

    @ValueMapValue(name = ContentFragment.PN_VARIATION_NAME, injectionStrategy = InjectionStrategy.OPTIONAL)
    @Nullable
    private String variationName;

    @ValueMapValue(name = ContentFragment.PN_DISPLAY_MODE, injectionStrategy = InjectionStrategy.OPTIONAL)
    @Nullable
    private String displayMode;

    @PostConstruct
    private void initModel() {
    }
}

Call Adapt to this new class using ModelFactory(recommended for error handling) to get the injected resource properties

@PostConstruct
	private void init() {
		CFModelImpl cfModel = modelFactory.getModelFromWrappedRequest(request, currentResource, CFModelImpl.class);
		cfModel.getFragmentPath();
		cfModel.getElementNames();
	}

@Data is a convenient shortcut annotation that bundles the features of @ToString, @EqualsAndHashCode, @Getter / @Setter

In other words, @Data generates all the boilerplate that is normally associated with simple POJOs (Plain Old Java Objects) and beans:

  • Getters for all fields, setters for all non-final fields
  • Appropriate toString equals and hashCode implementations that involve the fields of the class,
  • Constructor that initializes all final fields
  • All fields marked as transient will not be considered for hashCode and equals. All static fields will be skipped entirely (not considered for any of the generated methods, and no setter/getter will be made for them).

Create Example Bean with @Data annotation

package com.mysite.core.bean;

import lombok.Data;

@Data
public class ExampleBean {
    private String name;
    private String value;
}

Create an object for ExampleBean and set some values

@PostConstruct
private void init() {
    ExampleBean exampleBean = new ExampleBean();
    exampleBean.setName("Name");
    exampleBean.setValue("Kiran");
}

Create a constructor to only name field

define the field name field as final

package com.mysite.core.bean;

import lombok.Data;

@Data
public class ExampleConstBean {
    private final String name;
    private String value;
}

Create an object ExampleConstBean

In the below example, you can see we are able to call the constructor for the name field

@PostConstruct
private void init() {
    ExampleConstBean exampleConstBean = new ExampleConstBean("Name");
    exampleConstBean.setValue("Kiran");
}

You can also use @allargsconstructor if you want a constructor for all the fields

if you want certain fields constructor then declare those fields as final

cons:

the parameters of these annotations (such as callSuper, include field names, and exclude) cannot be set with @Data. If you need to set non-default values for any of these parameters, just add those annotations explicitly;

References:

https://projectlombok.org/features/GetterSetter

https://projectlombok.org/features/Data

Best Practices for Writing a Sling Servlet

Problem Statement:

What is the best way to write a Sling servlet?

Requirement:

The article aims to address the problem of finding the best way to write a Sling servlet and the recommended way to register it in Apache Sling.

Introduction:

Servlets and scripts are themselves resources in Sling and thus have a resource path: this is either the location in the resource repository, the resource type in a servlet component configuration or the “virtual” bundle resource path (if a script is provided inside a bundle without being installed into the JCR repository).

OSGi DS – SlingServletResourceTypes

OSGi DS 1.4 (R7) component property type annotations for Sling Servlets (recommended)

  • Component as servlet based on OSGi R7/8
  • Provide resourcetype, methods, and extensions
  • ServiceDescription for the servlet
package com.mysite.core.servlets;

import com.day.cq.commons.jcr.JcrConstants;
import org.apache.sling.api.SlingHttpServletRequest
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.propertytypes.ServiceDescription;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(service = { Servlet.class })
@SlingServletResourceTypes(resourceTypes = "mysite/components/page", methods = HttpConstants.METHOD_GET, extensions = "txt")
@ServiceDescription("Simple Demo Servlet")
public class SimpleServlet extends SlingSafeMethodsServlet {

	private static final long serialVersionUID = 1L;

	@Override
	protected void doGet(final SlingHttpServletRequest req, final SlingHttpServletResponse resp)
			throws ServletException, IOException {
		final Resource resource = req.getResource();
		resp.setContentType("text/plain");
		resp.getWriter().write("Title = " + resource.getValueMap().get(JcrConstants.JCR_TITLE));
	}
}

Simple OSGi DS 1.2 annotations

Registering Servlet by path

  • Component as servlet based on OSGi R7/8
  • Servlet paths, method, and other details
package com.mysite.core.servlets;

import com.drew.lang.annotations.NotNull;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(service = { Servlet.class }, configurationPolicy = ConfigurationPolicy.REQUIRE, property = {
        "sling.servlet.paths=" + PathBased.RESOURCE_PATH, "sling.servlet.methods=GET" })
public class PathBased extends SlingSafeMethodsServlet {
    static final String RESOURCE_PATH = "/bin/resourcepath";

    @Override
    protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response)
            throws ServletException, IOException {
    }
}
@SlingServlet(
    resourceTypes = "/apps/my/type",
    selectors = "hello",
    extensions = "html",
    methods = "GET")
public class MyServlet extends SlingSafeMethodsServlet {

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
        ...
    }
}

Configuration Policy – Required

  • configuration policy is mandatory to avoid calling any servlet or services outside a particular environment
  • Its always recommended to run certain services like data sources to work only on authors
<strong>File Name - com.mysite.core.servlets.PathBased</strong>
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
          jcr:primaryType="sling:OsgiConfig"/>

Project Structure

References:

https://sling.apache.org/documentation/the-sling-engine/servlets.html

Best Practices for Writing OSGi Services Using R7/R8 Annotations

Problem Statement:

What is the best way to write an OSGi service?

Requirement:

What is the best way to write an OSGi service? What is the recommended way to register a service, as per R7/R8 annotations?

Introduction:

A Service Component is a Java class that can be optionally registered as an OSGi service and can optionally use OSGi services. The runtime for DS is called Service Component Runtime (SCR) and uses component descriptions in the bundle (in XML form) to instantiate and wire components and services together. The component descriptions are used by SCR as a performance choice so that SCR can avoid processing all classes in a bundle to search for annotations at runtime

As per Adobe’s best practices please start using OSGi R7/R8 annotations, avoid using felix based component annotations

OSGi R7/8

Create Services using Service Component

A Service Component is a Java class that can be optionally registered as an OSGi service and can optionally use OSGi services. The runtime for DS is called Service Component Runtime (SCR) and uses component descriptions in the bundle (in XML form) to instantiate and wire components and services together. The component descriptions are used by SCR as a performance choice so that SCR can avoid processing all classes in a bundle to search for annotations at runtime

Create Example Service – ExampleService

package com.mysite.core.services;

import java.util.Set;

public interface ExampleService {
    /**
     * Start generation content list
     *
     * @return result set
     */
    public Set<String> generateContentList(String path);
}

Create Example Service Object class definition – ExampleServiceConfig

  1. Creating a separate Interface annotation class will help in the long run when we want to introduce more fields
  2. Service config name and description
  3. Attribute name, description, and type
package com.mysite.core.services.config;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(name = "Example Content list generator Service", description = "Example Content list generator Service generates Set")
public @interface ExampleServiceConfig {

    @AttributeDefinition(name = "Enabled Generator", description = "True, to generate the list", type = AttributeType.BOOLEAN)
    boolean isEnabled() default false;
}

Create Service Implementation – ExampleServiceImpl

  1. Get OCD based configs @Activate without any method
  2. You can also get config inside @Modfied with the method
  3. To get a resource resolver inside the service it’s recommended to use “try-with-resource” which helps to never forget to close a resource(avoids unclosed resource resolver warnings)
package com.mysite.core.services.impl;

import com.mysite.core.services.ExampleService;
import com.mysite.core.services.config.ExampleServiceConfig;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@Component(service = ExampleService.class, immediate = true, name = "Example Content Generator Service")
@Designate(ocd = ExampleServiceConfig.class)
public class ExampleServiceImpl implements ExampleService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ExampleServiceImpl.class);
    /** Service User name */
    private static final String SERVICE_USER = "exampleUset";
    private boolean serviceEnabled;

    @Reference
    private ResourceResolverFactory resolverFactory;

    //R8 Annotation
    @Activate
    ExampleServiceConfig exampleServiceConfig;

    //R7 Annotation but captures and @Modified configs
    @Modified
    protected void activate(final ExampleServiceConfig exampleServiceConfig) {
        this.exampleServiceConfig = exampleServiceConfig;
    }

    @Override
    public Set<String> generateContentList(String path) {
        if(exampleServiceConfig.isEnabled()){
            try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))){

            } catch (LoginException e) {
                LOGGER.error("Error Occured during Login", e.getMessage());
            }
        }
        return new HashSet<>();
    }
}

Project Structure

Code structure for best practices

Note: Make sure to add the config XML’s for the services and please don’t depend on the OCD default values.

Best Practices for Creating Extensible and Secure Sling Models in AEM

Problem Statement:

What is the best way to use the Sling model?

Requirements:

Need to create a sling model with best practices like OOTB components and increase the extensibility of the component and security.

Introduction:

The Sling model is a basic POJO, the class is annotated with @Model and the adaptable class. Fields that need to be injected are annotated with recommended injectors.

Its commonly used for injecting component resources and handling manipulation on those properties

Create a Sling model interface

Create a Sling model interface class and impl package with its implementation this helps in the long run when we are trying to write extra features by creating versions for the same component:

Create Interface Class – HelloWorldModel

In the below code, we can see an interface that extends the Component and a few constants and methods which increases the security

package com.mysite.core.models;

import com.adobe.cq.wcm.core.components.models.Component;
import org.osgi.annotation.versioning.ConsumerType;

@ConsumerType
public interface HelloWorldModel extends Component {
    /**
     * if required by multiple classes
     */
    String PN_PARENT_PAGE = "navigationRoot";

    /**
     * Returns the hellow world message
     *
     * @return the message containing resourcetype and page path.
     */
    default String getMessage() {
        throw new UnsupportedOperationException();
    }
}
Create Implementation Class – HelloWorldModelImpl
package com.mysite.core.models.impl;

import static org.apache.sling.api.resource.ResourceResolver.PROPERTY_RESOURCE_TYPE;
import java.util.Optional;
import javax.annotation.PostConstruct;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import com.adobe.cq.export.json.ExporterConstants;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.mysite.core.models.HelloWorldModel;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {HelloWorldModel.class}, resourceType = HelloWorldModelImpl.RESOURCE_TYPE)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME , extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class HelloWorldModelImpl implements HelloWorldModel {

    public static final String RESOURCE_TYPE = "mysite/components/content/hello";

    @ValueMapValue(name=PROPERTY_RESOURCE_TYPE, injectionStrategy=InjectionStrategy.OPTIONAL)
    @Default(values="No resourceType")
    protected String resourceType;

    @SlingObject
    private Resource currentResource;

    @SlingObject
    private ResourceResolver resourceResolver;

    private String message;

    @PostConstruct
    protected void init() {
        PageManager pageManager = resourceResolver.adaptTo(PageManager.class);
        String currentPagePath = Optional.ofNullable(pageManager)
                .map(pm -> pm.getContainingPage(currentResource))
                .map(Page::getPath).orElse("");

        message = "Hello World!\n"
                + "Resource type is: " + resourceType + "\n"
                + "Current page is:  " + currentPagePath + "\n";
    }

    public String getMessage() {
        return message;
    }
}

Implementation class will implement and adapters to interface class HellWorldModel

Please make sure

  1. The default adaption(adaptables) strategy is preferably SlingHTTPRequest compared or Resource
  2. We should always add Jackson exporter so that the model will be export ready
  3. Make sure always use relevant injector instead of @inject annotations refer to all the injectors: https://sling.apache.org/documentation/bundles/models.html
  4. if multiple fields have the optional injection, then add the following code to the class to make all the variables optional
@Model(adaptables = SlingHttpServletRequest.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)

Project structure

AEM Core Image component:

References

Interface:
https://github.com/adobe/aem-core-wcm-components/blob/master/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/models/Image.java

Implementation:
https://github.com/adobe/aem-core-wcm-components/blob/master/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/models/v3/ImageImpl.java

Advantages of this pattern:

  1. Create multiple versions of the component
  2. Improves code readability
  3. Don’t have to retouch the existing implementation but can extend the parent class to improve functionality
  4. Easy to revert back to the production code
  5. Improves unit testing and regression testing