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

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