Eclipse Extended Contribution Pattern

Working in the Panorama project, we developed several architectures and designs to improve the collaboration of heterogeneous systems. Although focused on automotive and the aerospace scope, several topics are useful in general, like for example the pipelining of model processing services via a generic REST API. While on the one side the combination of several self-contained services can be achieved, the collaboration of heterogeneous organisations is also a big topic. In particular how multiple partners with different knowledge and technical skills can contribute to a common platform, e.g. the Eclipse IDE or especially APP4MC which is an Eclipse IDE based product.

The Eclipse Platform is actually designed to be extensible and there exist many products that are based on the Eclipse IDE or can be installed into the Eclipse IDE as additional plug-ins. But to create such extensions you need to know the base you want to extend. In a setup with multiple partners that use different technology stacks and have different levels of experience with Eclipse based technology, you can’t assume that everything works easily. There are partners that have either experience with Eclipse 3 or Eclipse 4, you have partners that are neither aware of the Eclipse 3 or the Eclipse 4 platform, and you even have partners that do not want to take care about the underlying platform. Therefore we needed to find a way to make it easy for anyone to contribute new features, without having too much platform dependencies to take care about.

As a big fan of OSGi Declarative Services (you might know if you read some of my previous blog posts), I searched for a way to contribute a new feature to the user interface by implementing and providing an OSGi service. As an Eclipse Platform committer I know that the Eclipse 4 programming model fits very good for connecting the OSGi layer with the Eclipse layer. Something that doesn’t work that easily with the Eclipse 3 programming model. I called the solution I developed the Extended Contribution Pattern, which I want to describe here in more detail. And I hope with the techniques I show here, I can convince more people to use OSGi Declarative Services and the Eclipse 4 programming model in their daily work when creating Eclipse based products.

The main idea is that an integration layer is implemented with the Eclipse 4 programming model. That integration layer is responsible for the contribution to the Eclipse 3 based application (again, this is Eclipse 4 + Compatibility layer). Additionally it takes and processes the contributions provided via OSGi DS.

For people knowing the Eclipse 3 programming model, this sounds pretty similar to how Extension Points work. And the idea is actually the same. But in comparison, as a developer of the integration layer:

  • you don’t need to specify the extension point in an XSD/XML way
  • you can use dependency injection instead of operating on the ExtensionRegistry, which is quite some code that is also not type safe
  • you can completely rely on the Eclipse 4 programming model and contribute simple POJOs instead of following the class hierarchy in multiple places

As a contributor to the integration layer:

  • you don’t need to specify the extension via plugin.xml
  • you don’t need to even care about the Eclipse platform
  • you simply implement an OSGi service using Declarative Service Annotations and implement a method that follows the contract of the contribution

Note:
The Integration Layer is not needed for connecting the OSGi layer with the Eclipse layer. You can directly consume OSGi services easily via injection in Eclipse 4. The Integration Layer is used to abstract out the UI integration.

Example

In this example I will show how the Extended Contribution Pattern can be used to contribute menu items to the context menu of the navigator views. Of course this could also be achieved by either contribute to the Eclipse 3 extension point or by directly contribute via Eclipse 4 model fragments. But the idea is that contributors of functionality should not care about the integration into the platform.

Step 1: Create the plugin for the integration layer

  • Switch to the Plug-in Perspective
  • Create a new Plug-in Project via File -> New -> Plug-in Project
  • Choose a meaningful name (e.g. org.fipro.contribution.integration)
  • Ensure that Generate an activator is unchecked
  • Ensure that This plug-in will make contributions to the UI is checked
  • Set Create a rich client application? is set to No

Step 2: Define the service interface

This step is the easiest one. The service interface needs to be a simple marker interface that will be used to mark a contribution class as an OSGi service.

  • Create a package org.fipro.contribution.integration
  • Create a marker interface NavigatorMenuContribution
package org.fipro.contribution.integration;

public interface NavigatorMenuContribution { }
  • Open the META-INF/MANIFEST.MF file
  • Switch to the Runtime tab and export the org.fipro.contribution.integration package
  • Specify the version 1.0.0 on the package via Properties…

Step 3: Create the plugin for the service contribution

  • Switch to the Plug-in Perspective
  • Create a new Plug-in Project via File -> New -> Plug-in Project
  • Choose a meaningful name (e.g. org.fipro.contribution.service)
  • Ensure that Generate an activator is unchecked
  • Ensure that This plug-in will make contributions to the UI is checked
  • Set Create a rich client application? is set to No

Step 4: Implement a service

Now let’s implement a service for a functionality we want to contribute. The Integration Layer is not complete yet and typically you would not show the contribution service implementation at this point. But to get a better understanding of the next steps in the Integration Layer, it is good to see how the contribution will look like.

Note:
Don’t forget to enable the DS Annotation processing in the Preferences. Otherwise the necessary OSGi Component Descriptions are not generated. As it is not enabled by default, it is a common pitfall when implementing OSGi Declarative Services with PDE tooling.

First we need to define the dependencies:

  • Open the META-INF/MANIFEST.MF file
  • Switch to the Dependencies tab and add the following packages to the Imported Packages
    • javax.annotation
      Needed for the @PostConstruct annotation
    • org.fipro.contribution.integration (1.0.0)
      Needed for the previously created marker interface
    • org.osgi.service.component.annotations [1.3.0,2.0.0) optional
      Needed for the OSGi DS annotations
  • Add the following plug-ins to the Required Plug-ins section
    • org.eclipse.jface
      Needed for showing dialogs
    • org.eclipse.core.resources
      Needed for the Eclipse Core Resources API to access the Eclipse resources
    • org.eclipse.core.runtime
      Needed as transitive dependency for operating on the resources

Note:
Typically I recommend to use Import-Package instead of Require-Bundle. For plain OSGi this is the best solution. But I learned over the years that especially in the context of Eclipse IDE contributions being that strict doesn’t work out. Especially because of some split package issues in the Eclipse Platform. My personal rule for PDE based projects is:

  • Bundles / plug-ins that contain services that are not related to UI and could be also part of other OSGi runtimes (e.g. executable jars or integrated in webservices) should only use Import-Package
  • Bundles / plug-ins that contribute to the UI, e.g. the Eclipse IDE, can also use Require-Bundle in some cases, to reduce the manual effort on dependency management

Now create the service:

  • Create a package org.fipro.contribution.service
  • Create a component class FileSizeContribution
@Component(property = {
    "name = File Size",
    "description = Show the size of the selected file" })
public class FileSizeContribution implements NavigatorMenuContribution {

    @PostConstruct
    public void showFileSize(IFile file, Shell shell) {
        URI uri = file.getRawLocationURI();
        Path path = Paths.get(uri);
        try {
            long size = Files.size(path);
            MessageDialog.openInformation(
                shell,
                "File size",
                String.format("The size of the selected file is %d bytes", size));
        } catch (IOException e) {
            MessageDialog.openError(
                shell, 
                "Failed to retrieve the file size", 
                "Exception occured on retrieving the file size: "
                + e.getLocalizedMessage());
        }
    }
}

The important things to notice in the above snippet are:

  • The class needs to implement the marker interface NavigatorMenuContribution
  • The class needs to be annotated via @Component to mark it as an OSGi DS component
  • The @Component annotation has two properties to specify the name and the description. They will later be used for the user interface integration. In my opinion these two properties are component configurations and should therefore be specified as such. You could on the other side argue that this information could also be provided via some dedicated methods, but implementing methods to provide configurations for the service instance feels incorrect.
  • The class contains a single method that is annotated via @PostConstruct. The first method parameter defines for which type the service is responsible.

For a contributor the rules are pretty simple:

  • Mark the contribution with @Component as an OSGi Declarative Service
  • Implement the marker interface
  • Provide a method that is annotated with @PostConstruct
  • The first method parameter needs to be the type the contribution takes care of

A contributor does not need to take care about the infrastructure in the Eclipse application and can focus on the feature that should be contributed.

Step 5: Implement a registry as service consumer

Back to the Integration Layer now. To provide as much flexibility on the contributor side, there needs to be a mechanism that can map that flexibility to the real integration. For this we create a registry that consumes the contributions in first place and stores them for further usage. For the storage we introduce a wrapper around the service, that stores the type for which the service should be registered and the properties that should be used in the user interface (e.g. name and description). For the service properties the issue is that the properties are provided on OSGi DS injection level and can be retrieved from the ServiceRegistry, but they are not easily accessible in the Eclipse layer. By keeping the information in a wrapper that is populated when the service becomes available, the problem can be handled.

The wrapper class looks similar to the following snippet:

public class NavigatorMenuContributionWrapper {

    private final String id;
    private final NavigatorMenuContribution instance;

    private final String name;
    private final String description;
    private final String type;

    public NavigatorMenuContributionWrapper(
        String id,
        NavigatorMenuContribution instance,
        String name,
        String description,
        String type) {

        this.id = id;
        this.instance = instance;
        this.name = name;
        this.description = description;
        this.type = type;
    }
	
    public String getId() {
        return this.id;
    }
	
    public NavigatorMenuContribution getServiceInstance() {
        return this.instance;
    }
	
    public String getName() {
        return name;
    }
	
    public String getDescription() {
        return description;
    }
	
    public String getType() {
        return type;
    }
}

Note:
If you are sure that the IDE you are contributing to is always started with Java >= 16, you can of course also implement that wrapper as a Java Record, which avoids quite some boilerplate code. In that case the accessor methods are different, as they are not prefixed with get.

public record NavigatorMenuContributionWrapper(
    String id,
    NavigatorMenuContribution serviceInstance,
    String name,
    String description,
    String type) { }

In this tutorial I will stick with the old POJO approach, so people that are not yet on the latest Java version can follow easily.

The registry that consumes the NavigatorMenuContribution services and stores them locally has the following characteristics:

  • It is an OSGi service that actually does not need an interface as there will be only one implementation. That means we need to set the service parameter on the @Component annotation.
  • We use the event strategy (method injection) for consuming the NavigatorMenuContribution services. The reason is that we need to create the wrapper instances with the component properties. Field injection would not work here.
  • The Dynamic Reference Policy is used to ensure that services can be registered/unregistered at runtime.
    (more information on reference policies can be found here).
  • There are accessor methods for retrieving the services based on the type.
  • There needs to be a method that extracts the type for which the service is responsible from the @PostConstruct method via reflection. To avoid reflection you could support a component property that gets evaluated, but that would make the contribution not so intuitive, as you would need to specify the same information twice. And actually the reflection is only executed once per service binding, so it should not really have an effect at runtime.
  • Last but not least, if you want OSGi logging you need to get the Logger via method injection of the LoggerFactory. This is due to the fact that PDE does not support DS 1.4 annotation processing. With that support you could get the Logger directly via field injection. Alternatively you can of course use a logging framework like SLF4J and don’t use the OSGi logging at all.

The complete implementation looks like this:

@Component(service = NavigatorMenuContributionRegistry.class)
public class NavigatorMenuContributionRegistry {

    LoggerFactory factory;
    Logger logger;

    private ConcurrentHashMap<String, Map<String, NavigatorMenuContributionWrapper>> registry = new ConcurrentHashMap<>();
	
    @Reference(
        cardinality = ReferenceCardinality.MULTIPLE,
        policy = ReferencePolicy.DYNAMIC)
    protected void bindService(
        NavigatorMenuContribution service, Map<String, Object> properties) {
		
        String className = getClassName(service, properties);
        if (className != null) {
            Map<String, NavigatorMenuContributionWrapper> services = 
                this.registry.computeIfAbsent(
                    className, 
                    key -> new ConcurrentHashMap<String, NavigatorMenuContributionWrapper>());
			
            String id = (String) properties.getOrDefault("id", service.getClass().getName());
            if (!services.containsKey(id)) {
                services.put(id,
                    new NavigatorMenuContributionWrapper(
                        id, 
                        service, 
                        (String) properties.getOrDefault("name", service.getClass().getSimpleName()), 
                        (String) properties.getOrDefault("description", null),
                        className));
            } else {
                if (this.logger != null) {
                    this.logger.error("A NavigatorMenuContribution with the ID {} already exists!", id);
                } else {
                    System.out.println("A NavigatorMenuContribution with the ID " + id + " already exists!");
                }
            }
        } else {
            if (this.logger != null) {
                this.logger.error(
                    "Unable to extract contribution class name for NavigatorMenuContribution {}", 
                    service.getClass().getName());
            } else {
                System.out.println(
                    "Unable to extract contribution class name for NavigatorMenuContribution " 
                    + service.getClass().getName());
            }
        }
    }

    protected void unbindService(
        NavigatorMenuContribution service, Map<String, Object> properties) {

        String className = getClassName(service, properties);
        String id = (String) properties.getOrDefault("id", service.getClass().getName());
        if (className != null) {
            Map<String, NavigatorMenuContributionWrapper> services = 
                this.registry.getOrDefault(className, new HashMap<>());
            services.remove(id);
        }
    }
	
    @SuppressWarnings("unchecked")
    public List<NavigatorMenuContributionWrapper> getServices(Class<?> clazz) {
        HashSet<String> classNames = new LinkedHashSet<>();
        if (clazz != null) {
            classNames.add(clazz.getName());
            List<Class<?>> allInterfaces = ClassUtils.getAllInterfaces(clazz);
            classNames.addAll(
                allInterfaces.stream()
                    .map(Class::getName)
                    .collect(Collectors.toList()));
        }

        return classNames.stream()
            .filter(Objects::nonNull)
            .flatMap(name -> this.registry.getOrDefault(name, new HashMap<>()).values().stream())
            .collect(Collectors.toList());
    }

    public NavigatorMenuContributionWrapper getService(String className, String id) {
        return this.registry.getOrDefault(className, new HashMap<>()).get(id);
    }

    /**
     * Extracts the class name for which the service should be
     * registered. Returns the first parameter of the method annotated with
     * {@link PostConstruct} .
     * 
     * @param service The service for which the contribution class name
     *                      should be returned.
     * @param properties    The component properties map of the
     *                      service object.
     * @return The contribution class name for which the service should be
     *         registered.
     */
    private String getClassName(NavigatorMenuContribution service, Map<String, Object> properties) {
        String className = null;

        // find method annotated with @PostConstruct
        Class<?> contributionClass = service.getClass();
        Method[] methods = contributionClass.getMethods();
        for (Method method : methods) {
            if (method.isAnnotationPresent(PostConstruct.class)) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes.length > 0) {
                    if (Collection.class.isAssignableFrom(parameterTypes[0])) {
                        // extract generic information for List support
                        Type[] genericParameterTypes = method.getGenericParameterTypes();
                        if (genericParameterTypes[0] instanceof ParameterizedType) {
                            Type[] typeArguments =
                                ((ParameterizedType)genericParameterTypes[0]).getActualTypeArguments();
                            className = typeArguments.length > 0 ? typeArguments[0].getTypeName() : null;
                        }
                    } else {
                        className = parameterTypes[0].getName();
                    }
                    break;
                }
            }
        }

        return className;
    }

    @Reference(
        cardinality = ReferenceCardinality.OPTIONAL,
        policy = ReferencePolicy.DYNAMIC)
    void setLogger(LoggerFactory factory) {
        this.factory = factory;
        this.logger = factory.getLogger(getClass());
    }

    void unsetLogger(LoggerFactory loggerFactory) {
        if (this.factory == loggerFactory) {
            this.factory = null;
            this.logger = null;
        }
    }
}

Remember to update the Dependencies in the MANIFEST.MF to include the necessary packages.

Plug-in Dependencies

With the above implementation we need to add additional dependencies. To avoid complications at implementation time in the next step, we update the plug-in dependencies in advance. As we know that we want to consume OSGi services and operate on Eclipse resources, we know what dependencies we need. In a real-world project the dependencies typically grow while implementing.

  • Open the META-INF/MANIFEST.MF file
  • Switch to the Dependencies tab and add the following packages to the Imported Packages if they are not included yet
    • javax.annotation
      Needed for the @PostConstruct annotation
    • javax.inject (1.0.0)
      Needed for the common injection annotations
    • org.apache.commons.lang (2.6.0)
      Needed to be able to use ClassUtils in the inspection.
    • org.osgi.service.component.annotations [1.3.0,2.0.0) optional
      Needed for the OSGi DS annotations
    • org.osgi.service.log (1.5.0)
      Needed to be able to consume the OSGi Logger
  • Add the following plug-ins to the Required Plug-ins section if they are not included yet
    • org.eclipse.e4.core.contexts
      Needed for the IEclipseContext
    • org.eclipse.e4.core.di
      Needed for the Eclipse specific injection annotations (e.g. @Evaluate)
    • org.eclipse.e4.core.di.extensions
      Needed for the Eclipse specific injection annotations (e.g. @Service)
    • org.eclipse.e4.ui.di
      Needed for the Eclipse UI specific injection annotations (e.g. @AboutToShow)
    • org.eclipse.e4.ui.model.workbench
      Needed to dynamically creating model elements (e.g. MMenuElement)
    • org.eclipse.e4.ui.services
      Needed for the Eclipse UI service specific classes (e.g. IServiceConstants)
    • org.eclipse.e4.ui.workbench
      Needed for the Eclipse UI services (e.g. EModelService)
    • org.eclipse.jface
      Needed for showing dialogs
    • org.eclipse.core.resources
      Needed for the Eclipse Core Resources API to access the Eclipse resources
    • org.eclipse.core.runtime
      Needed as transitive dependency for operating on the resources

Step 6: Define the application model contribution

After the services and the Integration Layer are specified, let’s have a look on how to use it. For this create a Model Fragment to contribute a dynamic menu contribution to the context menus.

  • Right click on the project org.fipro.contribution.integration
  • New -> Other -> Eclipse 4 -> Model -> New Model Fragment

The wizard that opens will do the following three things:

  1. Create a file named fragment.e4xmi
    This is the application model fragment needed for the contribution.
  2. Create a plugin.xml file and add it to the build.properties
    Needed to contribute the Model Fragment via Extension Point
  3. Update the MANIFEST.MF
    Include the necessary dependency for the Extension Point

Since Eclipse 2021-06 (4.20) it is also possible to register a Model Fragment via Manifest header. To make use of this follow these steps:

  • Delete the plugin.xml file, remember to also remove it from the build.properties again
  • Add the following line to the MANIFEST.MF file
    Model-Fragment: fragment.e4xmi;apply=always
  • Remove the dependency to the bundle org.eclipse.e4.ui.model.workbench from the MANIFEST.MF file if not needed. In this example we will not remove it, as we need it in another use case for dynamically creating model elements.

Note:
Adding support for the new Model-Fragment header in the PDE tooling is currently ongoing, e.g. via Bug 572946. So with the next Eclipse 2021-12 (4.22) the manual modification is not necessary anymore. The Eclipse 2021-12 M3 is already including the support. Using that version you will see this wizard:

Model Fragment Wizard in Eclipse 2021-12 M3

The next step is to define the model contributions. This example is about contributing a Dynamic Menu Contribution to the context menu of the Navigators. Therefore it is necessary to contribute a Command, a Handler and the Menu Contribution. To do this start by opening the fragment.e4xmi file.

Command Contribution

  • Select Model Fragments in the tree on the left side of the Eclipse Model Editor
  • Click the Add button on the detail pane on the right side (can also be done via context menu in the tree)
  • In the details pane for the created Model Fragment set
    • Extended Element-ID: xpath:/
    • Feature Name: commands
    • Click the Add button to add a new Command
  • In the details pane for the created Command
    • Name: File Navigator Action
    • Click the Add button to add a Command Parameter
  • In the details pane for the created Command Parameter
    • ID: contribution.id
    • Name: ID
    • Optional: unchecked
  • Select the previously created Command in the tree and add an additional Command Parameter
  • In the details pane for the created Command Parameter
    • ID: contribution.type
    • Name: Type
    • Optional: unchecked

Handler Contribution

  • Select Model Fragments in the tree on the left side of the Eclipse Model Editor
  • Click the Add button on the detail pane on the right side (can also be done via context menu in the tree)
  • In the details pane for the created Model Fragment set
    • Extended Element-ID: xpath:/
    • Feature Name: handlers
    • Click the Add button to add a new Handler
  • In the details pane for the created Handler
    • Command: File Navigator Action
    • Click on Class URI to open the wizard for the creation of the handler implementation
      • Package: org.fipro.contribution.integration
      • Name: FileNavigatorActionHandler
      • Click Finish

Menu Contribution

  • Select Model Fragments in the tree on the left side of the Eclipse Model Editor
  • Click the Add button on the detail pane on the right side (can also be done via context menu in the tree)
  • In the details pane for the created Model Fragment set
    • Extended Element-ID: xpath:/
    • Feature Name: menuContributions
    • Click the Add button to add a new MenuContribution
  • In the details pane for the created MenuContribution
    • Parent-ID: popup
    • Position: after=additions
    • Select Menu in the dropdown and click the Add button to add a Menu
  • In the details pane for the created Menu
    • Label: Navigator Contributions
    • Visible-When Expression: ImperativeExpression
    • Select Dynamic Menu Contribution in the dropdown and click the Add button
  • In the details pane for the created Dynamic Menu Contribution
    • Click on Class URI to open the wizard for the creation of the implementation
      • Package: org.fipro.contribution.integration
      • Name: DynamicMenuContribution
      • Click Finish
  • Select the Imperative Expression of the Menu in the tree pane
    • Click on Class URI to open the wizard for the creation of the implementation
      • Package: org.fipro.contribution.integration
      • Name: ResourceExpression
      • Click Finish

After the above steps the model fragment is prepared for the contributions and the corresponding classes are generated. The next step is to implement the Imperative Expression, the Handler and the Dynamic Menu Contribution.

Imperative Expression

Imperative Expressions are the replacement for Core Expressions if you want to rely on plain Eclipse 4 without the plugin.xml. Using Imperative Expressions you have the option to implement an expression rather than describing it in an XML format. As in my opinion the definition of a Core Expression in the plugin.xml was never really intuitive, I really like the Imperative Expression in Eclipse 4. You might want to discuss that the declarative way of the Core Expressions is more powerful, but actually I have not yet found a case where an Imperative Expression is not suitable as a replacement.

The following code shows the implementation of the ResourceExpression that checks if a single element is selected and that element is an IResource and there is at least one contribution service registered for that type.

public class ResourceExpression {
	
    @Evaluate
    public boolean evaluate(
        @Optional @Named(IServiceConstants.ACTIVE_SELECTION)
        IStructuredSelection selection,
        @Service
        NavigatorMenuContributionRegistry registry) {
		
        return (selection != null && selection.size() == 1
            && (selection.getFirstElement() instanceof IResource)
            && !registry.getServices(
                   selection.getFirstElement().getClass()).isEmpty());
    }
}

Dynamic Menu Contribution

The Dynamic Menu Contribution implementation takes the selected element and tries to retrieve the registered contribution services from the registry. If services for the selected type are registered it creates the menu items that should be added to the context menu.

public class DynamicMenuContribution {
	
    @AboutToShow
    public void aboutToShow(
        List<MMenuElement> items, 
        EModelService modelService,
        MApplication app,
        @Service NavigatorMenuContributionRegistry registry,
        @Named(IServiceConstants.ACTIVE_SELECTION) IStructuredSelection selection) {
		
        List<NavigatorMenuContributionWrapper> services = 
            registry.getServices(selection.getFirstElement().getClass());

        services.forEach(s -> {
            MHandledMenuItem menuItem =
                MMenuFactory.INSTANCE.createHandledMenuItem();
            menuItem.setLabel(s.getName());
            menuItem.setTooltip(s.getDescription());
            menuItem.setElementId(s.getId());
            menuItem.setContributorURI(
                "platform:/plugin/org.fipro.contribution.integration");

            List<MCommand> command = modelService.findElements(
                app, 
                "org.fipro.contribution.integration.command.filenavigatoraction", 
                MCommand.class);
                menuItem.setCommand(command.get(0));

            MParameter parameter = MCommandsFactory.INSTANCE.createParameter();
            parameter.setName("contribution.id");
            parameter.setValue(s.getId());
            menuItem.getParameters().add(parameter);

            parameter = MCommandsFactory.INSTANCE.createParameter();
            parameter.setName("contribution.type");
            parameter.setValue(s.getType());
            menuItem.getParameters().add(parameter);

            items.add(menuItem);
        });
    }
}

Handler

The handler is triggered by selecting the generated menu item and therefore gets the provided command parameters. It is then using the ContextInjectionFactory to execute the method annotated with @PostConstruct in the service instance. The following code shows how this could look like.

public class FileNavigatorActionHandler {
	
    @Execute
    public void execute(
        @Named("contribution.type") String type,
        @Named("contribution.id") String id,
        @Named(IServiceConstants.ACTIVE_SELECTION) IStructuredSelection selection,
        @Service NavigatorMenuContributionRegistry registry,
        IEclipseContext context) {
		
        NavigatorMenuContributionWrapper wrapper =
            registry.getService(type, id);

        if (wrapper != null) {

            IEclipseContext activeContext = 
                context.createChild(type + " NavigatorMenuContribution");
            activeContext.set(wrapper.getType(), selection.getFirstElement());

            try {
                ContextInjectionFactory.invoke(
                    wrapper.getServiceInstance(),
                    PostConstruct.class,
                    activeContext);
            } finally {
                // dispose the context after the execution to avoid memory leaks
                activeContext.dispose();
            }
        }
    }
}

Step 7: Testing

Let’s verify if everything works as intended. For this simply right click on one of the projects and select
Run As -> Eclipse Application

This will start an Eclipse IDE that has the plug-ins from the workspace installed.

In the newly opened Eclipse instance create a new project. In that project create a directory and a file. If you right click on the created directory, you should not see any additional menu entry. But on performing a right click on the created file, you should find the menu entry Navigator Contributions, which is a sub-menu that contains the File Size entry. Selecting that should open a dialog that shows the size of the selected file. Hovering the File Size menu entry should also open the tooltip with the description that is provided via service property.

Note:
For this example use a simple text file. Creating for example a Java source file will not work, as a Java source file is a CompilationUnit, which is not an IResource.

Step 8: Extending the example

Now let’s extend the example and contribute some more features to verify if the Extended Contribution Pattern works.

  • Switch to the Plug-in Perspective
  • Create a new Plug-in Project via File -> New -> Plug-in Project
  • Choose a meaningful name (e.g. org.fipro.contribution.extended)
  • Ensure that Generate an activator is unchecked
  • Ensure that This plug-in will make contributions to the UI is checked
  • Set Create a rich client application? is set to No
  • Open the META-INF/MANIFEST.MF file
  • Switch to the Dependencies tab and add the following packages to the Imported Packages
    • javax.annotation
      Needed for the @PostConstruct annotation
    • org.fipro.contribution.integration (1.0.0)
      Needed for the previously created marker interface
    • org.osgi.service.component.annotations [1.3.0,2.0.0) optional
      Needed for the OSGi DS annotations
  • Add the following plug-ins to the Required Plug-ins section
    • org.eclipse.jface
      Needed for showing dialogs
    • org.eclipse.core.resources
      Needed for the Eclipse Core Resources API to access the Eclipse resources
    • org.eclipse.core.runtime
      Needed as transitive dependency for operating on the resources
  • Create another contribution for IFile handling, e.g. FileCopyContribution as shown below
@Component(property = {
    "name = File Copy", 
    "description = Create a copy of the selected file" })
public class FileCopyContribution implements NavigatorMenuContribution {

    @PostConstruct
    public void copyFile(IFile file, Shell shell) {
        URI uri = file.getRawLocationURI();
        Path path = Paths.get(uri);
        Path toPath = Paths.get(
            path.getParent().toString(), 
            "CopyOf_" + file.getName());
        try {
            Files.copy(path, toPath);
			
            // refresh the navigator
            file.getParent().refreshLocal(IResource.DEPTH_INFINITE, null);
        } catch (IOException | CoreException e) {
            MessageDialog.openError(
                shell, 
                "Failed to copy the file size", 
                "Exception occured on copying the file: "
                    + e.getLocalizedMessage());
        }
    }
}
  • Create a contribution to operate on an IFolder, e.g. FolderContentContribution as shown below
@Component(property = {
    "name = Folder Content",
    "description = Show the number of files in the selected folder" })
public class FolderContentContribution implements NavigatorMenuContribution {

    @PostConstruct
    public void showFolderContent(IFolder folder, Shell shell) {
        URI uri = folder.getRawLocationURI();
        Path path = Paths.get(uri);
        try {
            long count = Files.list(path).count();
            MessageDialog.openInformation(
                shell,
                "Folder Content",
                String.format("The folder contains %d files", count));
        } catch (IOException e) {
            MessageDialog.openError(
                shell, 
                "Failed to retrieve the folder content", 
                "Exception occured on retrieving the folder content: "
                    + e.getLocalizedMessage());
        }
    }
}

If you start the application again like before, you will see an additional menu entry in the context menu for a file, and there is now even a menu entry in the context menu of a folder.

OSGi Dynamics

A nice side effect is that the solution supports OSGi dynamics. That means a contribution can come and go at runtime without the need to restart the Eclipse IDE. To verify this, open the Host OSGi Console (open the Console view and switch to the Host OSGi Console via the view menu).

Enter the following command to find the id of the org.fipro.contribution.extended bundle:

lb fipro

Then stop that bundle via

stop <id>

The console looks like this in my environment as an example:

osgi> lb fipro
START LEVEL 6
ID|State |Level|Name
649|Active | 4|Service (1.0.0.qualifier)|1.0.0.qualifier
685|Active | 4|Integration Layer (1.0.0.qualifier)|1.0.0.qualifier
689|Active | 4|Extended (1.0.0.qualifier)|1.0.0.qualifier

osgi> stop 689

Now verify that the menu contributions for the folder and the File Copy menu entry are gone. If you start the bundle again via start <id> the menu entries are available again.

Maybe it is only me that is so excited about that, but supporting OSGi Dynamics more and more in the Eclipse IDE itself feels good.

Conclusion

With the Extended Contribution Pattern it is possible to create a framework that eases the collaboration between heterogeneous organisations. While you only need a few people that manage the Integration Layer and therefore know about Eclipse Platform details, every developer in the collaboration is able to contribute a functionality. As you can see above, the implementation of a contribution service is simple in terms of integration. This is by the way similar to how popular web frameworks are designed.

As I said in the introduction, the APP4MC project uses the Extended Contribution Pattern in various places. We have implemented a Model Visualization that shows a visualization of a selected AMALTHEA Model element, e.g. via JavaFX, PlantUML or plain SWT. You can get some more details in the APP4MC Online Help.

APP4MC 2.0 will also include context sensitive actions on selected AMALTHEA Model elements. So it is possible to contribute processing actions for a selected model element or actions to create model elements in a selected model element container.

You can also see that the combination of OSGi Declarative Services and the Eclipse 4 programming model brings a lot of benefits. And there was quite some progress over the last years to improve this. Actually the implementation and usage of OSGi services becomes really usable with the usage of the Eclipse 4 programming model, as you can easily consume services via injection (note the @Service annotation). The only thing to remember is that the PROTOTYPE scope is not yet supported in the Eclipse injection, which means the services are single instances, which blocks you from using states in your services for the Extended Contribution Pattern.

Finally some words about Eclipse 3.x vs. Eclipse 4.x. As an Eclipse Platform committer I am used to the Eclipse 4 programming model for several years. Since 2015 I published articles about the migration from Eclipse 3 to Eclipse 4 and talked about that topic on conferences. But still people rely on the Eclipse 3 programming model and ask questions about Eclipse 4 migrations. IMHO there are several reasons why Eclipse 3 is still active in so many places:

  1. The Eclipse IDE itself as one of the biggest Eclipse Platform based products is based on Eclipse 3. Well, actually it is based on Eclipse 4, but still most of the plugins and biggest projects in that area are Eclipse 3 based (e.g. Navigator views, JDT, CDT, PDE, and so on), so there is the Compatibility Layer in place to support the backwards compatibility. As long as the Eclipse IDE itself and most of the major projects/plugins are based on Eclipse 3, a complete shift of projects that extend the IDE to Eclipse 4 will never happen.
  2. There are still more tutorials in the web that show how to do things with Eclipse 3 than for Eclipse 4. There are quite some Eclipse 4 related tutorials out there and people like for example Lars Vogel, Olivier Prouvost, Jonas Helming and myself published several of those. But it doesn’t seem to be enough for the overall community.
  3. There is more tooling available around Eclipse 3 (e.g. number of wizards) than for Eclipse 4. to be honest I am probably not the target audience for wizards as I am mostly faster in creating plugins without an example created by a wizard. And once familiar with Eclipse 4, which is much simpler than the Eclipse 3 programming model, the existing wizards and tools are really sufficient.

In this blog post you should have noticed that it is possible and even not complicated to extend an Eclipse 3.x based application like the Eclipse IDE with plain Eclipse 4.x mechanisms. If you look at techniques like Imperative Expressions and contribution of model fragments via manifest header, your contributing bundle does not contain a single Eclipse 3.x mechanism like extension points and the corresponding plugin.xml file.

This also means, if you still ask yourself if you should migrate from Eclipse 3.x to Eclipse 4.x, just give it a try. Start with a small part and test what you can do. A migration scenario is not a “big bang”, you can do it incrementally. And remember, you probably won’t be able to get rid of everything, e.g. file based editors linked to the navigators, but you can improve in several spots in your project.

Here are some useful links to previous blog posts if you are not yet familiar with all the topics included here:

The sources of this blog post can be found here.

About Dirk Fauth

Dirk Fauth is a Software Architect for Rich Client Systems working for the Robert Bosch GmbH in Stuttgart and a lecturer in Java basics for the Baden-Wuerttemberg Cooperative State University (DHBW). He is active in developing, teaching and talking about OSGi, Eclipse RCP applications and Eclipse related technologies. He is project lead of the Nebula NatTable project, Eclipse Platform committer and also a committer and contributor to several other Eclipse projects. (Twitter: @fipro78)
This entry was posted in Dirk Fauth, Eclipse, Java, OSGi, Other. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *