Category: AEM OSGi

Apache Felix – Filtering service references

Via an earlier blog Apache Felix – Multiple implementation of Service, we had dicussed:

  1. How to create multiple implementation of a Service
  2. Get references of the registered service implementations as a List.

Here we would be discussing, on how to filter the references and get ONLY pre-defined specific implementation

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.

The lecturer had his/her course live for an year & is appreciated by many curious students. The lecturer now wishes to update the assessment questionnaire, to challenge students to dig deeper.

He/She now submit the latest questionnaire to the onlint course portal for updates.

At this point, the course website needs to assure, that only the “Assessment Import” service implementation is called amogst all available Import implementation (Page, Videos and Assessment)

Implementation details:

Specific service references can be fetched by utilizing the “target” attribute of @Reference annotation. It would comprise of 2 simple steps

Step 1: Add property to identify each implementation

You can add any custom property to uniquely identify the service. For example, in the below sample code, I have added “type” property to uniquely identify my each implementation of CourseImport Service

Page Import Implementation
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import blog.techrevel.service.CourseImport;

@Component(name = "Import HTML Pages")
@Service
@Properties({ @Property(name = "type", value = "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");
    }
}
Video Import Implementation
@Component(name="Import course videos")
@Service
@Properties({ @Property(name = "type", value = "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");
    }
}
Assessment Import Implementation
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import blog.techrevel.service.CourseImport;

@Component(name="Import assessment questions")
@Service
@Properties({ @Property(name = "type", value = "assessment")})
public class AssessmentImport implements CourseImport{

    private static final Logger LOGGER = LoggerFactory.getLogger(AssessmentImport.class);

    @Override
    public void importContent() {
        LOGGER.info("Business Logic to import assessment questions");
    }

    @Override
    public boolean canProcess(String fileName) {
       return StringUtils.equals(fileName, "assessment.xls");
    }
}

 

Step 2: Use target attribute of @Refernce annotation to filter implementation

The target attribute filters services based on a property available in their ComponentContext

Following sample code, filters only PageImport service by matching ‘page’ value against type property of each implementation.

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Modified;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import blog.techrevel.service.CourseImport;
import blog.techrevel.service.CourseImportHandler;

@Component(immediate = true)
@Service
public class AssessmentUpdateHandler implements CourseImportHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(CourseImportHandler.class);

    @Reference(target="(type=page)")
    private CourseImport courseImport;

    @Override
    public void importContent() {
        courseImport.importContent();
    }
}

 

Using patterns for reference filtering

The target attribute of @Reference annotation also accepts regex (ldap) patterns. For example:

@Reference(target="(type=*age)")
@Reference(target="(|(type=p*)(type=generic))")

 

 

Advertisements

Default component configurations via Conf Nodes

Have you faced issue of design dialog configurations being overwritten by deployment?

Starting AEM 6.1, AEM provides ability to avoid design configuration overwrite, by extracting configurations from /etc to /conf.

Example:

Lets consider a scenario where business had requested a default title ‘Contact Us’ for a component. Few months later, they change the title to ‘Contact with us’ via Design Dialog.

Your development team is working on second version of the Contact Us component, to add a new field for Email ID with default value ‘contactus@adobe.com’

Once completed, the team deploys the second version of component with default value of email ID. Result would be “Design configuration ‘Contact with us’ configured by business, is restored to old value ‘Contact Us'”

Resolution through /conf to fix design configuration overwrite

  1. Use mode=”merge” in filter.xml for deploying design. This would ensure that on deployment existing nodes are not deleted. For more details refer to link
    • <filter root=”/etc/designs/<app_name>/jcr:content” mode=”merge”/>
  2. Configure default values in /conf: Sling Models can then be used to resolved values in required preference order. Example: Edit -> Design -> Default (from /conf path)

Steps to configure default values in /conf

Step 1: Define Configurations

In this step, we create a suitable configuration hierarchy considering rules described below. Also, add required default configurations in the jcr:content nodes.

Capture.PNG

In the above image, we can observe that configuration hierarchy should have:

  1. ‘sling:Folder’ structure below ‘/conf’ to define the site/app that the configurations belong to.
  2. ‘settings’ of type ‘sling:Folder’ which holds all the configurations.
  3. Configuration nodes below ‘setting’ of type ‘cq:Page’
  4. Configuration properties in ‘jcr:content’ of configuration nodes

 

Step 2: Add cq:conf property to the root of the project.

type: String

value: Site’s configuration path

Capture1.PNG

Step 3: Provide read permission to the target users & authors on:

  • Site configuration node under /conf
  • Root node of the site where cq:conf is configured.

 

Step 4: Use a Sling Model to derive the values in the required  order (edit, design, conf).

There are 2 APIs that could be utilized:

  1. com.adobe.granite.confmgr.Conf resolves the configuration from currentPage
  2. com.adobe.granite.confmgr.ConfMgr resolves the configuration from resource.

Fetch default configuration via com.adobe.granite.confmgr.Conf

import javax.annotation.PostConstruct;
import javax.inject.Inject;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.Model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.granite.confmgr.Conf;
import com.day.cq.wcm.api.NameConstants;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.designer.Style;

@Model(adaptables = SlingHttpServletRequest.class)
public class ContactUsModel {
private static final Logger LOG = LoggerFactory.getLogger(ContactUsModel.class);

   @Inject
   private Page currentPage;

   private ValueMap contactUsSettings;

   @Inject
   private Style currentStyle;

   @PostConstruct
   protected void init() {
       Conf conf = currentPage.adaptTo(Conf.class);
       if(conf==null){
           LOG.warn("Configurations not found for: " + currentPage.getPath());
       } else {
           String templatePath = currentPage.getProperties().get(NameConstants.NN_TEMPLATE, "");
           contactUsSettings = conf.getItem(templatePath+"/contactUsComponent");
       }
   }

   public String getDesignTitle() {
       /*
       * Return title in following preference order:
       * 1. Title from Design
       * 2. Title from conf
       */
       String designTitle = currentStyle.get("title", "");
       if(StringUtils.isEmpty(designTitle)){
           //Title not configured in Design. Fetching from /conf
           LOG.debug("Fetching default title from /conf");
           return contactUsSettings.get("contactUsTitle", "");
       } else {
           //Title configured in Design.
           LOG.debug("Title configured in design dialog: " + designTitle);
           return designTitle;
       }
    }
}

Fetch default configuration via com.adobe.granite.confmgr.ConfMgr

import javax.annotation.PostConstruct;
import javax.inject.Inject;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.Model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.granite.confmgr.Conf;
import com.adobe.granite.confmgr.ConfMgr;

@Model(adaptables = SlingHttpServletRequest.class)
public class ContactUsModel {
private static final Logger LOG = LoggerFactory.getLogger(ContactUsModel.class);

    @Inject
    private ConfMgr confMgr;

    @Inject
    private Resource currentResource;

    private ValueMap contactUsSettings;

    @PostConstruct
    protected void init() {
       final Conf conf = confMgr.getConf(currentResource);
       if(conf==null){
           LOG.warn("Configurations not found for: " + currentResource.getPath());
       } else {
           contactUsSettings = conf.getItem("/conf/techrevel/settings/apps/confEx/templates/someTemplate/contactUsComponent");
       }
    }

    public String getTitle() {
        LOG.debug("Fetching default title from /conf");
        return contactUsSettings.get("contactUsTitle", "");
    }
}

For more details, please refer to link

ResourceChangeListener v/s Sling Event

With introduction of ResourceChangeListener, this blog is an effort to help developers choose between ResourceChangeListener and Sling Event for an implementation.

ResourceChangeListener:

pros:

  • Can be configured to listen to only specific paths. Multiple watch paths can also be configured to provide it a more granular approach.
  • For a bulk operation concerning N resources, listener will be executed ONLY once. Thus, total number of persistence operations = 1 (via handler) + 1 (Bulk operation) = 2

cons:

Sling Event:

pros:

cons:

  • Cannot restrict events to a certain path. Sling Event listens to all nodes starting from the root node of a repository (/)
  • For a bulk operation concerning N resources, handler will be called N times. Thus, total number of persistence operations = N (via handler) + 1 (Bulk operation) = N + 1

AbstractResourceVisitor for traversing resource trees

We often come across features that require traversal of a resource tree (e.g. processing assets in a folder). In such scenarios, recommendation is to use tree traversal than queries. More details of similar scenarios can found on link 

Sling provides AbstractResourceVisitor API, which performs traversal through a resource tree, allowing a developer to focus on processing child resources.

How to use AbstractResourceVisitor?

Step 1: Create class that extends AbstractResourceVisitor

Step 2: Implement visit() to process child resources

class ProcessChildVisitor extends AbstractResourceVisitor {
    @Override
    protected void visit(Resource resource) {
        //Business logic to process the resource
    }
}
  • The visit method will be called for each child resource of ‘parentResource’ mentioned in Step 3

Step 3: Initiate processing of child resources by calling accept() of class created in Step 2

    ProcessChildVisitor processChildVisitor = new ProcessChildVisitor();
    processChildVisitor.accept(parentResource);

Code for AbstractResourceVisitor class can be found on link

Improving traversal for specific node types

There might be instances, where we need to traverse only specific type of child resources (e.g. Assets). To implement such scenarios:

Step 1: Create a class that extends AbstractResourceVisitor

Step 2: Override accept() Or traverseChildren() method.

  • accept() decides if the child resources of current resource should be iterated.
  • traverseChildren() iterates over child resources.

Example: Consider that we need to iterate over all Assets in a folder. As we iterate, we should NOT process:

  • sub-folders, but only the assets that they contain.
  • resources below asset (e.g renditions).

Below is the snippet to implement such a Resource Visitor :

import org.apache.sling.api.resource.AbstractResourceVisitor;
import org.apache.sling.api.resource.Resource;

import com.adobe.granite.asset.api.Asset;

/**
* The <code>AbstractAssetVisitor</code> helps in traversing a
* resource tree by decoupling the actual traversal code
* from application code. Concrete subclasses should implement
* the {@link AbstractAssetVisitor#visit(Resource)} method.
*
*/
public abstract class AbstractAssetVisitor extends AbstractResourceVisitor {

    /**
    * Visit the given resource and all its descendants.
    * @param res The resource
    */
    @Override
    public void accept(final Resource res) {
        if (res != null) {
            visit(res);

            if(res.adaptTo(Asset.class)==null){
                traverseChildren(res.listChildren());
            }
        }
    }
}

Sling-based ResourceChangeListener

With AEM 6.2, a new sling observation support has been provided. The Sling alternative, called “ResourceChangeListener” is only recommended for resource change events. Non-resource events (ex. Workflow events), should be handled via Sling Event Handlers.

For more parameters to help resolve the choice between listener and sling events, refer to link

Benefits of ResourceChangeListener:

  • Avoid long-lived sessions.
  • Avoid Oak observation queue size issues.

Sample Implementation:

@Component(immediate = true)
@Service(value = ResourceChangeListener.class)
@Properties(value = {
@Property(name = ResourceChangeListener.PATHS, value = { "/content/unhcr/intranet" }),
@Property(name = ResourceChangeListener.CHANGES, value = { "CHANGED", "ADDED"}, propertyPrivate=true)
})
public class PropertyMergeListener implements ResourceChangeListener {

    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    /**
    * Called when a resource is added/deleted/modified in Target path
    * @param resourceChangeList List of resource Changes identified by Listener
    */
    @Override
    public void onChange(List<ResourceChange> resourceChangeList) {
        Map<String, Object> param = new HashMap<String, Object>();
        param.put(ResourceResolverFactory.SUBSERVICE, "SubServiceName");
        ResourceResolver resourceResolver = null;
        try {
            resourceResolver = resourceResolverFactory.getServiceResourceResolver(param);

            for (ResourceChange resourceChange : resourceChangeList) {
                String resourcePath = resourceChange.getPath();
                Resource resource = resourceResolver.getResource(resourcePath);

                ...

 

Dynamic Path for ResourceChangeListener

As a authoring environment grows, the paths that a ResourceChangeListener should listen to, might change. They could either:

  • become more granular
  • become more generic
  • apply for only specific AEM instances

A developer can use OSGi configurations to adapt to such changes. This would assure that no code changes are required to deal with the path updates.

Step 1: Create a service that activates only when a configuration node is available in AEM Instance.

@Component(label = "Service Label", immediate = true, metatype = true, policy = ConfigurationPolicy.REQUIRE)
@Service
@Properties(value = { @Property(name = ResourceChangeListener.CHANGES, value = { "CHANGED", "ADDED",
"REMOVED" }, propertyPrivate = true) })
public class SampleServiceImpl implements SampleService, ResourceChangeListener {
....

Step 2: Declare a property  for ‘resource.paths’:

public class SampleServiceImpl implements SampleService, ResourceChangeListener {

 // Paths to watch for cache update
 @Property(label = "Paths to watch for cache update", value = { PageConstants.DUTY_STATION_FOLDER_PATH,
 AssetConstants.FLAG_IMAGE_FOLDER_PATH })
 private static final String PATHS_TO_WATCH = ResourceChangeListener.PATHS;
....

Step 3: Create an OSGi configuration for the service with property ‘resource.paths’

Step 4: Deploy your code. Verify that the Listener listens to the updated paths configured via:

  • OSGi Configuration node created in Step 3
  • Path updates via OSGi Web Console