Dynamic Selection of one from many Implementations of an AEM service


In AEM, the Multiple Service Implementation feature allows the establishment of multiple implementations for a specific Service. This enables the execution of a particular implementation based on the given use-case.

A similar example can be seen in Replication Agents within AEM. Here, a specific replication implementation such as Static Agent, Default Agent, Custom Transport Handler/Builders, etc., is invoked depending on the configuration.

To filter and select one implementation from multiple options, there are two approaches available:

  1. Using the @Reference annotation: In this approach, a developer is aware of a target service implementation that can be uniquely identified by an attribute. More details about this approach can be found in the provided link.
  2. Utilizing a method implemented by all service implementations: In this method, implementations are evaluated dynamically to determine if they are suitable for the current business use-case. The current blog covers this scenario.

Let’s now construct a multiple service implementation example to illustrate this concept.

Scenario used in the Examples:

In the context of an online course website, when a lecturer submits a course package consisting of HTML pages, videos, and assessment questions, each content type needs to be processed using a specific import-service implementation:

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

In the following sections, we will create multiple implementations to handle each content type accordingly.

Lets get started with the implementation !!!

Implementation details

The implementation can be divided into 3 simple steps:

  1. Create an interface that each Service implementation would adhere to.
  2. Implement concrete service implementations
  3. Identify the mechanism to uniquely identify an implementation
  4. Develop another service that would execute the relevant implementation.

Step 1: Interface which each service implementation would implement

The interface would comprise of abstract methods which execute business logic for each service implementation

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 [optional]
    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.

For instance, the following three Java files represent different implementations of the CourseImport service, each with its own purpose for importing a specific content type:

  • PageImport is solely dedicated to import pages.
  • AssessmentImport is solely dedicated to import assessments.
  • VideoImport is solely dedicated to import videos.

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

    // This handler method is optional. Details covered in Step-3 
    @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());
    }

    // This handler method is optional. Details covered in Step-3 
    @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");
    }

    // This handler method is optional. Details covered in Step-3                @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: Uniquely identifying a service implementation

In each of the service implementations, we have created a method canProcess(). This method can be used to identify and fetch a specific implementation of the CourseImport Service. 

Step 4: Develop another service to trigger execution of relevant implementation.

In order to retrieve the appropriate implementation, we will obtain objects of all the implementations and select the one that is required.

Step 3.1: Aggregate references for all implementations .

It is achieved by using @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);
     }

//Browsing through available implementations, to call VideoImport to handle import of .mp4
@Override
     public void importContent() {
        for (CourseImport courseImportImpl : courseImportImplList) {
            if (courseImportImpl.canProcess("introduction.mp4")) {
                courseImportImpl.importContent();
                break;
            }
        }
    }
}

One thought on “Dynamic Selection of one from many Implementations of an AEM service

Leave a comment