Mundorévès

Cada día me veo en un mundo al revés

Confluence

Last changed Nov 20, 2009 11:48 by Roberto Dominguez
Labels: confluence, roller-coaster

Time to resume my journey to Confluence Plugin Nirvana this time to get the Approvals Workflow Plugin support 3.1 but still supporting earlier version of confluence with the same binaries.

Note that I am still on plugins v1, moving to v2 is a major endeavor.

This is a WORK-IN-PROGRESS Post as 3.1 has not been released. Hopefully some of these issues are going to be address when/if they are fixed before the final Release. Version tested is 3.1-beta2.

So far, I have managed to get my unit and integration tests to pass. I will be adding my notes to this post, so you may want to watch it.

TaskQueueFlushJob

UPDATE: looks like they have restored getQueueName() so this may be a no-issue

Seems they're trying to move everything to dependency injection. Before the key name was set through and abstract method setQueueName.

So, in my Job constructor I added:

initQueueName();

Then I created initQueueName():

private void initQueueName() {
    try {
        Method setter = getClass().getMethod("setQueueName",String.class);
        setter.invoke(this,this.getQueueName());
    } catch (NoSuchMethodException ignored) {
    } catch (Exception e) {
        log.error("",e);
    }
}

See http://jira.atlassian.com/browse/CONF-17413

Logout text changes causing Integration Unit Tests to fail.

A minor UI changes causes Integration Unit Tests to fail: AbstractConfluencePluginWebTestCase.logout() checks for a text in the response which has been changed causing the tests to fails.

All I had to do is override the logout method:

@Override
protected void logout() {
    gotoPage("/logout.action");
}

I am using an old version of the test framework (1.4.3-beta3), so my guess is that there is no issue on the latest version.

UI/Javascript Changes

Label manipulation changed again... haven't figured out yet

Html changes affects Integration tests

I was checking for "error" in HTML responses... Problem now is that they are setting a lot of error messages in the HTML code (as of 3.1-beta2). So I had to comment the assertion out.

//        assertTextNotInElement("content","error");

Not a big deal, as it was just a safety net.

Posted at Oct 30, 2009 by Roberto Dominguez | 0 comments
Last changed Apr 09, 2009 13:54 by Roberto Dominguez
Labels: confluence

I have been working on the Approvals Workflow Plugin (AWP) since 2007 and just recently released version 2.

Because of the nature of the application, it appeals to enterprises, and they're not that keen on upgrading that often, therefore I have to support several versions of Confluence, and that can be challenging.

For the new version of the plugin, I started using web integration tests to run tests which I used to do manually.

The supported versions of Confluence are from 2.6 to 2.10, so although I have managed to have only one binary, I still have to test on all the supported versions on every release.

In addition to the 80+ unit tests I already have, there are the manual tests that require creating pages, approving documents, adding labels, changing permissions, etc.

But now that I created the Web Tests and included them into my Bamboo plans, every time I commit changes, the tests run in just a few minutes on 2.8.2, 2.9.1 and 2.10.1, which I used to do only when I was about to release.

Although I am not able to run all the tests against all the versions I need, it reduces the amount of manual testing required.

There is not too much documentation on the subject, so I started by looking at the chart-plugin test and still took me a while to get it going, so for the community, here is what I have learnt.

UPDATE: Customware has put together this neat documentation

POM Changes

The dependency is already included in the confluence-plugin-base so as long as you created your plugin using the plugin archetype you are all set.

To start, you only have to make a couple of changes in your xml.pom (here's mines, which you could use a reference).

First you need to define some properties:

<properties>
    <confluence.version>2.10.1</confluence.version>
    <atlassian.product.test-lib.version>1.4.2</atlassian.product.test-lib.version>
    <atlassian.product.data.version>2.10</atlassian.product.data.version>
    <confluence.plugin.bundled>true</confluence.plugin.bundled>
    <confluence.jars.to.remove.regex.list>${basedir}/test/resources/jar-files-to-remove-from-bundle.txt</confluence.jars.to.remove.regex.list>
</properties>
Property Description Notes
confluence.version The version of Confluence on which you are going to be compiling and running the tests. I was only able to run against 2.8.x, 2.9.x and 2.10.x. There are missing libraries on earlier versions.
In theory you also have to define atlassian.product.version but I've found that confluence.version is enough
atlassian.product.test-lib.version test lib version I am currently using version 1.4.2 of the confluence-functestrpc-plugin. The latest stable version (2.1) uses a newer version of JWebUnit and changed all the packages. I tried upgrading but had several problems running it.
atlassian.product.data.version data set version You can omit defining this one if you are testing against 2.8 or 2.9, but you must set it to 2.10 if running on 2.10
confluence.jars.to.remove.regex.list the name says it all The file has to be included as part of the test resorces, see below. Mines contains the rexep of my plugin: ^approvalsworkflow-.*.jar$ This important when rerunning the tests.

Then you need to define your test resources, where you need to store that jar-files-to-remove-from-bundle.txt file and any other files you need for your testing (more on this below).

<build>
    ...
    <testSourceDirectory>test</testSourceDirectory>
    <testResources>
        <testResource>
            <directory>test/resources</directory>
            <includes>
                <include>**/*.*</include>
            </includes>
        </testResource>
    </testResources>                          
    ...
</build>

You can look at my pom.xml.

The abstract web test case

You can start by looking at ChartMacroTestCase.java for a set of simple tests. Something to be noted is that the Web tests' package names must start with 'it.'.

My tests were more complex because:

  • Some of tests deal with content permissions
  • Some require the Metadata Plugin to be installed
  • The plugin has some macros which are disabled by default.
  • The tests require workflows to be loaded. (workflows are a connection of macros, which contain approvals and triggers)

I had to create an abstract web test class, to provide some common services. Some of them can be useful for other applications and also can give you some ideas.

public class AbstractAwpWebTestCase extends AbstractConfluencePluginWebTestCase {
    protected void setUp() throws Exception {
        super.setUp();
        SpaceHelper spaceHelper;
        spaceHelper = getSpaceHelper();
        spaceHelper.setKey(SPACE_KEY);
        spaceHelper.setName("AWP Test Space");
        spaceHelper.setDescription("AWP Test Space");
        assertTrue(spaceHelper.create());

        configureSpacePermissions();

        enableMacro( "workflow");
        enableMacro("include-segment");
        enableMacro("send-email");
        configurePlugin();
        configureUsers();
        loadRequiredPlugins();
    }
    ...
}

Setting space permissions

The first part of setUp() only creates test space. However, because some of my testing deals with permissions, I had to configure space permissions.

What is done through the UI like this:


It is done in the web test like this:

private void configureSpacePermissions() throws Exception {
    gotoPage("/spaces/editspacepermissions.action?key=" + SPACE_KEY);
    setFormElement("groupsToAdd",GROUP_CONFLUENCEUSERS);
    submit("groupsToAddButton");

    gotoPage("/spaces/editspacepermissions.action?key=" + SPACE_KEY);
    checkCheckbox("confluence_checkbox_viewspace_group_confluence-users");
    checkCheckbox("confluence_checkbox_editspace_group_confluence-users");
    checkCheckbox("confluence_checkbox_exportpage_group_confluence-users");

    checkCheckbox("confluence_checkbox_removepage_group_confluence-users");
    checkCheckbox("confluence_checkbox_editblog_group_confluence-users");
    checkCheckbox("confluence_checkbox_comment_group_confluence-users");
    checkCheckbox("confluence_checkbox_createattachment_group_confluence-users");
    submit();
}

You can use firebug or look at the HTML source to identify the form element names.

Enabling plugin's modules

Then I need to enable some of the plugin's modules that are disabled by default upon installation.

So again, what is done through the UI like this:

It is done in the web test like this:

protected void enableMacro(String name) throws Exception {
    gotoPage("/admin/plugins.action?mode=enable&moduleKey=com.comalatech.workflow%3A" + name);
    assertEquals(200,getDialog().getResponse().getResponseCode());
}

Adding users

The Confluence web test library provide some helpers which use the SOAP API for some operations.

Because of my test cases, I need to create some users:

(view code)

Loading other plugins

My tests require the Metadata Plugin so I copied the jar to the test resources directory and this is how it gets loaded:

private void loadRequiredPlugins() throws Exception {
    ConfluenceWebTester confluenceWebTester = getConfluenceWebTester();
    File plugin = new File(SystemUtils.JAVA_IO_TMPDIR,"metadata-2.1.0.jar");
    InputStream in = null;
    OutputStream out = null;
    try {
        in = new BufferedInputStream(getClass().getClassLoader().getResourceAsStream("metadata-2.1.0.jar"));
        out = new BufferedOutputStream(new FileOutputStream(plugin));
        IOUtils.copy(in, out);
    } finally {
        IOUtils.closeQuietly(out);
        IOUtils.closeQuietly(in);
    }
    confluenceWebTester.setConfluencePluginJar(plugin);
    confluenceWebTester.setConfluencePluginName("Metadata Plugin");
    installPlugin();
}

Loading test pages

I need some predefined workflows for my tests. The workflows are plain wiki markup. The workflows are stored also in the test resources directory, and loading them is just a matter of creating a page using the helper classes:

(view code)

There are other methods I created, which include workflow-specific operations (i.e. approving pages) and assertion (i.e. asserting approvals availability and status) .

The test cases

Developing the first tests was very time consuming, as it takes a while to get confluence running, but it was worth it, as the new ones were really easy.

Here's an example of a test that simulates what this demo does:

(view workflow)
(view code)

Running the tests and adding to Bamboo

Running the tests is just a matter of:

mvn clean integration-test

I also added the tests to the Trunk Plan which compiles and tests on 2.8.2.

Running the tests take a few minutes because some of them depend on queued tasks on Confluence.

I also created two other plans, one for 2.9.2 and another for 2.10.1.

(Note that for 2.10 I had to define atlassian.product.data.version).

The tests on 2.9.2 and 2.10.1 are fired when the Trunk build is completed:

Conclusion

I found difficult getting the integration testing going. Besides having to dig into code because of lack of documentation, running the tests can take a while.

However, as everything invested in repeatable testing, it was worth the effort. The benefit is not only for checking changes but also for verifying new versions of confluence. i.e. running the tests on 3.0-m3 was just a matter of:

mvn  -Dconfluence.version=3.0-m3 -Dmaven.test.unit.skip=true clean integration-test;

References

Posted at Jan 29, 2009 by Roberto Dominguez | 2 comments
Last changed Jul 21, 2008 14:32 by Roberto Dominguez
Labels: confluence

I've been using a trick for allowing customizing CSS in the Approvals Workflow Plugin and in themes.

The trick proved to be very handy in a recent high-speed project with Headshift I just completed. The Designer was able to make last-minute CSS tweaks by simply updating an attachment. I would later update the theme's stylesheet.

The way it works is by using an attachment in the space's home page as an alternative stylesheet.

A helper checks if the attachment styles.css exists in the space's home page, and if so, it will return a link to if as a stylesheet, which is included in the HTML.

public class CssHelper {

    private static final String CUSTOM_CSS_NAME = "styles.css";

    private SpaceManager spaceManager;

    public String getImportThemeCssLink(String spaceKey, String contextPath) {
        Space space = spaceManager.getSpace(spaceKey);
        if (space == null) {
            return "";
        }
        Page homePage = space.getHomePage();
        Attachment attachment = homePage.getAttachmentNamed(CUSTOM_CSS_NAME);
        if (attachment == null) {
            return "";
        }
        return "<link rel=\"stylesheet\" href=\"" +
                contextPath + "/download/attachments/" + 
                homePage.getIdAsString() + "/" + CUSTOM_CSS_NAME +
                "\" type=\"text/css\" />";
    }

    public void setSpaceManager(SpaceManager spaceManager) {
        this.spaceManager = spaceManager;
    }
}

it has to be defined in atlassian-plugin.xml:

    <velocity-context-item key="cssHelper" name="Theme CSS Helper" 
          context-key="cssHelper"
          class="com.headshift.confluence.css.helper.CssHelper"/>

In the main.vmd you can add the following at the end of the <head> section:

$cssHelper.getImportThemeCssLink($spaceKey,$req.contextPath)

That's it! the extra CSS will be included only if it exists.

Posted at Jul 21, 2008 by Roberto Dominguez | 3 comments
Last changed May 22, 2008 07:26 by Roberto Dominguez
Labels: confluence

I submitted the Sticky Notes and Snip-Edit Plugin to the 2008 Codegeist. Although I spent more time that I had expected (too much fun) I turned out to be very straight forward.

The plugin relies heavily on jQuery which dramatically simplifies the way you can integrate HTML with Javascript.

Atlassian introduced it in Confluence 2.8 (but could be included in older versions) and it is going to make life way easier in theming and extending Confluence's capabilities. The Sticky Notes and Snip-Edit Plugin is an example.

Sticky Notes

Here's a simplified explanation of how the plugin works. You can get the source code here.

How it works

The plugin allows you to add notes on every heading (h1 to h5) of a document.

Each comment added is identified by a noteId which is based on the heading caption, and a commentId, which is generated when storing the comment.

when the HTML is loaded, the noteIds are generated, the existing notes marked, and the proper assigned to hover, adding or deleting notes:

jQuery(document).ready(initStickyNotes);

function initStickyNotes() {
    jQuery(".wiki-content").find("h1, h2, h3, h4, h5").each(setSectionsIds);
    jQuery(".existing-note").each(setExistingNote);
    jQuery(".sticky-action").each(setStickyAction);
}

SetSectionsIds will generate the noteId and will append a <span> element on which the sticky notes HTML will be included:

function setSectionsIds() {
    var jthis = jQuery(this);
    var content = jthis.text();
    var id;
    id = 'sticky_' + content.replace(/\W/g, '').toLowerCase();
   jthis.append("<div class='sticky-action' id='" + id + "'><span class='sticky-icon'></span></div>");
}

setExistingNote will set the proper icon for existing notes. The existing notes are rendered in the HTML generated by the {stickynotes} macro. For each existing note we are rendering the noteId as "<span class="existing-note">$noteId</span> in a hidden div.

function setExistingNote() {
    var noteid = jQuery(this).html();
    toggleActionIcon(jQuery("#" + noteid), true);
}

function toggleActionIcon(section, hasNotes) {
    if (hasNotes) {
        section.addClass("sticky-notes");
        section.find(".sticky-icon").addClass("has-notes");
    } else {
        section.removeClass("sticky-notes");
        section.find(".sticky-icon").removeClass("has-notes");
    }
}

setStickyAction will then load the existing notes, and set the proper actions for clicking and hovering over the icons:

function setStickyAction() {
    setActionIcon(jQuery(this), true);
}

function setActionIcon(section, pinNote) {
    if (section.hasClass("sticky-notes")) {
        section.hover(hoverInNotes, hoverOutNotes);
        section.find(".sticky-icon").click(pinStickyNote);
    } else {
        section.find(".sticky-icon").click(addStickyNote);
        section.hover(function() {}, function() {});
    }
}

When the sticky notes icons are hovered, and if they have notes, then they are loaded and display. If they hovered out, they are closed:

function hoverInNotes() {
    var jthis = jQuery(this);
    if (! jthis.find(".sticky").length) {
        openNote(jthis);
    }
}

function openNote(section) {
    var id = section.attr("id");
    var popup = createPopup(section);
    var uri = makeUri("get") + "&noteId=" + id;
    popup.load(uri, function() {
        if (stickyReadOnly) {
            jQuery(this).sticky().find(".sticky-add").hide();
            jQuery(this).find(".sticky-delete").hide();
        } else {
            jQuery(this).sticky().find(".sticky-add").click(addStickyNote);
            jQuery(this).find(".sticky-delete").click(deleteStickyNote);
        }
    });
}

function hoverOutNotes() {
    var popup = jQuery(this).find(".sticky-popup");
    if (! popup.find(".sticky-pinned").length) {
        popup.remove();
    }
}

addStickyNote and deleteStickyNote would just display the proper html and actions when the buttons are clicked.

What happens in the server is plain old Confluence plugin code.

Of course there is actually more happening in the plugin (the {stickable} macro, the action icon, handling cookies to keep sticky notes closed, pinning notes) but it a nutshell this is how it works.

Posted at May 09, 2008 by Roberto Dominguez | 0 comments
Last changed Jun 27, 2008 17:25 by Roberto Dominguez
Labels: confluence

Had a chat a couple of weeks ago with Guy Fraser and Alain Moran on achieving Confluence Plugin Nirvana: same binary for most of the current versions of confluence. They have managed to do that with their Theme Builder, so I had to get the Approvals Workflow Plugin up to the challenge.

In the last year or so, Atlassian have thrown a few monkey wrenches to plugin developers that have caused binary incompatibility:

So this is how I have dealt with these problems to come up with a single binary of the Approvals Workflow Plugin starting in version 1.4.

I18NBean change

That one was easy. Use ConfluenceActionSupport.getText() and ConfluenceActionSupport.getTextStatic() instead.

Lucene upgrade in 2.7

This one wasn't that easy. Lucene 2.2.0's Document.add changed to take a Fieldable instead of a Field of earlier version.

This one required to create a wrapper, which uses reflection to obtain the appropriate method:

public class DocumentIndexer {

    private Method addMethod;
    public DocumentIndexer() {
        Class fieldClass;
        try {
            fieldClass = Class.forName("org.apache.lucene.document.Fieldable");
        } catch (ClassNotFoundException e) {
            try {
                fieldClass = Class.forName("org.apache.lucene.document.Field");
            } catch (ClassNotFoundException e1) {
                throw new RuntimeException(e);
            }
        }
        try {
            addMethod = Document.class.getMethod("add", new Class[] {fieldClass});
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void add(Document document, String key, String value) {
        try {
            Field field = new Field(key, value, Field.Store.YES,Field.Index.TOKENIZED);
            addMethod.invoke(document,new Object[] {field});
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

CacheManager/Cache package changes

Solution here was wrapping Cache and CacheManager and do the magic through reflection. The plugin would use then the wrapped classes instead.

package com.comalatech.confluence.workflow.wrappers.caching;
public interface Cache  {

    public String getName();

    public Object get(Object key);

    public void put(Object key, Object value);

    public void remove(Object key);

    public void removeAll();
package com.comalatech.confluence.workflow.wrappers.caching;
public interface CacheManager {

    public Cache getCache(String name);    
}
package com.comalatech.confluence.workflow.wrappers.caching;

import com.atlassian.spring.container.ContainerManager;

import java.lang.reflect.Method;

public class WrappedCacheManager implements CacheManager {
    private static Method getNameMethod;
    private static Method getMethod;
    private static Method putMethod;
    private static Method removeMethod;
    private static Method removeAllMethod;

    public Cache getCache(String name) {
        Object cacheManager = ContainerManager.getComponent("cacheManager");
        if (cacheManager == null) {
            throw new RuntimeException("cannot get cacheManager");
        }
        try {
            Method m = cacheManager.getClass().getMethod("getCache", new Class [] {String.class});
            Object cache = m.invoke(cacheManager,new Object[] {name});
            if (getNameMethod == null) {
                initMethods(cache.getClass());
            }
            return new WrappedCache(cache);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void initMethods(Class cacheClass) {
        try {
            getNameMethod = cacheClass.getMethod("getName",new Class[] {});
            getMethod = cacheClass.getMethod("get",new Class[] {Object.class});
            putMethod = cacheClass.getMethod("put",new Class[] {Object.class, Object.class});
            removeMethod = cacheClass.getMethod("remove",new Class[] {Object.class});
            removeAllMethod = cacheClass.getMethod("removeAll",new Class[] {} );
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    private class WrappedCache implements Cache {

        private Object cache;

        private WrappedCache(Object cache) {
            this.cache = cache;
        }

        public String getName() {
            try {
                return (String)getNameMethod.invoke(cache,new Object[] {});
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        public Object get(Object key) {
            try {
                return getMethod.invoke(cache,new Object[] {key});
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        public void put(Object key, Object value) {
            try {
                putMethod.invoke(cache,new Object[] {key, value});
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        public void remove(Object key) {
            try {
                removeMethod.invoke(cache,new Object[] {key});
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        public void removeAll() {
            try {
                removeAllMethod.invoke(cache,new Object[] {});
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}

web-item location changes in 2.8

Web item locations changed in 2.8 and the old locations are just ignored.

Solution is simple: defining two web items, one for each location in atlassian-plugin.xml. For example:

    <web-item key="snipedititem" name="Snip-edit Page Action" 
         section="system.page.actions" weight="3">
        <label key="snipeditmark"/>
        <link></link>
    </web-item>

    <web-item key="snipedititemfor28" name="Snip-edit Page Action for 2.8 and newer"
         section="system.content.action/marker" weight="3">
        <label key="snipeditmark"/>
        <link></link>
    </web-item>

ViewPageAction changes in 2.8

Because of the ways page actions (i.e. edit, view, info, etc) are handled in 2.8, ViewPageAction was changed to set visibility of at least the Edit Page action.

I am extending ViewPageAction so this got me in trouble. The solution had to be very creative

First I have MyViewPageAction class that does the stuff I want:

public class MyViewPageAction extends ViewPageAction implements PageAware {
    ...
}

The I created MyViewPageAction28 that would extend the new interface CommentAware and add the required hack change to make the Edit Page action:

public class MyViewPageAction28 extends MyViewPageAction implements PageAware, CommentAware {
    public WebInterfaceContext getWebInterfaceContext() {
        DefaultWebInterfaceContext result = new DefaultWebInterfaceContext(super.getWebInterfaceContext());
        if (getClass().equals(MyViewPageAction28.class)) {
            result.setParameter(ViewingContentCondition.CONTEXT_KEY, Boolean.TRUE);
        }
        return result;
    }
}

Note that getWebInterfaceContext is only replicated from ViewPageAction but using MyViewPageAction28.class instead.

In addition to the xwork package required to override confluence's ViewPage action, I created yet-another action override in atlassian-confluence.xml:

<xwork name="Approvals Page Actions, extension for 2.8" key="myactions28" state="disabled">
    <package name="pages" extends="pages" namespace="/pages">
        <default-interceptor-ref name="validatingStack"/>
        <action name="viewpage" class="com.comalatech.confluence.workflow.actions.MyViewPageAction28">
        ....
        </action>
    </package>
</xwork>

Note that the state is disabled, therefore the module is going to be disabled even for 2.8.

Now here is the magic. Later on atlassian-plugin.xml I have a component call WorkflowsInitializer, which is StateAware and does the magic to enable the 2.8 xwork module:

public class WorkflowsInitializer implements StateAware {
    public void enabled() {
        boolean is2point8 = Integer.parseInt(GeneralUtil.getBuildNumber()) >= 1314;
        if (is2point8) {
            pluginController.enablePluginModule("com.comalatech.workflow:myactions28");
        } else {
            pluginController.disablePluginModule("com.comalatech.workflow:myactions28");
        }
    }    
}
Posted at May 04, 2008 by Roberto Dominguez | 0 comments

Labels

 
(None)




follow me at http://twitter.com

Confluence Development
hosted by rodogu.

Join now


Chat about what's on your mind. More about public chats.