I’ve seen this question come through here a couple of times before (or very similar questions):
“I want to use SpecialService but it’s only available in product version 7.6 and I need to to support 7.2+” .
========================================================================
Note: After posting this - @scott suggested an alternate approach which is a lot simpler - scroll down to Supporting older java api's when building for new versions - #4 by scott for it.
========================================================================
Usually the solution proposed is to use ComponentAccessor (shudder) to get hold of things dynamically and then use reflection (shudder again) to get hold of objects and build your interaction out with that. The other approach is to branch your code and basically have 2 plugins (shudder). I’ve done both in the past and in the end you either end up with a very hard reading piece of code OR really crappy user experience: "You just upgrade - yeah you need to upgrade
your app as well from 3.0-72 to 3.0-76).
So let me start of with the customary disclaimer - this works for me - I hope it works for you.
Jira “recently” added support for PrioritySchemes which kinda made this flare up again for me. So I thought I’d take a different stab at it ( I’ve published a sample repo of this at Bitbucket if you want to follow along).
Oh and before I go any further - this approach is basically built around the plugin that was published at https://bitbucket.org/bwoskow/optional-service-sample-plugin to show how to import a service from another app that may or may not be installed (So thank you to @bwoskow) . I made some very minor changes to it for looking internally at the host app and generally tried to figure out the magic he performed.
Let’s get started
We’ll start in the pom.xml ( Bitbucket )with the dependencies. PrioritySchemeManager came into Jira around 7.7 - so we’ll need to point the api we’re targeting to be that. As long as we make ourself have a gentle person’s agreement with ourselves only to use api’s that are in 7.2 - we should be good, so bump the jira version: <jira.version>7.7.1</jira.version>
.
Then we’re going to have ‘fun’ with Osgi - so there are 2 dependencies we’ll need to add:
<dependency>
<groupId>com.atlassian.plugins</groupId>
<artifactId>atlassian-plugins-osgi</artifactId>
<version>4.5.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.osgi</groupId>
<artifactId>spring-osgi-core</artifactId>
<version>1.1.3</version>
<scope>provided</scope>
</dependency>
This will let us interact with the Osgi Services appropriately.
Let’s do some coding
All of this code is at Bitbucket
First of we’ll create our magical interaction with Osgi stuff (Again - this is directly from @bwoskow’s sample):
public abstract class OptionalService<T> implements InitializingBean, DisposableBean
{
private final Class<T> type;
private final ServiceTracker tracker;
public OptionalService(final BundleContext bundleContext, final Class<T> type)
{
this.type = checkNotNull(type, "type");
this.tracker = new ServiceTracker(bundleContext, type.getName(), null);
}
/**
* Returns the service (of type {@code T}) if it exists, or {@code null} if not
* @return the service (of type {@code T}) if it exists, or {@code null} if not
*/
protected final T getService()
{
return type.cast(tracker.getService());
}
@Override
public void afterPropertiesSet() throws Exception
{
tracker.open();
}
@Override
public final void destroy()
{
tracker.close();
}
}
Basically what this does is to use osgi magic to look up a service (as well as open up a tracker that will get released when we are done).
We then extend this OptionalService in PrioritySchemeServiceFactory:
public class PrioritySchemeServiceFactory extends OptionalService<PrioritySchemeManager>
{
public PrioritySchemeServiceFactory(final BundleContext bundleContext)
{
super(bundleContext, PrioritySchemeManager.class);
}
public ProxyPrioritySchemeManager get()
{
PrioritySchemeManager prioritySchemeManager = getService();
return new ProxyPrioritySchemeManagerImpl( prioritySchemeManager);
}
}
At this point we can get hold of a wrapping proxy object called ProxyPrioritySchemeManager which just has the real PrioritySchemeManager in it. We’ll come back to this object in a bit.
We need a real “service” that we can inject into things. For this we’ll create an interface (we could since we’re using Spring Scanner skip this and just flatten things down - but in case there’s somebody not using spring scanner - we’ll go with the interface:
public interface PrioritySchemeAccessor
{
public ProxyPrioritySchemeManager getPrioritySchemeManager();
}
We implement this in PrioritySchemeAccessorImpl:
@Component
public class PrioritySchemeAccessorImpl implements PrioritySchemeAccessor
{
private static final Logger log = LoggerFactory.getLogger(PrioritySchemeAccessorImpl.class);
private final ApplicationContext applicationContext;
private ProxyPrioritySchemeManager proxyPrioritySchemeManager;
@Inject
public PrioritySchemeAccessorImpl(ApplicationContext applicationContext)
{
this.applicationContext = checkNotNull(applicationContext, "applicationContext");
}
@Override
public ProxyPrioritySchemeManager getPrioritySchemeManager()
{
if( proxyPrioritySchemeManager==null)
{
initProxyPrioritySchemeManager();
}
return proxyPrioritySchemeManager;
}
private void initProxyPrioritySchemeManager()
{
try
{
Class<?> prioritySchemeServiceFactoryClass = getWittifiedPrioritySchemeManagerServiceFactoryClass();
if (prioritySchemeServiceFactoryClass != null)
{
this.proxyPrioritySchemeManager = ((PrioritySchemeServiceFactory)applicationContext.getAutowireCapableBeanFactory().
createBean(prioritySchemeServiceFactoryClass, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)).get();
}
}
catch (Exception e)
{
log.error("Could not create PrioritySchemeServiceFactory", e);
}
if( this.proxyPrioritySchemeManager==null)
{
log.info("Proxy failed. Going with NoOp");
this.proxyPrioritySchemeManager = new NoOpProxyPrioritySchemeManager();
}
}
private Class<?> getWittifiedPrioritySchemeManagerServiceFactoryClass()
{
try
{
getClass().getClassLoader().loadClass("com.atlassian.jira.issue.fields.config.manager.PrioritySchemeManager");
return getClass().getClassLoader().loadClass("demo.compat.PrioritySchemeServiceFactory");
}
catch (Exception e)
{
return null;
}
}
}
You’ll notice that this is the only class that we’re doing @Component
(and @Inject
). This is intentional. This is the service that we’re going to pass around in our app in a bit. We inject spring’s ApplicationContext so we can use this later.
Now when we call getPrioritySchemeManager - we check to see if we already have a proxyPrioritySchemeManager object. If we do - we return it. If we don’t then we initialize it and then return it. This way we’re not being a performance hog each time.
Inside initProxyPrioritySchemeManager()
we call getPrioritySchemeManagerServiceFactoryClass()
which looks up to see if the PrioritySchemeManager class is available on our classpath. If it is then we return the class of demo.compat.PrioritySchemeServiceFactory
. If we can get it - we return back a null object.
Time for the magic
If getPrioritySchemeManagerServiceFactoryClass()
returns back in a String then we know that PrioritySchemeManager
exists. If it returns back a null - we know it doesn’t. If it does exists, we pass the string of the class to the applicationContext and instantiate the PrioritySchemeServiceFactory
object and then call get() on it. This will then return us the wrapping PrioritySchemeManager
wrapping class ( ProxyPrioritySchemeManagerImpl
).
If the string though is null (i.e. PrioritySchemeManager doesn’t exists) - we create a NoOpProxyPrioritySchemeManager
object.
Both the NoOpProxyPrioritySchemeManager and ProxyPrioritySchemeManagerImpl uses an interface ProxyPrioritySchemeManager
. In here we implement all of our methods that we want to use of the PrioritySchemeManager:
public interface ProxyPrioritySchemeManager
{
public boolean hasPrioritySchemes();
public List<FieldConfigScheme> getAllSchemes();
}
Now I added hasPrioritySchemes()
for ease of use - this way we can have a simple boolean to tell us what type of world we live in - do we have priorities or not (awesome for conditions). We don’t have to define all of the methods on the PrioritySchemeManager - just the ones we need.
Then when we do the implementation of these - we do it twice. First in the NoOpProxyPrioritySchemeManager
:
public class NoOpProxyPrioritySchemeManager implements ProxyPrioritySchemeManager
{
public List<FieldConfigScheme> getAllSchemes()
{
return new ArrayList<>();
}
public boolean hasPrioritySchemes()
{
return false;
}
}
And then in the ProxyPrioritySchemeManagerImpl
public class ProxyPrioritySchemeManagerImpl implements ProxyPrioritySchemeManager
{
private final PrioritySchemeManager prioritySchemeManager;
public ProxyPrioritySchemeManagerImpl(PrioritySchemeManager prioritySchemeManager)
{
this.prioritySchemeManager = checkNotNull(prioritySchemeManager, "prioritySchemeManager");
}
public boolean hasPrioritySchemes()
{
return true;
}
public List<FieldConfigScheme> getAllSchemes()
{
return this.prioritySchemeManager.getAllSchemes();
}
}
The ProxyPrioritySchemeManagerImpl doesn’t really have any magic to it - it’s just calling the prioritySchemeManager directly.
Actually making use of this
For this plugin I created a servlet at Bitbucket
@Scanned
public class DemoServlet extends HttpServlet
{
private final PrioritySchemeAccessor prioritySchemeAccessor;
@Inject
public DemoServlet(PrioritySchemeAccessor prioritySchemeAccessor)
{
this.prioritySchemeAccessor = prioritySchemeAccessor;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
resp.setContentType("text/plain");
PrintWriter writer = resp.getWriter();
ProxyPrioritySchemeManager proxyPrioritySchemeManager = prioritySchemeAccessor.getPrioritySchemeManager();
if( proxyPrioritySchemeManager.hasPrioritySchemes())
{
writer.write("This version does have priority schemes!");
writer.write("\n");
writer.write("The following schemes exist:");
for(FieldConfigScheme fieldConfigScheme: proxyPrioritySchemeManager.getAllSchemes())
{
writer.write( fieldConfigScheme.getName());
writer.write("\n");
}
}
else
{
writer.write("This Jira version does not have priority schemes");
}
writer.close();
}
}
It’s @Scanned
and @Inject
so we can get the PrioritySchemeAccessor object. Then inside the doGet(…) we call
ProxyPrioritySchemeManager proxyPrioritySchemeManager = prioritySchemeAccessor.getPrioritySchemeManager();
At this point from this code - there is no "magic. ProxyPrioritySchemeManager will either call our NoOp implementation or the proxying one. We either get a list of the priority schemes or that there isn’t support for it.
There is still some clean up that could be on this code (refactoring so we flatten down the service factory look up, etc) but for a sample this works pretty well. Also, make sure to perform the appropriate regression tests so that you don’t run into any weirdness because you’re using a higher api version (it could happen - it hasn’t happened to me – yet ).
Note: Don’t use this for app dependencies as is
While you could in theory use this code to attach to other app’s exported services - remember they’ll come and go (if a host app’s service comes and goes - then we have a problem ). For this @bwoskow’s code is definitively the route to go (or add in PluginEnabled/PluginDisabled event awareness).
Did I thank @bwoskow for his code sample yet?