AEM Core Component Delegation

Problem Statement:

What is a delegate? How to delegate OOTB components? Can I add new parameters or methods using delegation?


Requirement:

Delegate OOTB image component and add custom logic on existing methods like getWidth(), getHeight() and getSrcSet() and also add new method 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 Lombok delegation happens:

Lombok request handling and delegation
Lombok request delegation

Whenever we make 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 implements 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 Image component will remain 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
Image is from wknd site

2 thoughts on “AEM Core Component Delegation

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s