Adobe Experience Manager Blog

AEM concepts, snippets and implementation

DS Annotations – dependency updates

0

With AEM 6.2, we can use the new Declarative Service annotations. These are improvements over Felix annotations. Adding the recommendation excerpt from Apache Felix website:

While the Apache Felix Maven SCR Plugin is a great tool (see below), for developing OSGi components using Declarative Services you should use the official annotations from the OSGi R6 specification. The development of the Apache Felix SCR Plugin is in maintenance mode.

The examples and dependencies have been verified on AEM 6.3 Instance

Maven dependency changes:

In order to use the new OSGi annotations, we need to add following dependencies to pom.xml.

 <dependency>
     <groupId>org.osgi</groupId>
     <artifactId>osgi.core</artifactId>
     <version>6.0.0</version>
     <scope>provided</scope>
 </dependency>
 <dependency>
     <groupId>org.osgi</groupId>
     <artifactId>osgi.cmpn</artifactId>
     <version>6.0.0</version>
     <scope>provided</scope>
 </dependency>
 <dependency>
     <groupId>org.osgi</groupId>
     <artifactId>osgi.annotation</artifactId>
     <version>6.0.1</version>
     <scope>provided</scope>
 </dependency>
 

Use latest version of mvn-bundle-plugin (>=3.2.0)

For a project upgrading from Felix to DS annotations, you can remove following:

  • All Felix dependencies. Example:
    • org.apache.felix.scr.annotations
    • biz.aQute.bnd
  • Plugins
    • maven-scr-plugin: is required to resolve felix annotations at build time

Code changes:

You can choose to update java files at once, or one-by-one.

  • Incase you wish to modify all files together, then remove felix dependencies and plugins. The IDE would now recognize all the files that need modifcation.
  • However, if you wish to change files one-by-one, then you can keep both DS and Felix dependencies in pom.xml. A bundle with both types of annotations would still be good. Once all the code changes are done, you should remove all felix dependencies.

How to identify the changes:

You can easily identify the files, by looking for package imports of “org.apache.felix.scr.annotations.*”.

We would be using the following packages instead:

  • org.osgi.service.component.annotations.*
  • org.osgi.service.metatype.annotations.*

For more details on code changes involved, please visit the specific links:

Verify annotation resolution

To verify if the DS annotation is generated:

  • decompile the jar created after mvn clean install
  • Check for the Service description available below /OSGI-INF/ folder

service-description.PNG

 

Advertisements

DS Annotations – Component, property and configurations

1

@Component Annotation

An component is a piece of code that is managed by OSGi container. The container would be responsible for its instantiation and management.

A component is activated only after all its service dependencies are satisfied by the container.

Attributes of a component: 

component-attributes.PNG

The above table have been noted from: https://www.knopflerfish.org/releases/5.2.0/docs/javadoc/org/osgi/service/component/annotations/Component.html

  • configurationPolicy: The attribute can hold following values of ConfigurationPolicy
    • IGNORE: Always allow the component configuration to be satisfied and do not use the corresponding Configuration object even if it is present.
    • OPTIONAL: Use the corresponding Configuration object if present but allow the component to be satisfied even if the corresponding Configuration object is not present.
    • REQUIRE: There must be a corresponding Configuration object for the component configuration to become satisfied.
  • factory: used to create a configuration factory. More implementation details are available on link.
  • name: The attribute doesn’t support special characters. If invalid, the component will not be registered.
  • property: A replacement for “@Properties felix annotation used at Class-level”.
  • service: registers component as a Service. More implementation details available on link.

Example:

To create a component, add @Component annotation to a class. Also, configure its attributes as per your need.

In the following example, we have:

  • @Activate annotation is used to mark a function which would be called when the component activates. The function can have any name.
  • @Deactivate annotation is used to mark a function which would be called when the component deactivates. The function can have any name.
  • Declared a custom property using property attribute.
    • The property value is read from componentContext in @Activate/@Deactivate methods.

More on defining property:

One may also need to declare multiple properties of a component. In such scenarios, declare an array of values for property attribute:

To define multiple values of a property, create each value as a separate element of the property Array.

 

 

Creating configurable properties

The annotations described in this section will help you to create Components whose configurations can be edited via OSGi console. To achieve the same:

  1. Create a separate or an inner interface which would hold configurations. In example, we have created Config interface.
  2. Add @ObjectClassDefinition annotation to the interface. Also, add desired attributes
    • name: The name would help you search the configuration in OSGi’s configuration manager.
  3. Add @Designate to the Component that would consume the configurations.  The ocd attribute should refer to the Configuration interface created in Step-2.
  4. Declare properties that you would like to configure via @AttributeDefinition
    • Following image maps annotation attributes with the OSGi UI.osgi.PNG
    • Please note that there are 2 ways to define default values:
      • defaultValue attribute of @AttributeDefinition: The value is displayed to the user, when he/she tries to configure the interface via Configuration manager. OSGi will NOT pick this default value, if no Configuration exists. Thus, when you install a bundle, the output would appear as:default-values.PNG
      • Specifying default value in variable declarartion: The value is displayed to the user, when he/she tries to configure the interface via Configuration manager. OSGi will pick this default value, even if no Configuration exists.

Also, note that we no longer need PropertiesUtil to resolve OSGi configurations. 🙂

Notes:

Via Declarative Services, the number of annotations have been reduced. For example: @Component annotation is used for:

  • Component
  • Service
  • Servlet
  • Filter etc..

All of the above can be created by utilizing attributes of @Component details. More details are available on specific links

 

 

Toggle Field visibility based on dropdown’s value – Coral 3

0

AEM’s dropdown comes with a simple and useful feature, to toggle visibility of other Dialog fields based on its selection.

An OOTB implementation is available for component “/apps/core/wcm/components/list/v1/list

Let’s quickly check how to configure it (verified on AEM 6.3 with Coral-3 dropdown):

Step 1: Register dropdown that is supposed to show/hide other dialog fields.

Achieved by adding “granite:class” as “cq-dialog-dropdown-showhide”

Show-Hide drodown.PNG

 

Step 2: Identify the target dialog fields

This is achieved in 2 small steps:

  1. Informing dropdown about the class that all target elements would have, i.e :
    • Add a new child node “granite:data” to the dropdown
    • Add property “cq-dialog-dropdown-showhide-target” as “.<some_class_name>” showhidetarget.PNG
  2. Adding the class to all target dropdown fields, i.e:
    • Add “granite:class” as “hide <some_class_name>” to all target dropdown fields.target-granite-class.PNG

 

Step 3: Define dropdown’s value for which to show the target field

This is achieved in 2 small steps:

  1. Add ‘value’ property to all options of the trigger dropdowndropdown-value.PNG
  2. Declare dropdown value for which to display the target dialog field
    • Add a new child node “granite:data” to the dialog field
    • Add property “showhidetargetvalue” as “<required_dropdown_option_value>” 

target-show-value.PNG

 

 

AEM Template Editor – Design configuration via policies

0

In template editors, policies are used to configure component design. Example: component’s design configurations,  allowed components for a container, mapping asset into components etc.

Configuring a template-editor’s policy is similar to a Static template’s design dialog. Following are the steps to define and access a new policy:

Step 1: Create policy configuration dialog

A component’s policy dialog is defined by adding a cq:design_dialog to the component. Example:

Capture1.PNG

Sample .content.xml for design dialog:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="My Title Component"
sling:resourceType="cq/gui/components/authoring/dialog">
    <content jcr:primaryType="nt:unstructured"
       sling:resourceType="granite/ui/components/coral/foundation/container">
        <items jcr:primaryType="nt:unstructured">
            <tabs jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/tabs"
                maximized="{Boolean}true">
                <items jcr:primaryType="nt:unstructured">
                    <properties
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Available Stage types"
                        sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
                        margin="{Boolean}true">
                        <items jcr:primaryType="nt:unstructured">
                            <content
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/container"
                                margin="{Boolean}false">
                                <items jcr:primaryType="nt:unstructured">
                                    <title
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                        fieldLabel="Title"
                                        name="./title"/>
                                </items>
                            </content>
                        </items>
                   </properties>
               </items>
            </tabs>
        </items>
    </content>
</jcr:root>

Step 2: Configuring policy

Once the design dialog is created:

  • Open the template in Structure mode.
  • Click on the component
  • Following button should now be available to configure the design properties.

Sprints.PNG

Policy Storage and sharing:

  • Policies are stored in following location by default:

/conf/project_name/settings/wcm/policies/component_name/policy_randomNumber

Policies can be also be stored in /apps or /libs folder. In this case the resource will be resolved in following preference order: /conf, /apps, /libs.

  • Please observe in the policy path, that policies are stored centrally for a project. This enables authors to share a design policy among multiple templates.
  • Template -> policy mapping is done by referring policy via:

cq:policy=/conf/project_name/settings/wcm/policies/component_name/policy_randomNumber

Capture.PNG

       At location:

    /conf/project_name/settings/wcm/templates/template-name/policies/jcr:content/root/component_name

Step 3: Accessing policy

A component’s policy configuration can be accessed by ContentPolicyManager. Sharing an example below:

import javax.annotation.PostConstruct;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import com.day.cq.wcm.api.policies.ContentPolicy;
import com.day.cq.wcm.api.policies.ContentPolicyManager;

@Model(adaptables = Resource.class)
public class StageModel {
    @SlingObject
    private ResourceResolver resourceResolver;

    /** The resource. */
    @Self
    protected Resource resource;

    private String title;

    @PostConstruct
    protected void constructStageType(){
        ContentPolicyManager policyManager = resourceResolver.adaptTo(ContentPolicyManager.class);
        if (policyManager != null) {
            ContentPolicy contentPolicy = policyManager.getPolicy(resource);
            if (contentPolicy != null) {
                title= (String) contentPolicy.getProperties().get("title");
            }
        }
    }

    /**
    * @return title
    */
    public String getTitle() {
        return title;
    }
}

DS Annotation – Multiple implementation of Service

0

Multiple Service Implementation can be used to define multiple implementations of a Service, and then execute all/specific implementation depending on the use-case.

A close example would be Replication Agents. Here a specific replication implementation (Static Agent, Default Agent, Custom Transport Handler/Builders etc) is called, depending on the configuration.

Lets construct a multiple service implementation via an Example.

Scenario:

Consider an online course website. A lecturer can publish his content by submitting a course bundle comprising of html pages, videos and assessment questions.

While the bundle is processed, each content type is to be dealt with specific import-service implementation:

  1. Page import
  2. Video import
  3. Assessment import

Lets get started with the implementation !!!

Create multiple service implementation

The implementation can be divided into 3 simple steps:

  1. Create an interface which each of Service implementation would implement.
  2. Implement concrete service implementations
  3. A handler which would hold list of service implementation & execute relevent implementation.

Step 1: Interface which each service implementation would implement

The interface would comprise of abstract methods which:

  • would execute business logic for each service.
  • allow identification of best service to process current use-case.

Sample interface for the course import scenario:

package blog.techrevel.service;
public interface CourseImport {
    //Import pages, videos and assessment
    public abstract void importContent();

    //Identify the service which would be able to process current file
    public abstract boolean canProcess(String fileName);
}

Step 2: Implement concrete service implementations

Here we would be create multiple content import implementation to deal specifically with each content type. Example:

PageImport.java

package blog.techrevel.service.impl;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import blog.techrevel.service.CourseImport;
@Component(name="pages", service = CourseImport.class, property={"type=page"})
public class PageImport implements CourseImport {
    private static final Logger LOGGER = LoggerFactory.getLogger(PageImport.class);
    @Override
    public void importContent() {
        LOGGER.info("Business Logic to import HTML Pages");
    }

    @Override
    public boolean canProcess(String fileName) {
        return StringUtils.endsWith(fileName, ".html");
    }

    @Activate
    @Modified
    protected void activate(ComponentContext componentContext){
         LOGGER.info("Registering: " + componentContext.getProperties().get("type").toString());
    }
}

 AssessmentImport.java

package blog.techrevel.service.impl;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.slf4j.Logger;import org.slf4j.LoggerFactory;
import blog.techrevel.service.CourseImport;
@Component(name = "assessment", service = CourseImport.class, property = { "type=assessment" })
public class AssessmentImport implements CourseImport {
    private static final Logger LOGGER = LoggerFactory.getLogger(AssessmentImport.class);
    @Activate
    @Modified
    protected void activate(ComponentContext componentContext)
    {
        LOGGER.info("Registering: " + componentContext.getProperties().get("type").toString());
    }
@Override
    public void importContent() {
         LOGGER.info("Business Logic to import assessment questions");
    }
@Override
    public boolean canProcess(String fileName) {
       return StringUtils.equals(fileName, "assessment.xls");
    }
}

VideoImport.java

package blog.techrevel.service.impl;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.slf4j.Logger;import org.slf4j.LoggerFactory;
import blog.techrevel.service.CourseImport;
@Component(name = "videos", service = CourseImport.class, property = { "type=video" })
public class VideoImport implements CourseImport {
    private static final Logger LOGGER = LoggerFactory.getLogger(VideoImport.class);
    @Override
    public void importContent() {
         LOGGER.info("Business Logic to import course videos");
    }
@Override
    public boolean canProcess(String fileName) {
         return StringUtils.endsWith(fileName, ".mp4");
    }
@Activate
    @Modified
    protected void activate(ComponentContext componentContext) {
         LOGGER.info("Registering: " + componentContext.getProperties().get("type").toString());
    }
}

Step 3: Handler to reference service and trigger execution of appropriate implementation.

Step 3.1: To aggregate references for all implementations in a collection, use @Reference annotation with following attributes:

  • name=“The name of this reference.”
  • cardinality=”The cardinality of the service reference. This must be one of value from the enumeration ReferenceCardinality
    • AT_LEAST_ONE: The reference is mandatory and multiple.
    • MANDATORY: The reference is mandatory and unary.
    • MULTIPLE: The reference is optional and multiple.
    • OPTIONAL: The reference is optional and unary.
  • policy=”ReferencePolicy.DYNAMIC Or ReferencePolicy.STATIC”
    • If dynamic the service will be made available to the component as it comes and goes.
    • If static the component will be deactivated and re-activated if the service comes and/or goes away
  • unbind=”name of the method to be called when the service is to be unbound from the component”
    • To declare no unbind method, the value "-" must be used.
    • If not specified, the name of the unbind method is derived from the name of the annotated bind method. If the annotated method name begins with bindset or add, that is replaced withunbindunset or remove, respectively, to derive the unbind method name. Otherwise, un is prefixed to the annotated method name to derive the unbind method name. The unbind method is only set if the component type contains a method with the derived name.
private List courseImportImplList;
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void bind(CourseImport courseImport) {
    if (courseImportImplList == null) {
        courseImportImplList = new ArrayList();
    }
    courseImportImplList.add(courseImport);
}
protected void unbind(CourseImport courseImport) {
    courseImportImplList.remove(courseImport);
}

Step 3.2 Trigger execution of appropriate implementation

Use the ‘collection created in Step-3.1’ to shortlist the best implementation for current use-case. The shortlisted implementation reference can then be used to trigger corresponding business logic.

In the below sample code, canProcess() identifies the right implementation to deal with current content type. Corresponding importContent() is then executed to import html as per business requirement.

package blog.techrevel.service.impl;
import java.util.ArrayList;
import java.util.List;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;import org.slf4j.LoggerFactory;
import blog.techrevel.service.CourseImport;
import blog.techrevel.service.CourseImportHandler;
@Component(name = "courseImportHandler", immediate = true, service=CourseImportHandler.class)
public class CourseImportHandlerImpl implements CourseImportHandler {
     private static final Logger LOGGER = LoggerFactory.getLogger(CourseImportHandlerImpl.class);
     private List courseImportImplList;
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
     protected void bind(CourseImport courseImport) {
          if (courseImportImplList == null) {
              courseImportImplList = new ArrayList();
          }
          courseImportImplList.add(courseImport);
     }
protected void unbind(CourseImport courseImport) {
         courseImportImplList.remove(courseImport);
     }
@Override
     public void importContent() {
        for (CourseImport courseImportImpl : courseImportImplList) {
            if (courseImportImpl.canProcess("introduction.mp4")) {
                courseImportImpl.importContent();
                break;
            }
        }
    }
}

DS annotations – Configuration Factory

2

OSGi provides many modular features which enhances a developer’s implementation experience. Current blog focuses on one such feature: Configuration Factories via new OSGi Annotations

The concept is similar to logging configuration in AEM, where we define multiple configurations for different loggers, but the service implementation stays the same. Thus, multiple configuration, single implementation.

Create and access a configuration factory

It takes just two steps to create and use a Configuration factory.

Step 1: Create Configuration factory

This is achieved by adding ‘factory=true’ attribute to the Component annotation.

package blog.techrevel.service.impl;
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.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;import org.slf4j.LoggerFactory;

import blog.techrevel.service.ConfigurationFactory;

@Component(service = ConfigurationFactory.class)
@Designate(ocd = ConfigurationFactoryImpl.Config.class, factory=true)
public class ConfigurationFactoryImpl implements ConfigurationFactory
{
    private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationFactoryImpl.class);

    @ObjectClassDefinition(name = "Techrevel configuration factory")
    public @interface Config {
        @AttributeDefinition(name = "Content Consumer URL", defaultValue = "http://localhost:8081")
        String getContentConsumerUrl();
    }

    private String contentConsumerUrl;

    @Activate
    @Modified
    protected void activate(final Config config) {
        contentConsumerUrl = config.getContentConsumerUrl();
        LOGGER.info("Read the content Consumer Url : " + contentConsumerUrl);
    }

    @Override public String getContentConsumerUrl() {
     return contentConsumerUrl;
    }
}

Step 2: Consume factory’s configurations

This is the service which would act based on values configured via factory.

Step 2.1: To aggregate configurations values in a collection use @Reference annotation with following attributes:

  • name=“The name of this reference.”
  • cardinality=”The cardinality of the service reference. This must be one of value from the enumeration ReferenceCardinality
    • AT_LEAST_ONE: The reference is mandatory and multiple.
    • MANDATORY: The reference is mandatory and unary.
    • MULTIPLE: The reference is optional and multiple.
    • OPTIONAL: The reference is optional and unary.
  • policy=”ReferencePolicy.DYNAMIC Or ReferencePolicy.STATIC”
    • If dynamic the service will be made available to the component as it comes and goes.
    • If static the component will be deactivated and re-activated if the service comes and/or goes away
  • unbind=”name of the method to be called when the service is to be unbound from the component”
    • To declare no unbind method, the value "-" must be used.
    • If not specified, the name of the unbind method is derived from the name of the annotated bind method. If the annotated method name begins with bindset or add, that is replaced withunbindunset or remove, respectively, to derive the unbind method name. Otherwise, un is prefixed to the annotated method name to derive the unbind method name. The unbind method is only set if the component type contains a method with the derived name.
package blog.techrevel.service.impl;
import java.util.ArrayList;
import java.util.List;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;import org.slf4j.LoggerFactory;

import blog.techrevel.service.ConfigurationFactory;
import blog.techrevel.service.ConfigurationFactoryConsumer;
@Component(immediate = true, service = ConfigurationFactoryConsumer.class)
public class ConfigurationFactoryConsumerImpl implements ConfigurationFactoryConsumer {
    private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationFactoryConsumerImpl.class);
    private List configurationList;
/**
    * Executed on Configuration Add event
    * @param config New configuration for factory
    */
    @Reference(name = "configurationFactory", cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
    protected synchronized void bindConfigurationFactory(final ConfigurationFactory config) {
        LOGGER.info("bindConfigurationFactory: " + config.getContentConsumerUrl());
        if (configurationList == null) {
            configurationList = new ArrayList(); }
            configurationList.add(config);
        }
    /**
    * Executed on Configuration Remove event
    * @param config New configuration for factory
    */
    protected synchronized void unbindConfigurationFactory(final ConfigurationFactory config) {
        LOGGER.info("unbindConfigurationFactory: " + config.getContentConsumerUrl());
        configurationList.remove(config);
    }
}

Step 2.2: Consume the configuration collection

The configurationList collection created in Step 2.1 can be accessed through various functions of ConfigurationFactoryConsumer class.

Access the collection and execute the business logic as needed. Happy Coding !

AEM – Coral 2 multifield with acs-aem-commons JSON_STORE

0

In AEM component dialog, we often implement multifield which comprise of multiple widgets. Here we would cover  multifield which would store values in json format using:

STEP 1: Create a component.

STEP 2: Create component dialog with multifield comprising of:

  • textfield
  • pathbrowser

Add acs-commons-nested=”JSON_STORE” to the multifield

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"       jcr:primaryType="nt:unstructured"       jcr:title="Navigation"       sling:resourceType="cq/gui/components/authoring/dialog">
     <content jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container">
         <items jcr:primaryType="nt:unstructured">
             <tabs jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container">
                <layout jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/layouts/tabs" type="nav"/>
                    <items jcr:primaryType="nt:unstructured">
                        <navigationitems jcr:primaryType="nt:unstructured" jcr:title="Navigation Items" sling:resourceType="granite/ui/components/foundation/section">
                            <layout jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns" margin="{Boolean}false"/>
                                <items jcr:primaryType="nt:unstructured">
                                    <column jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container">
                                        <items jcr:primaryType="nt:unstructured">
                                            <navigationitems jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/multifield" class="full-width" fieldDescription="Click 'Add field' to add a new item" fieldLabel="Navigation Items">
                                                <field jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/fieldset" acs-commons-nested="JSON_STORE" name="./navigationItems">
                                                    <layout jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns" method="absolute"/>
                                                        <items jcr:primaryType="nt:unstructured">
                                                           <column jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container">
                                                               <items jcr:primaryType="nt:unstructured">
                                                                   <label jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/textfield" fieldLabel="Label" name="./linktext"/>
                                                                   			<link jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/pathbrowser" fieldLabel="Link" name="./linkURL" rootPath="/content"/>
                                                                </items>
                                                            </column>
                                                       </items>
                                            </field>
                                      </navigationitems>
                                 </items>
                             </column>
                         </items>
                     </navigationitems>
                 </items>
             </tabs>
         </items>
     </content>
</jcr:root>

STEP 3: Add dependencies to the pom.xml

Incase, the AEM doesn’t export javax.json.JsonObject API, then you would have to import the bundles in AEM.

<dependency>
    <groupId>org.apache.geronimo.specs</groupId>
    <artifactId>geronimo-json_1.0_spec</artifactId>
    <version>1.0-alpha-1</version>
</dependency>
<dependency>
    <groupId>org.apache.johnzon</groupId>
    <artifactId>johnzon-core</artifactId>
    <version>1.0.0</version>
</dependency>

STEP 4: Create Sling Model to access multifield values. The values returned as a List would be iterated in Component’s sightly to display links.

Bean to hold a link’s details:

/** * NavigationItem for Links. */
public class NavigationItem {
    private String linktext;
    private String linkURL;
    public NavigationItem(String linktext, String linkURL){
        this.linktext = linktext;
        this.linkURL = linkURL;
    }
    public String getLinktext() {
        return linktext;
    }
    public void setLinktext(String linktext) {
        this.linktext = linktext;
    }
    public String getLinkURL() {
        return linkURL;
    }
    public void setLinkURL(String linkURL) {
        this.linkURL = linkURL;
    }
}

Sling Model to gather details of all configured links:

package blog.techrevel.core.models;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Optional;

import blog.techrevel.NavigationItem;

/** * A model bean for the link component */
@Model(adaptables = Resource.class)
public class ProductNavigationModel {

   private static final String LINK_URL = "linkURL";
   private static final String LINK_TEXT = "linktext";

   @Inject
   @Optional
   private String[] navigationItems;
   private List<NavigationItem> list;

   @PostConstruct protected void init() {
       if (navigationItems != null) {
           this.list = new ArrayList<>();
           for (String linkJsonString : navigationItems) {
              try (StringReader stringReader = new StringReader(linkJsonString);
                  JsonReader jsonReader = Json.createReader(stringReader)) {
                  JsonObject linkJsonObject = jsonReader.readObject();
                  String linkPath = linkJsonObject.getString(LINK_URL);
                   list.add(new NavigationItem(linkJsonObject.getString(LINK_TEXT), linkPath));
              }
          }
       }
    }
    public List<NavigationItem> getList() {
        return list;
    }
}

STEP 5: Render links in Component’s sightly by accessing the Sling Model

<div data-sly-use.linkModel="blog.techrevel.core.models.ProductNavigationModel">
        <!--/*Links*/-->
        <sly data-sly-repeat.item="${linkModel.list}">
            <a href="${item.linkURL}"> ${item.linktext}</a>
        </sly>
        <sly data-sly-test="${!linkModel.list}">Navigation Component</sly></div>

Verified on AEM 6.3 with acs-aem-commons package version 3.9.0