AEM Get linked Content fragments content

Problem Statement:

We have the following content fragment as part of the AEM

  1. Car details
  2. Agent details

And each car can have multiple agents or agents will be selling multiple cars. How can we link between Cars and Agents CF? and how can we get the linked content onto the page?

Requirement:

Link the Car and Agent CF to maintain the relationship between the content fragments and the same can be pulled into the page and exported.

Introduction:

Content Fragments (CF) allow us to manage page-independent content. They help us prepare content for use in multiple locations/over multiple channels. These are text-based editorial content that may include some structured data elements that are considered pure content without design or layout information. Content Fragments are intended to be used and reused across channels.

Usage

  • Highly structured data-entry/form-based content
  • Long-form editorial content (multi-line elements)
  • Content managed outside the life cycle of the channels delivering it

Create the Car and Agents content fragment models as shown below:

Agent Content fragment
Car Content Fragment

Create a custom component called has linkedcontentfragment as shown below:

Based on the element name condition call the LinkedContentFragment Sling model and also pass the elements to be rendered (based on element names, element data will be pulled).

<template data-sly-template.element="${@ element='The content fragment element'}">
    <div class="cmp-contentfragment__element cmp-contentfragment__element--${element.name}" data-cmp-contentfragment-element-type="${element.dataType}">
        <dd class="cmp-contentfragment__element-value">
            <sly data-sly-test="${element.dataType == 'calendar'}" data-sly-use.tpl="core/wcm/components/contentfragment/v1/contentfragment/calendar.html"
                 data-sly-call="${tpl.element @ date = element.value}"></sly>
            <sly data-sly-test="${element.dataType == 'boolean'}">${element.value ? "true" : "false"}</sly>
            <sly data-sly-test="${element.dataType != 'calendar' && element.dataType != 'boolean' && element.name != 'agentDetails'}">${(element.value) @join='<br/>', context='html'}</sly>
            <sly data-sly-test="${element.name == 'agentDetails'}"
                 data-sly-use.linkedFragment="${'com.mysite.core.models.LinkedContentFragment' @elementValue=element, elements='agentTitle,agentDescription,agentAddress'}">
                data-sly-list.agentData="${linkedFragment.agentsList}">
                <dl data-cmp-data-layer="${agentData.data.json}"
                    data-sly-list.element="${agentData.elements}"
                    data-sly-use.elementTemplate="mysite/components/linkedcontentfragment/element.html">
                    <sly data-sly-call="${elementTemplate.element @ element=element}">
                    </sly>
                </dl>
            </sly>
        </dd>
    </div>
</template>

Create Sling model interface LinkedContentFragment as shown below:

package com.mysite.core.models;

import com.adobe.cq.wcm.core.components.models.Component;
import com.adobe.cq.wcm.core.components.models.contentfragment.ContentFragment;
import java.util.List;

/**
 * Defines the {@code Agent CF Model} Sling Model for the {@code /apps/mysite/components/linkedcontentfragment} component.
 */
public interface LinkedContentFragment extends Component {
    /**
     * Returns the Agents List
     *
     * @return the Content Fragment
     */
    default List<ContentFragment> getAgentsList() {
        throw new UnsupportedOperationException();
    }
}

Create model implementation class called LinkedContentFragmentImpl as shown below, get the element data (String array of paths) and elements to be pulled create a synthetic resource and adapt to core component Content fragment model to pull the element details as well as datalayer (tracking purposes)

package com.mysite.core.models.impl;

import com.adobe.cq.wcm.core.components.models.contentfragment.ContentFragment;
import com.adobe.cq.wcm.core.components.models.contentfragment.DAMContentFragment.DAMContentElement;
import com.adobe.cq.wcm.core.components.util.AbstractComponentImpl;
import com.adobe.granite.ui.components.ds.ValueMapResource;
import com.mysite.core.models.LinkedContentFragment;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.ValueMapDecorator;
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.RequestAttribute;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.factory.ModelFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {LinkedContentFragment.class}, resourceType = LinkedContentFragmentImpl.RESOURCE_TYPE)
public class LinkedContentFragmentImpl extends AbstractComponentImpl implements LinkedContentFragment {

    public static final String RESOURCE_TYPE = "mysite/components/linkedcontentfragment";
    private static final String CF_DISPLAY_MODE = "displayMode";
    private static final String CF_ELEMENTS = "elementNames";
    private static final String CF_FRAGMENT_PATH = "fragmentPath";

    @RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
    private DAMContentElement elementValue;

    @RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
    private String elements;

    @SlingObject
    private ResourceResolver resourceResolver;

    @OSGiService
    private ModelFactory modelFactory;

    @Override
    public List<ContentFragment> getAgentsList() {
        String[] elementList = elements.split(",");
        String[] agentPaths = elementValue.isMultiValue() ? (String[]) elementValue.getValue() : new String[] { (String) elementValue.getValue() };
        List<ContentFragment> elementsFragmentList = new ArrayList<>();
        Arrays.stream(agentPaths).forEach(agentPath -> {
            elementsFragmentList.add(modelFactory.getModelFromWrappedRequest(request, createLinkedSyntheticResource(agentPath, elementList), ContentFragment.class));
        });
        return elementsFragmentList;
    }

    private ValueMapResource createLinkedSyntheticResource(String path, String ... elementList) {
        ValueMap properties = new ValueMapDecorator(new HashMap<>());
        properties.put(CF_ELEMENTS, elementList);
        properties.put(CF_DISPLAY_MODE, "multi");
        properties.put(CF_FRAGMENT_PATH, path);
        return new ValueMapResource(resourceResolver, resource.getPath(), RESOURCE_TYPE, properties);
    }
}

Once we author the car model, we will be pulling the linked agent details as well as shown below:

Component Authoring
Content Fragment rendering

You can also refer to the below link to download the working code

https://github.com/kiransg89/LinkedContentFragment

AEM with Lombok

Problem Statement:

How to avoid boilerplate code?

Requirement:

Avoid boilerplate code like getters and setters and make sling models look 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 – create get method and @Setter creates set method

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

following class increases 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 is 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 slingscript 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, includeFieldNames 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

AEM Sling model – Best practices

Problem Statement:

What is the best way to use Sling model?

Requirements:

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

Introduction:

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

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

Create a Sling model interface

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

Create Interface Class – HelloWorldModel

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

package com.mysite.core.models;

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

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

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

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

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

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

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

    @SlingObject
    private Resource currentResource;

    @SlingObject
    private ResourceResolver resourceResolver;

    private String message;

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

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

    public String getMessage() {
        return message;
    }
}

Implementation class will implement and adapters to interface class HellWorldModel

Please make sure

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

Project structure

AEM Core Image component:

References

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

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