Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

How can I configure component imports as optional

Thorsten Kamann January 31, 2012

Hello,

I have a little (or not so little?) problem. I develop a plugin using components from the Subversion and Git plugin. But often only one of these plugins are installed. How can I manage this? If I configure both component import the start of my plugin fails if one of both plugins are missing? What is the best strategy for this?

Thanks

Thorsten

6 answers

1 accepted

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

3 votes
Answer accepted
JohnA
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
November 8, 2012

I'd highly recommend looking through Ben Woskow's Optional Service sample plugin:

https://bitbucket.org/bwoskow/optional-service-sample-plugin

Jens Rutschmann _K15t_
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
November 8, 2012

I've been working with this example as well and finally experienced some issues wrt. to the sequence that calling and called plugins were installed.

Threre was a discussion in the 'atlassian-marketplace' google group with all the details in mid of July.

Basically the problem was that the calling plugin always ran into a ClassNotfoundException if it was installed before the called plugin.

Do you know of any fixes related to this ?

Don Brown [Atlassian]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
November 9, 2012

Exactly. Optional imports are optionally resolved at resolution time, but never after that. Alternatively, you could use DynamicImport-Package, which will resolve the class at runtime. However, that has a performance impact and can be tricky to follow as classes come and go, requiring special handling of references.

Tim August 2, 2013

Sorry to bring an old question back from the dead, but I have been trying to use this optional service code.

I'm using it to optionally load in a MetadataManager from the randombits.org confluence-metadata plugin (to read in some Scaffolding data).

It works ok, but there is one problem. When I uninstall the metadata plugin my plugins get "half disabled" (by which I mean when I navigate to a page which uses one of my macros, that tries to read Scaffolding data, it displays an error message about a "ServiceProxyDestroyedException" instead of "Unknown macro").

If I then re-enable my plugins and the error persists.

I have to then disable and once again re-enable my plugins to get the error to go away and make my macro work again.

So basically:
Uninstall metadata plugin -> my plugin is automatically disabled -> enable my plugin -> disable my plugin -> enable my plugin

I can automate the process with an EventListener for the PluginUninstallEvent. In this listener I use an injected PluginController to disable and then enable my plugins.

This feels messy to me ... is there a better way to fix this issue?

4 votes
Don Brown [Atlassian]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
January 31, 2012

This can get tricky. You can't use component-import here, so either go down to Spring XML configuration or you can use OSGi apis directly. I tend to favor OSGI's ServiceTracker. It gets even more interesting if the class that provides that component may or may not be available. In this case, you have to use a combination of dynamic package imports, factory beans to switch implementations based on class loading, or if you need even more dynamic behavior, a bundle listener that allows dynamic switching of a service impl to one that uses the service exposed from the bundle.

This is more an overview answer, so if you need specifics on any of those approaches, just ask.

RDK February 1, 2012

Yes, please. I'm specifically interested in the case where "class that provides that component may or may not be available".

Thorsten Kamann February 2, 2012

Hello Don,

can we skype about these? I would blog about the result:)

3 votes
Don Brown [Atlassian]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
March 3, 2012

Ok, here we go:

Step 1 - Import the packages dynamically

Declare the optional classes with DynamicImport-Package in your <instructions> element in your pom like so (yes, you can use wildcards):

&lt;instructions&gt;
  &lt;DynamicImport-Package&gt;com.some.package.*&lt;/DynamicImport-Package&gt;
&lt;/instructions&gt;

This will tell OSGi to look up that package in currently running bundles everytime it does a lookup. The usual way to solve this is making a package import optional (foo.bar;resolution:=optional), but that doesn't work when the dependency plugin is installed after yours. The disadvantage of this approach is if the dependent plugin is upgraded, your plugin isn't restarted (and hence, rewired), so you need to be aware of this in your binding code (below).

Step 2 - Create an abstracting interface

Isolate all the functionality you need of the dependency plugin into a neutral interface with no class references. For example:

public interface GitAccessor
{
  Iterable&lt;MyGitDto&gt; getCommits(String branch);
}
In addition to the interface, create a no-op implementation.

Step 3 - Create a delegating implementation

Now the fun begins. Create a delegating implementation of this accessor that holds a volatile reference to either the no-op impl or the an implementation that uses classes and services from the git plugin (for example). This class is responsible for switching the volatile impl in response to events from the dependent plugin's service. Here is an example in psuedocode:

public class DelegatingGitAccessor implements DisposableBean {
  private volatile GitAccessor delegate;
  private final ServiceTracker tracker;

  public DelegatingGitAccessor(BundleContext ctx) {
    tracker = new ServiceTracker("com.otherplugin.Service", new ServiceTrackerCustomizer() {
      public void adding(...) {
        Service service = // get the service 
        delegate = new ActualGitAccessor(service);
      }
      public void removing(...) {
        delegate = new NoOpGitAccessor();
      }
// more methods
    });
    tracker.open();
  }

  public void dispose() {
    tracker.close();
  }
}
The key here is this delegate impl should have no references to the optional plugin, not even in the imports.

Step 4 - Create the actual implementation

Create the actual GitAccessor implementation, which from the above example, means you accept the actual referenced service in the constructor and implement normally.

Step 5 - Register the delegating impl as a component

Finally, register the delegating impl as a normal component and reference in other components as normal.

Rui Rodrigues
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
January 20, 2015

Hi, I need to do something like that, but when I'm try to get the service (inside of addingservice method) a java.lang.ClassNotFoundException is thrown. In my pom.xml there is a dependency of the other plugin with scope= provided. This problem only occurs when the other plugin is being installed. Any suggestion?

2 votes
Efim
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
December 27, 2018

HI all,

I've recently implemented a solution to similar problem when an external plugin X is trying to do optional component-import to the bundled Confluence plugin (in my case it was mywork plugin). The problem I was trying to solve is to make the plugin X work in both cases -- either when mywork is disabled or enabled. In case mywork is enabled, plugin X used the service from mywork to post notifications. When mywork is disabled, plugin just did nothing. This couldn't be done with regular component-import because in this case the plugin X will just not start if mywork is disabled.

You can see the solution as a simple proof-of-concept plugin here. I'll explain different parts below.

1) Keeping the plugin enabled when mywork is disabled

This can be achieved by using optional OSGi imports. Basically, this is done by putting explicit import to mywork in the pom.xml. If you build the plugin and look at MANIFEST.MF, you should see that all imports of mywork packages are optional. After you do this step, disabling mywork plugin no longer will transitively disable plugin X.

2) Injecting bean reference in dynamic way

To do this, we need to use spring xml instead of atlassian-plugin.xml. This can be done by putting spring descriptor xml file to resources/META-INF/spring/plugin-context.xml. It will complement atlassian-plugin.xml definitions. This osgi:reference trick can be used to inject OSGi proxy into constructor. It works in both cases -- when dependency is satisfied and when it is not.

3) Checking if the dependency can be used

After injection via osgi:reference you can try to call any method on the target bean and watch for exceptions. Example code can be found here. If toString method succeeds, you know that the bean is available and can be safely used. On the other hand, if toString method fails, bean shouldn't be accessed.

4) Limitation of this approach

The best thing about this solution -- it will always keep plugin X up and if the method isNotificationsServiceAvailable returns true, you can be certain that the service can be used. However, there are false negatives in this solution. For example, if you start the server and disable mywork plugin, injected OSGi reference will stop working, which is expected. But if you enable mywork once again, the link will remain unfunctional until the server is restarted. I haven't found a way to overcome this problem. Sometimes this is not a showstopper and can be tolerated.

5) Other problems you might encounter

Other important limitation of the solution is that it tries to inject bundled plugin, which class should be always available (because bundled plugin can't be uninstalled). I believe if the same approach was used for external plugin, we might need to add another layer which will guard us from NoClassDefFoundErrors. I haven't implemented this, but here are couple of thoughts about the problem. Prior to using osgi:reference we might want to explicitly try to call Class#forName and understand if the class present in the system. This should be done before using any references to the class. Also we might want to load the wrapping bean with <bean lazy-init="true" ... in order to not fail eagerly on class unavailability. Another possible solution will be to obtain bean reference always as Object and use reflection to call the methods.

Hope this will be helpful for someone to start moving in the right direction.

0 votes
Thorsten Kamann February 2, 2012

Here are my reqs:

I want to develop a plugin that checks the following things:

  • Get the last checkin date from the VCS (Subversion and/or Git)
  • Retrieve the last successful build date from the buildsystem (Bamboo or Hudson/Jenkins)
  • Compare both dates. If the last successfull build is newer than the newest checkin the user can resolve a ticket. Otherwise not.

For this plugin I need to support Subversion, Git, Bamboo, Hudson/Jenkins. But I don't want to redevelop all things, because with existings plugins all needed informations already exists. What I need is a function to check what plugins exists. If this work I need only develop one validator/condition.

0 votes
JamieA
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
January 31, 2012
Thorsten Kamann January 31, 2012

This I have done already. But I need a component-import too. Without there are Spring errors about missing dependencies. The missing dependencies are the component from the component-import element.

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

TAGS
AUG Leaders

Atlassian Community Events