AEM Invoke API – REST service using HTTP Client factory

Problem Statement:

The article addresses the problem of making REST-based calls from AEM to an external system and the best way to handle HTTP requests.

Requirement:

The article discusses the implementation of an OSGi-based REST service in AEM that integrates with an external system using the HTTP Client factory. The author provides detailed steps on how to create a new Apache Closable HTTP Client, prepare request configuration, pool HTTP connections, and use default headers and keepAlive strategy to execute requests.

Create an OSGi based REST service to integrate AEM with the external system, and also provide config to provide endpoint options and client factory configurations.

Introduction:

As we all know AEM is REST based Web application, however, is there a way to integrate OSGi based service to make calls to the external system.

After going through the ACS commons based HTTP Client factory, I created a more feature-friendly and rich HTTP client factory.

Create HTTPClientFactory Service Interface:

This service provides the implementation for most of the basic HTTP REST based operations like GET, PUT, POST, and DELETE operations.

package com.example.core.services;

import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request;

/**
 * Factory for building pre-configured HttpClient Fluent Executor and Request objects
 * based a configure host, port and (optionally) username/password.
 * Factories will generally be accessed by service lookup using the factory.name property.
 */
public interface HttpClientFactory {

    /**
     * Get the configured Executor object from this factory.
     *
     * @return an Executor object
     */
    Executor getExecutor();

    /**
     * Create a GET request using the base hostname and port defined in the factory configuration.
     *
     * @param partialUrl the portion of the URL after the port (and slash)
     *
     * @return a fluent Request object
     */
    Request get(String partialUrl);

    /**
     * Create a PUT request using the base hostname and port defined in the factory configuration.
     *
     * @param partialUrl the portion of the URL after the port (and slash)
     *
     * @return a fluent Request object
     */
    Request put(String partialUrl);

    /**
     * Create a POST request using the base hostname and port defined in the factory configuration.
     *
     * @param partialUrl the portion of the URL after the port (and slash)
     *
     * @return a fluent Request object
     */
    Request post(String partialUrl);

    /**
     * Create a DELETE request using the base hostname and port defined in the factory configuration.
     *
     * @param partialUrl the portion of the URL after the port (and slash)
     *
     * @return a fluent Request object
     */
    Request delete(String partialUrl);

    /**
     * Create a OPTIONS request using the base hostname and port defined in the factory configuration.
     *
     * @param partialUrl the portion of the URL after the port (and slash)
     *
     * @return a fluent Request object
     */
    Request options(String partialUrl);

    /**
     * Get External URI type is form the factory configuration.
     *
     * @return External URI Type
     */
    String getExternalURIType();

    /**
     * Get apiStoreLocatorHostName URI type is form the factory configuration.
     *
     * @return API StoreLocatorHost
     */
    String getApiStoreLocatorHostName();

    Request postWithAbsolute(String absolutelUrl);
}

Create HTTPClientFactoryConfig:

Add the required attributes to create the HTTPCLientFactory.

package com.example.services.config;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import com.example.constants.Constants;

@ObjectClassDefinition(name = "Http Client API Configuration", description = "Http Client API Configuration")
public @interface HttpClientFactoryConfig {

    @AttributeDefinition(name = "API Host Name", description = "API host name, e.g. https://example.com", type = AttributeType.STRING)
    String apiHostName() default Constants.DEFAULT_API_HOST_NAME;

    @AttributeDefinition(name = "API URI Type Path", description = "API URI type path, e.g. /services/int/v2", type = AttributeType.STRING)
    String uriType() default Constants.DEFAULT_API_URI_TYPE_PATH;

    @AttributeDefinition(name = "API URI Type Path", description = "API URI type path, e.g. /services/ext/v2", type = AttributeType.STRING)
    String uriExternalType() default Constants.DEFAULT_API_URI_EXTERNAL_TYPE_PATH;

    @AttributeDefinition(name = "Relaxed SSL", description = "Defines if self-certified certificates should be allowed to SSL transport", type = AttributeType.BOOLEAN)
    boolean relaxedSSL() default Constants.DEFAULT_RELAXED_SSL;

    @AttributeDefinition(name = "Store Locator API Host Name", description = "Store Locator API host name, e.g. https://example.com", type = AttributeType.STRING)
    String apiStoreLocatorHostName() default Constants.DEFAULT_STORE_LOCATOR_API_HOST_NAME;

    @AttributeDefinition(name = "Maximum number of total open connections", description = "Set maximum number of total open connections, default 5", type = AttributeType.INTEGER)
    int maxTotalOpenConnections() default Constants.DEFAULT_MAXIMUM_TOTAL_OPEN_CONNECTION;

    @AttributeDefinition(name = "Maximum number of concurrent connections per route", description = "Set the maximum number of concurrent connections per route, default 5", type = AttributeType.INTEGER)
    int maxConcurrentConnectionPerRoute() default Constants.DEFAULT_MAXIMUM_CONCURRENT_CONNECTION_PER_ROUTE;

    @AttributeDefinition(name = "Default Keep alive connection in seconds", description = "Default Keep alive connection in seconds, default value is 1", type = AttributeType.LONG)
    int defaultKeepAliveconnection() default Constants.DEFAULT_KEEP_ALIVE_CONNECTION;

    @AttributeDefinition(name = "Default connection timeout in seconds", description = "Default connection timout in seconds, default value is 30", type = AttributeType.LONG)
    long defaultConnectionTimeout() default Constants.DEFAULT_CONNECTION_TIMEOUT;

    @AttributeDefinition(name = "Default socket timeout in seconds", description = "Default socket timeout in seconds, default value is 30", type = AttributeType.LONG)
    long defaultSocketTimeout() default Constants.DEFAULT_SOCKET_TIMEOUT;

    @AttributeDefinition(name = "Default connection request timeout in seconds", description = "Default connection request timeout in seconds, default value is 30", type = AttributeType.LONG)
    long defaultConnectionRequestTimeout() default Constants.DEFAULT_CONNECTION_REQUEST_TIMEOUT;
}

Create HttpClientFactoryImpl Service implementation:

This provides the implementation class for HTTPClientFactory Service and during @Activate/@Modified we are trying to create a new Apache Closable HTTP Client using OSGi based HttpClientBuilderFactory.

HTTP client is like a dish, and you can taste it better if your recipe is great and if you prepare it well, before making calls to the external system.

Close all Connections:

Make sure to close the existing connection if any after bundle gets activated or modified

Preparing Request Configuration:

Create Request Config Object and set Connection timeout, socket timeout, and request timeout based on the service configurations

Pooling HTTP Connection:

PoolingHttpClientConnectionManager maintains a pool of HttpClientConnections and is able to service connection requests from multiple execution threads. Connections are pooled on a per route basis. A request for a route that already the manager has persistent connections for available in the pool will be serviced by leasing a connection from the pool rather than creating a brand new connection.

Hence set the max pool size and number default max per route (per endpoint)

Things to be aware of before pooling connection is, are you making HTTPS calls to the external system if yes? Then create an SSLConnectionSocketFactory with NOOP based verifier and add all the trusted certificates.

Default Keep Alive Strategy:

If the Keep-Alive header is not present in the response, HttpClient assumes the connection can be kept alive indefinitely. However, many HTTP servers in general use are configured to drop persistent connections after a certain period of inactivity to conserve system resources, often without informing the client. In case the default strategy turns out to be too optimistic, one may want to provide a custom keep-alive strategy.

HTTP Client Builder OSGi Service:

Get the reference to OSGi-based httpClientBuilderFactory service, prepare a new builder, set the request configuration, and add a connection manager with a pooling connection.

Add default headers and keepAlive strategy, so that we don’t have to create a new connection

Finally, create the HTTP Client out of this builder and set the client to Apache fluent Executor.

the fluent executor makes an arbitrary HttpClient instance and executes the request.

package com.example.core.services.impl;

import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.osgi.services.HttpClientBuilderFactory;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
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.example.core.services.HttpClientFactory;
import com.example.core.services.config.HttpClientFactoryConfig;

/**
 * Implementation of @{@link HttpClientFactory}.
 * <p>
 * HttpClientFactory provides service to handle API connection and executor.
 */
@Component(service = HttpClientFactory.class)
@Designate(ocd = HttpClientFactoryConfig.class)
public class HttpClientFactoryImpl implements HttpClientFactory {

    private static final Logger log = LoggerFactory.getLogger(HttpClientFactoryImpl.class);

    private Executor executor;
    private String baseUrl;
    private String uriExternalType;
    private String apiStoreLocatorHostName;
    private CloseableHttpClient httpClient;
    private HttpClientFactoryConfig config;

    @Reference
    private HttpClientBuilderFactory httpClientBuilderFactory;

    @Activate
    @Modified
    protected void activate(HttpClientFactoryConfig config) throws Exception {

        log.info("########### OSGi Configs Start ###############");
        log.info("API Host Name : {}", config.apiHostName());
        log.info("URI Type: {}", config.uriType());
        log.info("########### OSGi Configs End ###############");

        closeHttpConnection();

        this.config = config;
        if (this.config.apiHostName() == null) {
            log.debug("Configuration is not valid. Both hostname is mandatory.");
            throw new IllegalArgumentException("Configuration is not valid. Both hostname is mandatory.");
        }

        this.uriExternalType = this.config.uriExternalType();
        this.apiStoreLocatorHostName = this.config.apiStoreLocatorHostName();
        this.baseUrl = StringUtils.join(this.config.apiHostName(), config.uriType());

        initExecutor();
    }

    private void initExecutor() throws Exception {

        PoolingHttpClientConnectionManager connMgr = null;

        RequestConfig requestConfig = initRequestConfig();

        HttpClientBuilder builder = httpClientBuilderFactory.newBuilder();
        builder.setDefaultRequestConfig(requestConfig);

        if (config.relaxedSSL()) {

            connMgr = initPoolingConnectionManagerWithRelaxedSSL();

        } else {

            connMgr = new PoolingHttpClientConnectionManager();
        }

        connMgr.closeExpiredConnections();

        connMgr.setMaxTotal(config.maxTotalOpenConnections());
        connMgr.setDefaultMaxPerRoute(config.maxConcurrentConnectionPerRoute());

        builder.setConnectionManager(connMgr);

        List<Header> headers = new ArrayList<>();
        headers.add(new BasicHeader("Accept", "application/json"));
        builder.setDefaultHeaders(headers);
        builder.setKeepAliveStrategy(keepAliveStratey);

        httpClient = builder.build();

        executor = Executor.newInstance(httpClient);
    }

    private PoolingHttpClientConnectionManager initPoolingConnectionManagerWithRelaxedSSL()
            throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {

        PoolingHttpClientConnectionManager connMgr;
        SSLContextBuilder sslbuilder = new SSLContextBuilder();
        sslbuilder.loadTrustMaterial(new TrustAllStrategy());
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslbuilder.build(),
                NoopHostnameVerifier.INSTANCE);
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslsf).build();
        connMgr = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        return connMgr;
    }

    private RequestConfig initRequestConfig() {

        return RequestConfig.custom()
                .setConnectTimeout(Math.toIntExact(TimeUnit.SECONDS.toMillis(config.defaultConnectionTimeout())))
                .setSocketTimeout(Math.toIntExact(TimeUnit.SECONDS.toMillis(config.defaultSocketTimeout())))
                .setConnectionRequestTimeout(
                        Math.toIntExact(TimeUnit.SECONDS.toMillis(config.defaultConnectionRequestTimeout())))
                .build();
    }

    @Deactivate
    protected void deactivate() {
        closeHttpConnection();
    }

    private void closeHttpConnection() {
        if (null != httpClient) {
            try {
                httpClient.close();
            } catch (final IOException exception) {
                log.debug("IOException while clossing API, {}", exception.getMessage());
            }
        }
    }

    @Override
    public Executor getExecutor() {
        return executor;
    }

    @Override
    public Request get(String partialUrl) {
        String url = baseUrl + partialUrl;
        return Request.Get(url);
    }

    @Override
    public Request post(String partialUrl) {
        String url = baseUrl + partialUrl;
        return Request.Post(url);
    }

    @Override
    public Request postWithAbsolute(String absolutelUrl) {
        return Request.Post(absolutelUrl);
    }

    @Override
    public Request put(String partialUrl) {
        String url = baseUrl + partialUrl;
        return Request.Put(url);
    }

    @Override
    public Request delete(String partialUrl) {
        String url = baseUrl + partialUrl;
        return Request.Delete(url);
    }

    @Override
    public Request options(String partialUrl) {
        String url = baseUrl + partialUrl;
        return Request.Options(url);
    }

    @Override
    public String getExternalURIType() {
        return uriExternalType;
    }

    @Override
    public String getApiStoreLocatorHostName() {
        return apiStoreLocatorHostName;
    }

    ConnectionKeepAliveStrategy keepAliveStratey = new ConnectionKeepAliveStrategy() {

        @Override
        public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
            /*
             * HeaderElementIterator headerElementIterator = new BasicHeaderElementIterator(
             * response.headerIterator(HTTP.CONN_KEEP_ALIVE));
             *
             * while (headerElementIterator.hasNext()) { HeaderElement headerElement =
             * headerElementIterator.nextElement(); String param = headerElement.getName();
             * String value = headerElement.getValue(); if (value != null &&
             * param.equalsIgnoreCase("timeout")) { return
             * TimeUnit.SECONDS.toMillis(Long.parseLong(value)); } }
             */

            return TimeUnit.SECONDS.toMillis(config.defaultKeepAliveconnection());
        }
    };
}
OSGi configuration

References:

https://github.com/kiransg89/AEM-REST-Integration

How to use HTTP Client Factory?

Check this out URI

AEM Core Component Delegation

Problem Statement:

The article aims to explain the process of delegating OOTB components in AEM and customizing them by adding new parameters and methods.


Requirement:

This article is about delegating the Out Of The Box (OOTB) components of Adobe Experience Manager (AEM) using Lombok delegation. The author explains how to add new parameters and methods to the components using delegation. Specifically, the article covers how to delegate the OOTB image component, add custom logic to its existing methods, and introduce new methods such as getCredit() and getSizes().


Delegate OOTB image component and add custom logic on existing methods like getWidth(), getHeight(), and getSrcSet() and also add new methods getCredit() and getSizes()


Introduction:

Any field or no-argument method can be annotated with @Delegate to let Lombok generate delegate methods that forward the call to this field (or the result of invoking this method).

Lombok delegates all public methods of the field’s type (or method’s return type), as well as those of its supertypes except for all methods declared in java.lang.Object.

You can pass any number of classes into the @Delegate annotation’s types parameter. If you do that, then Lombok will delegate all public methods in those types (and their supertypes, except java.lang.Object) instead of looking at the field/method’s type.

All public non-Object methods that are part of the calculated type(s) are copied, whether or not you also wrote implementations for those methods. That would thus result in duplicate method errors. You can avoid these by using the @Delegate(excludes=SomeType.class) parameter to exclude all public methods in the excluded type(s), and their supertypes.

The below image shows how the Lombok delegation happens:

Lombok request handling and delegation
Lombok request delegation

Whenever we make a request to a class (for the Image Sling model) the Lombok delegation will copy all the public methods and exclude the methods which we want to override and provide the custom implementation to those methods.

Image Component Delegation

For our requirement, I am creating a class called ImageDelegate and implementing the Image component interface.

I will be making the call to the Image component using @self via ResourceSuperType and using @Delegate annotation I will be excluding some of the methods inside DelegationExclusion interface class

I will be adding custom code to the overridden methods and also I am able to introduce new methods getCredit(), getSizes()

package com.mysite.core.models.impl;

import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.Image;
import com.day.cq.dam.api.DamConstants;
import com.day.cq.dam.commons.util.DamUtil;
import com.day.cq.wcm.api.designer.Style;
import com.drew.lang.annotations.Nullable;
import lombok.experimental.Delegate;
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.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.injectorspecific.*;
import org.apache.sling.models.annotations.via.ResourceSuperType;
import org.apache.sling.servlets.post.SlingPostConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {
  Image.class
}, resourceType = ImageDelegate.RESOURCE_TYPE, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ImageDelegate implements Image {
  private static final Logger LOGGER = LoggerFactory.getLogger(ImageDelegate.class);
  public static final String RESOURCE_TYPE = "mysite/components/content/image";
  public static final String CONTENT_DAM_PATH = "/content/dam";
  public static final String HTTPS = "https://";
  public static final String PN_DISPLAY_SIZES = "sizes";
  public static final String WIDTH = "{.width}";

  @SlingObject
  private ResourceResolver resourceResolver;

  @SlingObject
  protected Resource resource;

  @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
  @Nullable
  protected String credit;

  @ScriptVariable
  protected Style currentStyle;

  @RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
  String host;

  @Self
  @Via(type = ResourceSuperType.class)
  @Delegate(excludes = DelegationExclusion.class)
  private Image image;

  @Override
  public String getSrc() {
    return prepareSuffix(image.getSrc());
  }

  @Override
  public String getWidth() {
    return DamUtil.resolveToAsset(resourceResolver.resolve(image.getFileReference())).getMetadataValueFromJcr(DamConstants.TIFF_IMAGEWIDTH);
  }

  @Override
  public String getHeight() {
    return DamUtil.resolveToAsset(resourceResolver.resolve(image.getFileReference())).getMetadataValueFromJcr(DamConstants.TIFF_IMAGELENGTH);
  }

  @Override
  public String getSrcset() {
    int[] widthsArray = image.getWidths();
    String srcUritemplate = image.getSrcUriTemplate();
    String[] srcsetArray = new String[widthsArray.length];
    if (widthsArray.length > 0 && srcUritemplate != null) {
      String srcUriTemplateDecoded = "";
      try {
        srcUriTemplateDecoded = HTTPS + host + prepareSuffix(URLDecoder.decode(srcUritemplate, StandardCharsets.UTF_8.name()));
      } catch (UnsupportedEncodingException e) {
        LOGGER.error("Character Decoding failed for {}", resource.getPath());
      }
      if (srcUriTemplateDecoded.contains(WIDTH)) {
        for (int i = 0; i < widthsArray.length; i++) {
          if (srcUriTemplateDecoded.contains("=" + WIDTH)) {
            srcsetArray[i] = srcUriTemplateDecoded.replace(WIDTH, String.format("%s", widthsArray[i])) + " " + widthsArray[i] + "w";
          } else {
            srcsetArray[i] = srcUriTemplateDecoded.replace(WIDTH, String.format(".%s", widthsArray[i])) + " " + widthsArray[i] + "w";
          }
        }
        return StringUtils.join(srcsetArray, ',');
      }
    }
    return null;
  }

  public String getCredit() {
    if (StringUtils.isEmpty(credit)) {
      return DamUtil.resolveToAsset(resourceResolver.resolve(image.getFileReference())).getMetadataValueFromJcr(DamConstants.DC_CREATOR);
    }
    return credit;
  }

  public String getSizes() {
    return currentStyle.get(PN_DISPLAY_SIZES, StringUtils.EMPTY);
  }

  private String prepareSuffix(String imageSrc) {
    if (StringUtils.isNotEmpty(imageSrc) && !StringUtils.containsIgnoreCase(imageSrc, CONTENT_DAM_PATH)) {
      int endIndex = imageSrc.lastIndexOf(SlingPostConstants.DEFAULT_CREATE_SUFFIX);
      String intermittenResult = imageSrc.substring(0, endIndex);
      endIndex = intermittenResult.lastIndexOf(SlingPostConstants.DEFAULT_CREATE_SUFFIX);
      return intermittenResult.substring(0, endIndex) + image.getFileReference();
    }
    return imageSrc;
  }

  private interface DelegationExclusion {
    String getSrc();
    String getSrcset();
    String getWidth();
    String getHeight();
  }
}

Sightly code:

Sightly call to the Image component will remain the same but I will be getting overridden method returns and new method returns as well

<div data-sly-use.image="com.adobe.cq.wcm.core.components.models.Image"
     data-sly-use.templates="core/wcm/components/commons/v1/templates.html"
     data-sly-test="${image.src}"
     data-cmp-dmimage="${image.dmImage}"
     data-asset-id="${image.uuid}"
     id="${image.id}"
     data-cmp-data-layer="${image.data.json}"
     class="cmp-image"
     itemscope itemtype="http://schema.org/ImageObject">
    <a data-sly-unwrap="${!image.imageLink.valid}"
       class="cmp-image__link"
       data-sly-attribute="${image.imageLink.htmlAttributes}"
       data-cmp-clickable="${image.data ? true : false}">
        <img srcset="${image.srcset}" src="${image.src}"
             loading="${image.lazyEnabled ? 'lazy' : 'eager'}"
             class="cmp-image__image cmp-image__image@tablet"
             itemprop="contentUrl"
             sizes="${image.sizes}"
             width="${image.width}" height="${image.height}"
             alt="${image.alt || true}" title="${image.displayPopupTitle && image.title}"/>
    </a>
    <span class="cmp-image__title" itemprop="caption" data-sly-test="${!image.displayPopupTitle && image.title}">${image.title}</span>
    <meta itemprop="caption" content="${image.title}" data-sly-test="${image.displayPopupTitle && image.title}">
</div>
<sly data-sly-call="${templates.placeholder @ isEmpty = !image.src, classAppend = 'cmp-image cq-dd-image'}"></sly>
sizes int the image
The image is from the wknd site

Best Practices for Committing and Saving Nodes Using AEM Resource Resolver

Problem Statement:

When should you commit or save nodes using resource resolver, and is it advisable to save nodes inside a loop?

Requirement:

This article discusses best practices for committing and saving nodes using AEM (Adobe Experience Manager) Resource Resolver. It highlights the importance of proper synchronization when working with resources and the different methods that can be used to manage them. The article also provides examples of saving nodes inside and outside for loops and explains how to avoid updating nodes unnecessarily by validating property existence and values.

Save 50 nodes with some properties

Introduction:

The ResourceResolver defines the API which may be used to resolve Resource objects and work with such resources as creating, editing or updating them. The resource resolver is available to the request processing servlet through the SlingHttpServletRequest.getResourceResolver() method. A resource resolver can also be created through the ResourceResolverFactory service.

A ResourceResolver is generally not thread safe! As a consequence, an application that uses the resolver, its returned resources, and/or objects resulting from adapting either the resolver or a resource, must provide proper synchronization to ensure no more than one thread concurrently operate against a single resolver, resource, or resulting objects.

An algorithm is used to resolve and getResource and provide various methods to manage resources like:

OperationDescription
Create(Resource, String, Map)for creating a new resource.
Delete(Resource)to delete a resource.
Adaptable.adaptTo(Class)allows to adapt a resource to a ModifiableValueMap to update a resource.
Move(String, String)to move resources.
Copy(String, String)to copy resources.
Commit()commits all staged changes.
Revert()reverts all staged changes.

All changes are transient and require committing them at the end

Hence as per API documentation, it’s better to stage all the changes before calling commit or revert.

But please make sure we are not trying to save millions of nodes at a time and also updating nodes takes more time compared to creating a new one as per the adapto conference showcase.

Hence check whether the node already has the property and value before you save it.

Resolution:

Saving resolver inside for loop

For our use case, I am using ResourceUtil.getOrCreateResource() for creating or getting the exiting node, and if it creating then it will be saving the node with default properties like jcr:primaryType = un:unstructured

Using ResourceUtil increases code readability and maintainability

Parameters:
resolver – The resource resolver to use for the creation
path – The full path to be created
resourceProperties – The optional resource properties of the final resource to create
intermediateResourceType – THe optional resource type of all intermediate resources
autoCommit – If set to true, a commit is performed after each resource creation.

In the below example, I am creating a for loop, I am creating the node (resource) with default properties, and I am setting auto-commit as true. After creating the resource, I am adapting it to ModifiableValueMap and I will be adding a new property name and value as “property+index” and committing the resolver.

public void saveNodes() {
  try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(
    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))) {
    for (int index = 0; index <= 50; index++) {
      @NotNull
      Resource savedResource = ResourceUtil.getOrCreateResource(resourceResolver, "/content/" + index,
        defualtNodeProperties, StringUtils.EMPTY, true);
      ModifiableValueMap map = savedResource.adaptTo(ModifiableValueMap.class);
      map.put("name", "property" + index);
      resourceResolver.commit();
    }
  } catch (LoginException | PersistenceException e) {
    LOGGER.error("Error Occured during Login", e.getMessage());
  }
}

Saving resolver outside for loop

In the below example, I have to remove auto save as false and the rest of the code remains the same, but I am committing resolver outside for loop. By doing so, I can stage the resource resolver and commit it at last.

public void saveNodes() {
  try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(
    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))) {
    for (int index = 0; index <= 50; index++) {
      @NotNull
      Resource savedResource = ResourceUtil.getOrCreateResource(resourceResolver, "/content/" + index,
        defualtNodeProperties, StringUtils.EMPTY, false);
      ModifiableValueMap map = savedResource.adaptTo(ModifiableValueMap.class);
      map.put("name", "property" + index);
    }
    resourceResolver.commit();
  } catch (LoginException | PersistenceException e) {
    LOGGER.error("Error Occured during Login", e.getMessage());
  }
}

What will happen if I rerun the same code?

ResourceUtil would handle getting the existing resource instead of recreating it, but I would still be updating the resource and committing the changes, which is a costly process.

Better implementation with Validation

In order to avoid the updating of the node we could validate that the property exists and check the value and if and only if the resolver has changed, will commit.

public void saveNodes() {
  try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(
    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))) {
    for (int index = 0; index <= 50; index++) {
      @NotNull
      Resource savedResource = ResourceUtil.getOrCreateResource(resourceResolver, "/content/" + index,
        defualtNodeProperties, StringUtils.EMPTY, false);
      ModifiableValueMap map = savedResource.adaptTo(ModifiableValueMap.class);
      if (!map.containsKey("name") || !StringUtils.equals(map.get("name", StringUtils.EMPTY), "property" + index)) {
        map.put("name", index);
      }
    }
    if (resourceResolver.hasChanges()) {
      resourceResolver.commit();
    }
  } catch (LoginException | PersistenceException e) {
    LOGGER.error("Error Occured during Login", e.getMessage());
  }
}

Best Practices for Creating Custom Workflow Processes in Adobe Experience Manager (AEM)

Problem statement:

What is the recommended way to create a custom workflow process in AEM?

Requirement:

This article discusses the recommended approach to creating custom workflow processes in Adobe Experience Manager (AEM) for implementing business logic. The author outlines the steps for creating a custom workflow process, including creating a component service with WorkflowProcessclass and providing a process. label, and override the execute method. The article includes a code example to illustrate the process.

Create the custom Workflow Process to do some business logic

Introduction:

WorkflowProcess is the interface to be used for automatic workflow steps implemented in Java. Classes implementing this interface define Java based processes that can be attached to a WorkflowNode and executed by the workflow engine.

Create Component Service with WorkflowProcessclass and implement WorkflowProcess class

Provide process.label for all the custom workflow process

Override execute method

package com.mysite.core.workflows;

import org.apache.sling.api.resource.ResourceResolver;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.adobe.granite.workflow.WorkflowException;
import com.adobe.granite.workflow.WorkflowSession;
import com.adobe.granite.workflow.exec.WorkItem;
import com.adobe.granite.workflow.exec.WorkflowProcess;
import com.adobe.granite.workflow.metadata.MetaDataMap;

/**
 * This process tracks back the properties which are changed in the generated
 * deck and updates its respective properties in all the assets.
 */
@Component(service = WorkflowProcess.class, property = { "process.label=Dynamic Deck Dynamo Write Back Process" })
public class CustomWorkflowProcess implements WorkflowProcess {
	private static final Logger LOGGER = LoggerFactory.getLogger(CustomWorkflowProcess.class);

	@Override
	public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaDataMap)
			throws WorkflowException {
		ResourceResolver resourceResolver;
		resourceResolver = workflowSession.adaptTo(ResourceResolver.class);
	}
}

References:

https://github.com/Adobe-Consulting-Services/acs-aem-commons/blob/master/bundle/src/main/java/com/adobe/acs/commons/indesign/dynamicdeckdynamo/workflow/processes/impl/DynamicDeckBackTrackProcess.java

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");
    }
}