How to refresh OSGI R6 components on bundle activation | Perficient Digital

How to refresh OSGI R6 components on bundle activation

Recently, I had the need to wrap the default implementation for AEM’s mail service in a new service called CustomMailService where I wanted to transform certain emails and let other emails be handled by the default service.

My main interest was for the AccountManagementService to bind my implementation of the MailService, instead the default implementation, you can achieve this by adding the service.ranking property and giving it an integer higher than the default implementation:

import com.day.cq.mailer.MailService;
import com.day.cq.mailer.MailingException;
import com.day.cq.mailer.MessageGateway;
import org.apache.commons.mail.Email;
import org.apache.commons.mail.EmailException;
import org.osgi.framework.Constants;
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;

@Component(
    property = {
        "label=Custom Mail Service",
        "description=CustomMail Service",
        Constants.SERVICE_RANKING + ":Integer=1000"},
    service = MailService.class, immediate = true)

public class CustomMailService implements MessageGateway<Email>, MailService {

 
  @Reference(target = "(service.pid=com.day.cq.mailer.DefaultMailService)",
      cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC)
  private volatile MailService defaultMailService;


  @Override
  public boolean handles(Class<? extends Email> aClass) {
    return true;
  }

  @Override
  public void send(Email email) throws MailingException {
    // transform email based on some conditions
    defaultMailService.send(email);
    
  }

  @Override
  public void sendEmail(Email email) throws EmailException {
    defaultMailService.send(email);
  }
}

 

But I hit a problem

The problem is, the AccountManagementService refuses to bind my CustomMailService unless I refreshed AccountManagementService. The reason is that the AccountManagementService has a static reference to MailService which means that adding a new MailService in OSGI would not cause AccountManagementService to look for a “better” service (higher ranking). See the OSGI spec for more details

 

I realize this is version 7 of the OSGI spec, but the same information is true for OSGI 6, you could download version 6 of the spec here

Now what?

Well, we could use a Bundle Activator and write some code to stop, then start the AccountManagementService. And do that we will!

First, we write a general util class to enable/disable components by providing the service PID, the bundle context and the ServiceComponentRuntime service.

 

import java.util.Optional;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.runtime.ServiceComponentRuntime;
import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO;
import org.osgi.util.promise.Deferred;
import org.osgi.util.promise.Promise;

public class OSGIComponentUtil {


  public static ComponentDescriptionDTO getDTO(BundleContext bundleContext, ServiceComponentRuntime scr, String componentName){
    for (Bundle bundle : bundleContext.getBundles()) {
      ComponentDescriptionDTO dto = scr.getComponentDescriptionDTO(bundle, componentName);
      if (dto != null) {
        return dto;
      }
    }
    return null;
  }

  public static Promise<Void> enable(ComponentDescriptionDTO dto, ServiceComponentRuntime scr){
    return Optional.ofNullable(dto)
        .map(scr::enableComponent)
        .orElse(getFailedPromise());
  }

  public static Promise<Void> disable(ComponentDescriptionDTO dto, ServiceComponentRuntime scr) {
    return Optional.ofNullable(dto)
        .map(scr::disableComponent)
        .orElse(getFailedPromise());
  }

  public static Promise<Void> getFailedPromise(){
    Deferred deferred = new Deferred<Void>();
    deferred.fail( new Exception());
    return deferred.getPromise();
  }
}

Now let’s write the bundle activator:

import com.adobe.cq.account.api.AccountManagementService;
import com.company.core.utils.OSGIComponentUtil;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.runtime.ServiceComponentRuntime;
import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO;

public class CustomBundleActivator implements BundleActivator {

  @Override
  public void start(BundleContext bundleContext) throws Exception {
    refreshAccountManagementService(bundleContext);
  }

  @Override
  public void stop(BundleContext bundleContext) throws Exception {
    refreshAccountManagementService(bundleContext);
  }

  private void refreshAccountManagementService(BundleContext bundleContext){
    ServiceComponentRuntime scr = getServiceComponentRuntime(bundleContext);
    ComponentDescriptionDTO dto = getAccountManagementServiceDTO(bundleContext, scr);
    OSGIComponentUtil.disable(dto, scr)
        .then(p -> OSGIComponentUtil.enable(dto, scr));
  }

  private ServiceComponentRuntime getServiceComponentRuntime(BundleContext bundleContext){
    ServiceReference reference = bundleContext.getServiceReference(ServiceComponentRuntime.class.getName());
    return  (ServiceComponentRuntime) bundleContext.getService(reference);
  }

  private ComponentDescriptionDTO getAccountManagementServiceDTO(BundleContext bundleContext, ServiceComponentRuntime scr){
    return OSGIComponentUtil.getDTO(bundleContext, scr, AccountManagementService.class.getName());
  }


}

and then providing the bundle activator class in the configuration for the maven-bundle-plugin:

 <plugin>
     <groupId>org.apache.felix</groupId>
     <artifactId>maven-bundle-plugin</artifactId>
     <configuration>
         <instructions>
             ...
             <Bundle-Activator>com.company.core.activator.CustomBundleActivator</Bundle-Activator>
         </instructions>
     </configuration>
</plugin>

and voila! the AccountManagementService is now refreshed every time this bundle is deployed! You could also write some code to check if the custom mail service really got bond to the AccountManagementService.

 

This implementation was really helpful with the code above: ComponentDisablerDriverDS13.java

Leave a Reply