Developing Jahia extensions in OSGi modules

  Written by The Jahia Team
 
Developers
   Estimated reading time:

Extension points

Multiple parts of Jahia can be extended by adding Java classes in modules. A module developer can for example add a new action that will add a new HTTP endpoint, add a render filter that will change the rendering, or modify the HTML cache key generation by adding CacheKeyPartGenerator.

In previous versions it was usually done by declaring Spring Beans. These beans were processed and registered into Jahia core. It is now possible to do the same thing by using OSGi services, without relying on any Spring context. You just need to expose the same classes as OSGi services, with the same properties. The following list gives you which class or interface to extend and to expose as a service : 

  • Render Actions : org.jahia.bin.Action  

  • Render filters : org.jahia.services.render.filter.RenderFilter
  • Error handlers : org.jahia.bin.errors.ErrorHandler 
  • ChoiceList initializers and renderers : org.jahia.services.content.nodetypes.initializers.ModuleChoiceListInitializer and org.jahia.services.content.nodetypes.renderer.ModuleChoiceListRenderer 
  • JCR event listeners : org.jahia.services.content.DefaultEventListener 
  • Node decorators and validators : org.jahia.services.content.decorator.JCRNodeDecoratorDefinition and org.jahia.services.content.decorator.validation.JCRNodeValidatorDefinition        
  • Cache key part generators : org.jahia.services.render.filter.cache.CacheKeyPartGenerator        
  • Servlet filters : org.jahia.bin.filters.AbstractServletFilter   

Some extensions need to be declared explicitely in a specific class, which contains a map of all the extensions you want to register. Only one instance of this class need to be exposed as a service to declare your extensions :  

  • Node decorators : declare a service extending org.jahia.services.content.decorator.JCRNodeDecoratorDefinition and override getDecorators to return your decorators classes.
  • Node validators : declare a service extending org.jahia.services.content.decorator.validation.JCRNodeValidatorDefinition and override getValidators to return your validator classes.
  • Rules global objects : register your rules services objects in an instance of org.jahia.services.content.rules.ModuleGlobalObject

In the same way, some configurations which were done with Spring earlier can still be done by declaring the beans as an OSGi service : 

  • Static asset mapping : register your asset mappings in an instance of org.jahia.services.render.StaticAssetMapping
  • Workflow registration : register your worflows definitions, form and permissions in an instance of org.jahia.services.workflow.WorkflowTypeRegistration

Using Declarative Services

Code samples

Sample code showing how to achieve that with Declarative Services are available on https://github.com/Jahia/OSGi-modules-samples .

Simple component

When using Declarative Services, you need to add the @Component annotation to declare your class as DS component, with the service attribute set to the base class. The component can set its attribute directly in its activator :

@Component(service = RenderFilter.class, immediate = true)
public class TestFilter extends AbstractFilter {

    @Activate
    public void activate() {
        setPriority(1);
        setApplyOnConfigurations("page");
    }

Configured component

A component can also rely on an OSGi configuration. The configuration will have by default the same name as the component, which is the fully qualified class name. In the following example, the content of <data>/karaf/etc/org.mypackage.MyChoiceListInitializer.cfg will be passed in the props parameter of the activator :

@Component(service = {ModuleChoiceListInitializer.class, ModuleChoiceListRenderer.class})
public class MyChoiceListInitializer extends AbstractChoiceListRenderer implements ModuleChoiceListRenderer, ModuleChoiceListInitializer {

    @Activate
    public void activate(Map<String, ?> props) {
        setKey((String) props.get("key"));
    }
}

You can also use an annotation type containing the properties you expect instead of the generic Map<String, ?> type. It allows you to have typed attributes and default values :

    @interface Param {
        String key() default "default-key";
    }

    @Activate
    public void activate(Param p) {
        setName(p.key());
    }

Note that configuration files can be provided in modules in resources/META-INF/configurations - they will be copied to <data>/karaf/etc .

Component factories

Another useful feature of the DS components is the component factory - you can create multiple instances of your beans based on multiple configuration files. Compared to the previous example, you need to modify the component scope to Bundle , as in the following example :

@Component(service = RenderFilter.class, scope = ServiceScope.BUNDLE)
public class MyFilter extends AbstractFilter {

    @Activate
    public void activate(Map<String, ?> props) {
        try {
            BeanUtils.copyProperties(this, props);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

You can then define multiple configurations files, all of them named org.mypackage.MyFilter-<id>.cfg , with a different id for each config. One instance of the component will be created for each configuration.