Custom extension points and extensions allow to customize your applications declaratively.

The OSGi service layer in general and the OSGi Declarative Services in particular already make it quite easy to decouple bundles.

But if you want to associate some meta data declaratively you will hit a limit with Declarative Services.

Drombler FX provides a mechanism based on the Extender Pattern to provide configurations declaratively. It looks for the following file in each bundle: /META-INF/drombler/application.xml

Drombler FX uses the concept of extension points and extensions.

Extension points, provided by Drombler FX itself or by some framework developers, allow other developers to register extensions to customize their applications, e.g. adding some GUI elements declaratively.

Most Drombler FX developers will register extensions of existing extension points rather than defining their own custom extension points.

1. Predefined Extension Points by Drombler FX

Drombler FX provides several extension points out-of-the-box:

  • actions, menu entries, menus, toolbar entries, toolbars - see Actions, Menus and Toolbars for more information

  • document data handlers, business object data handlers, file extensions - see Data Framework for more information

  • views, editors - see Docking Framework for more information

  • status bar entries - see Status Bar for more information

The extension points provided by Drombler FX are specified in the GUI-toolkit agnostic Drombler ACP and thus have no dependencies on JavaFX, which makes the implementations of the handlers slightly more complex.

Framework providers, however, are free to provide JavaFX specific extension points as well.

2. Register an Extension for an Extension Point

Extensions are registered in /META-INF/drombler/application.xml.

There are several ways to register an extension based on an extension point.

2.1. Extension Annotations

The easiest way to register an extension is usually to use the extension point specific annotations.

If the annotation processor was registered correctly alongside with the annotations then you just need to declare the corresponding dependency and add the annotations to a supported target.

The compiler will pick up the annotation processor and write the application.xml once all annotations of all extension points in a bundle are processed.

@Foo(bar = "a", position = 10)
public class FooComponent1 {

2.2. Extension File

Another way to register an extension is to provide an extension point specific XML file.

This approach can also be used when the extension point provider didn’t specify any extension point specific annotation.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<foos xmlns="http://www.mycompany.com/schema/myapp/foos">
    <foo>
        <bar>a</bar>
        <position>10</position>
        <fooClass>tutorial.extension.foo.impl.FooComponent2</fooClass>
    </foo>
</foos>

You can register this extension file using the @Extension annotation on a package (usually in package-info.java):

@Extension(extensionFile = "META-INF/drombler/foos.xml",
        extensionJAXBRootClass = FoosType.class)
package tutorial.extension.foo;

import org.drombler.acp.core.application.Extension;
import tutorial.extension.foo.jaxb.FoosType;

2.3. application.xml

You can also just provide the whole application.xml file at the correct location (/META-INF/drombler/application.xml).

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<application xmlns="http://www.drombler.org/schema/acp/application"
             xmlns:ns2="http://www.mycompany.com/schema/myapp/foos" >
    <extensions>
        <n2:foos>
            <n2:foo>
                <n2:bar>a</n2:bar>
                <n2:position>10</n2:position>
                <n2:fooClass>tutorial.extension.foo.impl.FooComponent2</n2:fooClass>
            </n2:foo>
        </n2:foos>
    </extensions>
</application>

All extension configurations are sub-elements of the extensions element.

2.4. Programmatic

Drombler FX registers each unmarshalled extension JAXB object as an OSGi service.

You can do this also programmatically.

    private void registerFoos(final BundleContext bundleContext) {
        FoosType foos = createFoos();

        bundleContext.registerService(FoosType.class, foos, null);
    }

    private FoosType createFoos() {
        FoosType foos = new FoosType();
        foos.getFoo().add(createFoo1());
        return foos;
    }

    private FooType createFoo1() {
        FooType foo = new FooType();
        foo.setBar("a");
        foo.setPosition(10);
        foo.setFooClass(FooComponent1.class.getName());
        return foo;
    }

If the extension point provider also specified an extension point specific descriptor (a more type-safe variant of the JAXB classes), then you can also register the descriptors directly:

    private void registerFooDescriptor(final BundleContext bundleContext) {
        FooDescriptor<FooComponent1> fooDescriptor
                = new FooDescriptor<>(FooComponent1.class, "a", 10);
        bundleContext.registerService(FooDescriptor.class, fooDescriptor, null);
    }

3. Custom Extension Points

Every extension point has to register a custom org.drombler.acp.core.application.ExtensionPoint implementation as an OSGi service.

The only method you have to implement is to provide the JAXB class for the root element.

import tutorial.extension.foo.jaxb.FoosType;
import org.drombler.acp.core.application.ExtensionPoint;
import org.osgi.service.component.annotations.Component;

@Component
public class FooExtensionPoint implements ExtensionPoint<FoosType> {

    @Override
    public Class<FoosType> getJAXBRootClass() {
        return FoosType.class;
    }

}

3.1. Handlers

Once you defined your extension point you usually need a handler service which does something with the extension.

When Drombler FX reads the application.xml, it will register each unmarshalled root element as an OSGi service.

This allows OSGi services to listen for new instances.

    @Reference(cardinality = ReferenceCardinality.MULTIPLE,
            policy = ReferencePolicy.DYNAMIC)
    public void bindFoosType(ServiceReference<FoosType> serviceReference) {
        BundleContext context = serviceReference.getBundle().getBundleContext();
        FoosType foosType = context.getService(serviceReference);
        registerFoos(foosType, context);

3.2. Annotations (optional)

If you want to provide some convenience for the developers who want to use your extension point, consider to provide a custom annotation. Though this is highly recommended, this is an optional step and not required by Drombler FX.

@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Foo {

    String bar();

    int position();
}

3.2.1. Annotation Processor (optional)

If you define a custom annotation for your extension point you also need to provide an annotation processor, which adds the extension configuration to the application.xml file.

It’s a best practice to provide and register the annotation processor in the same Maven artifact as the annotation, as this will pick up the annotation processor automatically when using the annotation.

You can extend the AbstractApplicationAnnotationProcessor which takes care of writing the application.xml once all annotations in one artifact are processed.

Implementations must add:

package tutorial.extension.foo.impl;

import java.util.Set;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import org.drombler.acp.core.application.processing.AbstractApplicationAnnotationProcessor;
import tutorial.extension.foo.Foo;
import tutorial.extension.foo.jaxb.FooType;
import tutorial.extension.foo.jaxb.FoosType;

@SupportedAnnotationTypes({"tutorial.extension.Foo"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class FooAnnotationProcessor extends AbstractApplicationAnnotationProcessor {

    private FoosType foos;

    @Override
    protected boolean handleProcess(Set<? extends TypeElement> annotations,
            RoundEnvironment roundEnv) {
        roundEnv.getElementsAnnotatedWith(Foo.class).forEach(element -> {
            Foo fooAnnotation = element.getAnnotation(Foo.class);
            if (fooAnnotation != null) {
                registerFoo(fooAnnotation, element);
            }
        });
        return false;
    }

    private void registerFoo(Foo fooAnnotation, Element element) {
        init(element);

        FooType foo = new FooType();
        foo.setBar(fooAnnotation.bar());
        foo.setPosition(fooAnnotation.position());
        foo.setFooClass(element.asType().toString());
        foos.getFoo().add(foo);
    }

    private void init(Element element) {
        if (foos == null) {
            foos = new FoosType();
            addExtensionConfiguration(foos);
            addJAXBRootClass(FoosType.class);
        }
        addOriginatingElements(element);
    }

}

As you can see in the example, if the element represents a class you can get the corresponding TypeMirror and class name of the annotated class with:

element.asType().toString()

Next, don’t forget to register the annotation processor in /META-INF/services/javax.annotation.processing.Processor.

tutorial.extension.foo.impl.FooAnnotationProcessor

This will allow the compiler to pick-up the annotation processor automatically without any further configuration.

You can read more about this standard service-provider loading facility in the Javadoc of ServiceLoader.

Please note that you need to disable annotation processing in artifacts which provide annotation processors themselves. You can define the following Maven property in the pom.xml file of the corresponding artifact.

<java.compiler.compilerArgument>-proc:none</java.compiler.compilerArgument>
Tips & Tricks

The SoftSmithy Utility Library provides some utility methods for working with model classes.

<dependency>
  <groupId>org.softsmithy.lib</groupId>
  <artifactId>softsmithy-lib-compiler</artifactId>
  <type>bundle</type>
</dependency>

To manage the versions of the individual SoftSmithy Utility Library artifacts you can use:

<dependencyManagement>
    <dependencies>
        [...]
        <dependency>
            <groupId>org.softsmithy.lib</groupId>
            <artifactId>softsmithy-lib</artifactId>
            <version>{softsmithy-version}</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
        [...]
    </dependencies>
</dependencyManagement>
Annotation Elements of Type Class

If we have an annotation with an element of type Class, such as:

@SomeAnnotation(someType = Sample.class)
public class SomeComponent {

we can get the corresponding TypeMirror in the annotation processor with ModelTypeUtils.getTypeMirror.

        roundEnv.getElementsAnnotatedWith(SomeAnnotation.class).forEach(element -> {
            SomeAnnotation someAnnotation = element.getAnnotation(SomeAnnotation.class);
            if (someAnnotation != null) {
                TypeMirror typeMirror
                        = ModelTypeUtils.getTypeMirror(someAnnotation::someType);

                //...
            }
        });
Annotation Elements of Type Class Array

If we have an annotation with an element of type Class array, such as:

@SomeOtherAnnotation(barClasses = {Bar1.class, Bar2.class})
public class SomeOtherComponent {

we can get the corresponding list of TypeMirrors in the annotation processor with ModelTypeUtils.getTypeMirrorsOfClassArrayAnnotationValue.

Here is a sample which gets all fully qualified class names:

        roundEnv.getElementsAnnotatedWith(SomeOtherAnnotation.class).forEach(element -> {
            List<? extends AnnotationMirror> annotationMirrors
                    = element.getAnnotationMirrors();
            List<String> classNames
                    = ModelTypeUtils.getTypeMirrorsOfClassArrayAnnotationValue(
                            annotationMirrors,
                            SomeOtherAnnotation.class,
                            "barClasses").stream()
                            .map(TypeMirror::toString)
                    .collect(Collectors.toList());

            //...
        });

This will return:

["tutorial.extension.other.package1.Bar1", "tutorial.extension.other.package2.Bar2"]

3.3. Descriptors (optional)

If you want to provide some convenience for the developers who want to use your extension point, consider to provide a custom descriptor. Descriptors are a more type-safe variant of the JAXB classes and descriptor is a naming convention in Drombler_FX.

Though this is highly recommended, this is an optional step and not required by Drombler FX.

public class FooDescriptor<T> {

    private final Class<T> fooClass;
    private final String bar;
    private final int position;

You can then provide some utility methods to load classes (or other resources) from the bundle (keep in mind that in OSGi every bundle has its own class loader).

    /**
     * Creates an instance of a {@link FooDescriptor} from a {@link FooType} unmarshalled from the application.xml.
     *
     * @param foo the unmarshalled foo
     * @param bundle the bundle of the application.xml
     * @return a FooDescriptor
     * @throws java.lang.ClassNotFoundException
     */
    public static FooDescriptor<?> createFooDescriptor(FooType foo, Bundle bundle)
            throws ClassNotFoundException {
        Class<?> fooClass = BundleUtils.loadClass(bundle, foo.getFooClass());
        return createFooDescriptor(foo, fooClass);
    }

    private static <T> FooDescriptor<T> createFooDescriptor(FooType foo,
            Class<T> fooClass) {
        return new FooDescriptor<>(fooClass, foo.getBar(), foo.getPosition());
    }

In the handler you can then create a descriptor for every unmarshalled JAXB object and register them as OSGi services.

    private void registerFoos(FoosType foosType, BundleContext context) {
        foosType.getFoo().forEach(fooType
                -> registerFoo(fooType, context));
    }

    private void registerFoo(FooType fooType, BundleContext context) {
        try {
            FooDescriptor<?> fooDescriptor
                    = FooDescriptor.createFooDescriptor(fooType, context.getBundle());
            context.registerService(FooDescriptor.class, fooDescriptor, null);
        } catch (ClassNotFoundException ex) {
            LOG.error(ex.getMessage(), ex);
        }
    }

This allows OSGi services to listen for new instances.

    @Reference(cardinality = ReferenceCardinality.MULTIPLE,
            policy = ReferencePolicy.DYNAMIC)
    public void bindFooDescriptor(FooDescriptor<? extends T> fooDescriptor) {
        registerFoo(fooDescriptor);
    }

3.4. Samples

3.4.1. Handler Sample

package tutorial.extension.foo.impl;

import java.util.ArrayList;
import java.util.List;
import org.drombler.acp.core.commons.util.concurrent.ApplicationThreadExecutorProvider;
import org.drombler.acp.core.context.ContextManagerProvider;
import org.drombler.commons.context.ContextInjector;
import org.drombler.commons.context.Contexts;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
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.Deactivate;
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 org.softsmithy.lib.util.PositionableAdapter;
import tutorial.extension.foo.FooDescriptor;
import tutorial.extension.foo.jaxb.FooType;
import tutorial.extension.foo.jaxb.FoosType;

@Component
public class FooHandler<T> {
    private static final Logger LOG = LoggerFactory.getLogger(FooHandler.class);

    private final List<FooDescriptor<? extends T>> unresolvedFooDescriptors
            = new ArrayList<>();

    @Reference
    private ApplicationThreadExecutorProvider applicationThreadExecutorProvider;

    @Reference
    private ContextManagerProvider contextManagerProvider;

    private ContextInjector contextInjector;

    @Reference(cardinality = ReferenceCardinality.MULTIPLE,
            policy = ReferencePolicy.DYNAMIC)
    public void bindFoosType(ServiceReference<FoosType> serviceReference) {
        BundleContext context = serviceReference.getBundle().getBundleContext();
        FoosType foosType = context.getService(serviceReference);
        registerFoos(foosType, context);
    }

    public void unbindFoosType(FoosType foosType) {
        //...
    }

    @Reference(cardinality = ReferenceCardinality.MULTIPLE,
            policy = ReferencePolicy.DYNAMIC)
    public void bindFooDescriptor(FooDescriptor<? extends T> fooDescriptor) {
        registerFoo(fooDescriptor);
    }

    public void unbindFooDescriptor(FooDescriptor<? extends T> fooDescriptor) {
        //...
    }

    @Activate
    protected void activate(ComponentContext context) {
        contextInjector = new ContextInjector(contextManagerProvider.getContextManager());
        resolveUnresolvedFooDescriptors();
    }

    @Deactivate
    protected void deactivate(ComponentContext context) {
        contextInjector = null;
    }

    private boolean isInitialized() {
        return applicationThreadExecutorProvider != null
                && contextManagerProvider != null
                && contextInjector != null;
    }

    private void registerFoos(FoosType foosType, BundleContext context) {
        foosType.getFoo().forEach(fooType
                -> registerFoo(fooType, context));
    }

    private void registerFoo(FooType fooType, BundleContext context) {
        try {
            FooDescriptor<?> fooDescriptor
                    = FooDescriptor.createFooDescriptor(fooType, context.getBundle());
            context.registerService(FooDescriptor.class, fooDescriptor, null);
        } catch (ClassNotFoundException ex) {
            LOG.error(ex.getMessage(), ex);
        }
    }

    private void registerFoo(FooDescriptor<? extends T> fooDescriptor) {
        if (isInitialized()) {
            registerFooInitialized(fooDescriptor);
        } else {
            unresolvedFooDescriptors.add(fooDescriptor);
        }
    }

    private void registerFooInitialized(FooDescriptor<? extends T> fooDescriptor) {
        // this uses the GUI-toolkit agnostic applicationThreadExecutorProvider
        applicationThreadExecutorProvider.getApplicationThreadExecutor().execute(() -> {
            try {
                T foo = createFoo(fooDescriptor);
                PositionableAdapter<? extends T> positionableFoo
                        = new PositionableAdapter<>(foo, fooDescriptor.getPosition());
                // do something on the application thread
            } catch (InstantiationException | IllegalAccessException ex) {
                LOG.error(ex.getMessage(), ex);
            }
        });
    }

    private T createFoo(FooDescriptor<? extends T> fooDescriptor)
            throws InstantiationException, IllegalAccessException {
        T foo = fooDescriptor.getFooClass().newInstance();
        Contexts.configureObject(foo,
                contextManagerProvider.getContextManager(),
                contextInjector);
        return foo;
    }


    private void resolveUnresolvedFooDescriptors() {
        List<FooDescriptor<? extends T>> unresolvedFooDescriptorsCopy
                = new ArrayList<>(unresolvedFooDescriptors);
        unresolvedFooDescriptors.clear();
        unresolvedFooDescriptorsCopy.forEach(this::registerFoo);
    }
}