Writing a Plugin Upgrade Task for JIRA 4.0

Still need help?

The Atlassian Community is here for you.

Ask the community

Overview

JIRA 4.0 will introduce a new dashboard, effectively making the Portlet Plugin Module​ obsolete. Legacy portlets will still be supported via a Legacy Gadget bridge; however, they will miss out on a lot of the new features that gadgets offer (e.g. the ability to share gadgets with other apps such as iGoogle). It therefore makes sense to convert portlets over to gadgets. Information about how to write a gadget can be found in the Gadget Development Hub​, and specifically the page about gadgets and JIRA portlets​.

If you've converted a portlet to a gadget, you will most likely need an upgrade task to convert existing data of your users into the new format used by the gadget you have written. This page describes the process of creating such an upgrade task.

Why an upgrade task?

Portlets generally have some configuration data associated with them by their users. For example, the First Response Time chart portlet, available in the Charting Plugin, allows users to configure how many days previous to draw the chart for (among other things). For efficiency reasons, gadgets do not use the same storage mechanism as portlets do to store these user preferences. An upgrade task is thus needed to convert existing user data over to the new format required by the new gadget.

Upgrade framework

JIRA 4.0 introduces a new plugin framework (version 2.2 or later of the Atlassian Plugin Framework, affectionately known as 'Plugins2'), which provides an events system that lets plugins register to listen for certain events (such as a 'Framework started' event). JIRA 4.0 also bundles SAL, which already includes a plugin upgrade framework. SAL provides a plugin upgrade manager that listens for the 'Framework started' event and will look for Plugin Upgrade Tasks to run in order to upgrade data for plugins.

What does all this mean? Effectively, plugin writers don't have to worry about providing an upgrade task framework. They can simply provide a Plugin Upgrade Task component and SAL will guarantee that their upgrade task is run on startup.

Example

Let's look at what needs to be done to run an upgrade task to convert the First Response Time chart portlet data over to gadget data.

1. Convert your Portlet to a Gadget

Please follow the documentation available in the Gadget Development Hub​ for this step, and specifically the page about gadgets and JIRA portlets​.

2. Add dependency on SAL

First we'll need access to the SAL API in the charting plugin project. Add the following dependency to the plugin's pom.xml:

pom.xml
...
  <dependency>
     <groupId>com.atlassian.sal</groupId>
     <artifactId>sal-api</artifactId>
     <version>2.0.17</version>
     <scope>provided</scope>
  </dependency>
...

Re-generate your IDE's project descriptor (mvn idea:idea or mvn eclipse:eclipse) after this step to allow you to access the new SAL API classes in your project.

3. Convert your plugin to Plugins2

SAL is a Plugins2 bundle and your plugin will have to be converted to the Plugins2 format first before you can write an upgrade task that will be picked up by the PluginUpgradeManager. Gadgets are also only supported in Plugins2 bundles.

There are generic instructions available for how to do this, but let's look specifically at the Charting plugin example. The only thing that is needed is to add the plugins-version="2" attribute in atlassian-plugins.xml:

atlassian-plugins.xml
<atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" system="true" plugins-version="2">
...

4. Writing your upgrade task

Now that all the prerequisites are done, the Upgrade task for the plugin can be written. This class simply needs to implement the PluginUpgradeTask interface provided by SAL.

Here's an example implementation:

GadgetUpgradeTask.java

package com.atlassian.jira.ext.charting.upgrade;

import com.atlassian.configurable.ObjectConfigurationException;
import com.atlassian.gadgets.dashboard.Color;
import com.atlassian.jira.ComponentManager;
import com.atlassian.jira.portal.OfbizPortletConfigurationStore;
import com.atlassian.jira.portal.PortletConfiguration;
import com.atlassian.jira.portal.PortletConfigurationImpl;
import com.atlassian.jira.portal.PortletConfigurationStore;
import com.atlassian.jira.propertyset.JiraPropertySetFactory;
import com.atlassian.jira.upgrade.util.SimpleLegacyPortletUpgradeTask;
import com.atlassian.jira.util.Consumer;
import com.atlassian.jira.util.NotNull;
import com.atlassian.jira.util.collect.EnclosedIterable;
import com.atlassian.sal.api.message.Message;
import com.atlassian.sal.api.upgrade.PluginUpgradeTask;
import com.opensymphony.module.propertyset.PropertySet;
import org.apache.log4j.Logger;

import java.net.URI;
import java.util.Collection;
import java.util.Map;

public class GadgetUpgradeTask implements PluginUpgradeTask
{
    private static final Logger log = Logger.getLogger(GadgetUpgradeTask.class);

    private final PortletConfigurationStore portletConfigurationStore;
    private final JiraPropertySetFactory propertySetFactory;

    public GadgetUpgradeTask(JiraPropertySetFactory propertySetFactory)
    {
        //NOTE: Can't get the portletConfigStore injected here since it is not made available to plugins2
        this.portletConfigurationStore = ComponentManager.getComponentInstanceOfType(PortletConfigurationStore.class);
        this.propertySetFactory = propertySetFactory;
    }

    /**
     * The build number for this upgrade task. Once this upgrade task has run the plugin manager will store this
     * build number against this plugin type.  After this only upgrade tasks with higher build numbers will be run
     */
    public int getBuildNumber()
    {
        return 1;
    }

    public String getShortDescription()
    {
        return "Upgrades legacy portlet configuration to new gadget user prefs.";
    }

    public Collection<Message> doUpgrade() throws Exception
    {
        final SimpleLegacyPortletUpgradeTask upgradeTask =
                new SimpleLegacyPortletUpgradeTask("com.atlassian.jira.ext.charting:firstresponsetime", 
                        URI.create("rest/gadgets/1.0/g/com.atlassian.jira.ext.charting:firstresponsetime/firstresponsetime.xml"));
        //First get all the portletConfigurations in the database.
        final EnclosedIterable<PortletConfiguration> iterable = portletConfigurationStore.getAllPortletConfigurations();
        iterable.foreach(new Consumer<PortletConfiguration>()
        {
            public void consume(@NotNull final PortletConfiguration pc)
            {
                //for each portletconfiguration, check if it's key matches the portlet key we want to upgrade
                if (pc.getKey() != null && pc.getKey().startsWith(upgradeTask.getPortletKey()))
                {
                    log.info("Upgrading portletconfig with id '" + pc.getId() + "'");
                    //first lets convert the preferences for this portlet to the new prefs format used for gadgets.
                    final Map<String, String> prefs;
                    try
                    {
                        prefs = upgradeTask.convertUserPrefs(pc.getProperties());
                    }
                    catch (ObjectConfigurationException e)
                    {
                        throw new RuntimeException(e);
                    }
                    //then create essentially a copy of the old portletConfig.  This new copy no longer needs to have
                    //the portletKey and propertySet set to any values.  It however does require the GadgetUri and user prefs to be set.
                    final PortletConfiguration newConfig =
                            new PortletConfigurationImpl(pc.getId(), pc.getDashboardPageId(), null, null, pc.getColumn(), pc.getRow(),
                                    null, upgradeTask.getGadgetUri(), Color.color8, prefs);
                    //Now lets store this new config back to the database.
                    portletConfigurationStore.store(newConfig);
                    //clear out the old properties for this portlet
                    removePropertySet(pc);
                }
            }
        });

        return null;
    }

    private void removePropertySet(final PortletConfiguration pc)
    {
        final PropertySet livePropertySet = propertySetFactory.buildNoncachingPropertySet(OfbizPortletConfigurationStore.TABLE, pc.getId());
        @SuppressWarnings ("unchecked")
        final Collection<String> keys = livePropertySet.getKeys();
        for (String propertyKey : keys)
        {
            livePropertySet.remove(propertyKey);
        }
    }

    /**
     * Identifies the plugin that will be upgraded.
     */
    public String getPluginKey()
    {
        return "com.atlassian.jira.ext.charting";
    }
}

There are a few things to note about this implementation:

  • getBuildNumber() and getPluginKey() determine if this upgrade task will run. getPluginKey() needs to match the key of the plugin that is being upgraded (in this case the charting plugin). getBuildNumber() returns the buildnumber for this upgrade task. '1' will do for any plugin that hasn't had any upgrade tasks run against it yet. SAL's PluginUpgradeManager will run this upgrade task and store the buildnumber against the plugin once completed. After this, only upgrade tasks with a higher build number than '1' will be executed.
  • doUpgrade() uses some helpers provided by JIRA (i.e. the SimpleLegacyPortletUpgradeTask) to convert the legacy portlet to a gadget. This is entirely optional, however, and plugin authors are free to implement this method however they like.

Please ensure that the plugin upgrade task ONLY upgrades portletConfigurations for the plugin that's being upgraded! Any other portletConfigurations MUST be left untouched, as otherwise there's a risk of clobbering other portlets' data!

5. Register the upgrade task

Now we simply need to register the upgrade task as a component in the plugin:

atlassian-plugin.xml
...
 <component key="gadgetUpgradeTask" name="Gadget Upgrade Task"
 class="com.atlassian.jira.ext.charting.upgrade.GadgetUpgradeTask" public="true">
     <interface>com.atlassian.sal.api.upgrade.PluginUpgradeTask</interface>
 </component>
...

The PluginUpgradeManager in SAL will automatically scan for components that implement the PluginUpgradeTask interface. Please note that they have to be declared as public="true".

That's it. Simply re-package the plugin, deploy it to the instance of JIRA to upgrade and restart the JIRA instance. The plugin upgrade task should be executed when JIRA starts up.

It's highly recommended that you perform a backup of your JIRA instance before attempting this!

Last modified on Nov 16, 2011

Was this helpful?

Yes
No
Provide feedback about this article
Powered by Confluence and Scroll Viewport.