Inter-plugin communication

This is an idea on a new plugin type in Confluence to support basic inter-plugin communication.

Plugins wanting to provide a public service implement the PluginService interface:

public interface PluginService {
    public Object execute(Map parameters) throws PluginServiceException;
}

A Confluence PluginServiceManager would dispatch the plugin services requests:

public interface PluginServiceManager {

    public Object execute(String serviceKey, Map parameters) throws PluginServiceException, 
                                             PluginServicesNotFoundException;
}

Here's an example.

Let's suppose we want the Reporting Plugin to create a fancy report of Pages that have received a given Approval through the Approvals Workflow Plugin.

The Approvals Workflow Plugin would implement the service this way:

public class ApprovalsWorkflowReporterService implements PluginService {
...
    public Object execute(String serviceKey, Map parameters) throws PluginServiceException {
        List approvedPages = approvalsManager.getApprovedPages(parameters.get("space"),parameters.get("approval"));
        return approvedPages;
    }
...

You declare your service in atlassian-plugin.xml:

<atlassian-plugin name='Approvals Workflow Plugin' key='com.comalatech.workflow'>
...
    <service name="approvals-report" class="com.comalatech.confluence.workflow.services.ApprovalsWorkflowReporterService"
        key="approvals-report" alias="awp-report"/>
...
</atlassian-plugin>

This is what the ContentReporterMacro would have to do:

...
String service = (String)parameters.get("service");
if (services != null && service.length() > 0) {
    try {
        List entities = (List)pluginServiceManager.execute(service,parameters);
        ...
    } catch (PluginServiceException e) {
        throw new MacroException("Error retrieving entries: " + e.getMessage());
    } catch (PluginServicesNotFoundException e) {
        throw new MacroException("There is no service provider " + service);
    } 
}

This is how you'd use the macro:

...
{content-reporter:service=awp-report|space=DRAFTSDLC|approval=Published} 
...

We could handle only core Java/Confluence objects. To handle more complex objects, we could provide another layer on top this one: serializing/de-serializing (i.e. using xstream) and providing a binaries containing the beans.

Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Mar 05, 2008

    David Peterson says:

    While this may work for simple cases, there are some issues to consider. 1. I'm...

    While this may work for simple cases, there are some issues to consider.

    1. I'm assuming that the PluginService class and friends would be deployed as part of Confluence, not in plugins. Otherwise, plugins won't be able to communicate anyway, since the Plugin A's version of PluginService will not be the same as Plugin B's PluginService.
    2. Plugins would have to ensure that any values in both the parameters and the Object being returned from execute are only classes which are present in the webapp classloader (eg String, Map, etc). Classes which are from the plugin or any bundled libraries will break when passed across plugin boundaries.
    3. Having to get users to enter long key values to identify what service they want to use will be both error-prone and impractical on more complex queries. For example, Reporting uses 'key chains' to display data. As soon as you start chaining more than one item, it starts getting really verbose. Here's an example of what a custom news report would look like (originally from here):

    {report-block:maxResults=5}
    
    {content-reporter:spaces=@all|type=news}
      {date-sort:net.customware.confluence.plugins.reporting:content:modification date|order=descending}
    {content-reporter}
    
    {report-body}
    h2. {report-image:net.customware.confluence.plugins.reporting:content:icon} {report-info:net.customware.confluence.plugins.reporting:content:title|link=true}
    
    {report-info:net.customware.confluence.plugins.reporting:content:body|render=wiki}
    _Labels:_ {report-info:net.customware.confluence.plugins.reporting:content:labels|link=true|default=_none_}
    _Posted on {report-info:net.customware.confluence.plugins.reporting:content:modification date|format=dd MMM @ h:mm a} by {report-info:net.customware.confluence.plugins.reporting:content:modifier|link=true}._
    {report-body}
    
    {report-empty}
    _No news items are available._
    {report-empty}
    
    {report-block}
    

    And that doesn't even use any key chaining ( "foo > bar" ). Let's say you wanted the title of the user's personal space's homepage for some reason. Your chain would look like this:

    {report-info:net.customware.confluence.plugins.reporting:global:current user > net.customware.confluence.plugins.reporting:user:personal space > net.customware.confluence.plugins.reporting:space:homepage > net.customware.confluence.plugins.reporting:content:title}
    

    As opposed to what it is current:

    {report-info:global:current user > personal space > homepage > title}
    

    4. There is no way for plugins to 'advertise' that they support a particular type of service. Let's say you build a generic shopping cart plugin for Confluence. It is designed so that someone administering the site can set up a few different options for payment gateways. Ideally, you would be able to build separate plugins for each type of payment gateway so that other gateways can be added later and have them listed in the shopping cart admin as selectable. With the solution above, the site administrator would have to enter the keys for each additional gateway plugin manually. Sure, it'll work but it is definitely error prone.

    The current solution used by Reporting is a bit more complex to set up, but a) it works right now, b) lets you pass complex java objects between plugins (albeit still with care taken about what is crossing the plugin boundary) and c) can advertise that it supports a particular service without having to know what other plugins will use that service. Unfortunately it's not well documented yet, but it's called 'Intercom'. If anyone cares enough to learn more email me and I'll provide more details (which may turn into some general documentation).

  2. Mar 05, 2008

    David Peterson says:

    The shorter key is in improvement from a user POV, but introduces the possibilit...

    The shorter key is in improvement from a user POV, but introduces the possibility of multiple plugins providing a service with the same key. Plugin authors would have to ensure that nobody else is using that service id. Funnily enough, the best way to guarantee that is with the full package name system

  3. Mar 27

    Alain Moran says:

    I have created a simple interplugin messaging system for use between builder and...

    I have created a simple inter-plugin messaging system for use between builder and bubbles, to use it you add the following code to your pom.xml

    <!-- plugin messaging -->
    <dependency>
      <groupId>com.adaptavist.confluence</groupId>
      <artifactId>adaptavist-plugin-message-client</artifactId>
      <version>0.0.3-SNAPSHOT</version>
      <scope>compile</scope>
    </dependency>

    For now you will need to install the attached jar manually, however I hope to get the maven repo sorted as part of the entry into codegeist

    To send a message you build a hashmap and send it to a plugin!

    eg: here is the code from bubbles where it requests builder to set the layout id of a space

    HashMap message = new HashMap();
    message.put("username",data.getUser());
    message.put("spacekey",data.getSpacekey());
    message.put("layoutId",communityType.getLayoutId());
    message.put("layoutLock",communityType.getLayoutLock());
    PluginMessageManager.getInstance().sendMessage("com.adaptavist.confluence.themes.sitebuilder","adaptavist.builder.setLayout",message);

    Now for the 'complicated' bit ... listening to a plugin message!

    To do this you need to create a class that extends the PluginMessageHandler, and then register the handler with the manager, here is the listener code that handles the message sent above.

    public class SetLayoutMessageHandler extends PluginMessageHandler {
        private static final Logger log = Logger.getLogger( SetLayoutMessageHandler.class );
    
        private static final String[] HANDLED_MESSAGES = {"adaptavist.builder.setLayout", "adaptavist.builder.setLayoutLock"};
    
        private SpaceManager spaceManager;
        private UserAccessor userAccessor;
    
        private SpaceManager getSpaceManager() {
            if (spaceManager==null) {
                spaceManager = (SpaceManager) ContainerManager.getComponent("spaceManager");
            }
            return spaceManager;
        }
    
        private UserAccessor getUserAccessor() {
            if (userAccessor==null) {
                userAccessor = (UserAccessor) ContainerManager.getComponent("userAccessor");
            }
            return userAccessor;
        }
    
        public void handleMessage(String messageId, HashMap data) {
            String username = (String) data.get("username");
            if (username!=null) {
                User user = getUserAccessor().getUser(username);
                if (user!=null) {
                    String spaceKey = (String) data.get("spacekey");
                    if (getSpaceManager().getSpace(spaceKey)!=null) {
                        String layoutId = (String) data.get("layoutId");
                        if (layoutId!=null) {
                            try {
                                LayoutManager.getLayoutManager().setSpaceLayoutId(spaceKey,layoutId,user);
                            } catch (BuilderException e) {
                                log.error(e.getMessage());
                            }
                        }
                        Boolean layoutLock = (Boolean) data.get("layoutLock");
                        if (layoutLock!=null) {
                            try {
                                LayoutManager.getLayoutManager().setSpaceLayoutLock(spaceKey,layoutLock.booleanValue(),user);
                            } catch (BuilderException e) {
                                log.error(e.getMessage());
                            }
                        }
                    }
                }
            }
        }
    
        public String[] getHandledMessageIds() {
            return HANDLED_MESSAGES;  //To change body of implemented methods use File | Settings | File Templates.
        }
    }

    to register the handler we use

    PluginMessageManager.getInstance().addMessageHandler(new SetLayoutMessageHandler());

    adaptavist-plugin-message-client-0.0.3-SNAPSHOT.jar