Commits

Joe Clark  committed feed41d

Committing tutorial code for Confluence Pipeline Transformer Demo

  • Participants

Comments (0)

Files changed (12)

+.idea/
+*.iml
+.DS_Store
+target/
+Copyright (c) 2012, Atlassian
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+You have successfully created a plugin using the Confluence plugin archetype!
+
+Here are the SDK commands you'll use immediately:
+
+* atlas-run   -- installs this plugin into Confluence and starts it on http://<machinename>:1990/confluence
+* atlas-debug -- same as atlas-run, but allows a debugger to attach at port 5005
+* atlas-cli   -- after atlas-run or atlas-debug, opens a Maven command line window:
+                 - 'pi' reinstalls the plugin into the running Confluence instance
+* atlas-help  -- prints description for all commands in the SDK
+
+Full documentation is always available at:
+
+https://developer.atlassian.com/display/DOCS/Developing+with+the+Atlassian+Plugin+SDK
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.example.confluence.transformer</groupId>
+    <artifactId>tutorial-confluence-transformer-demo</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <organization>
+        <name>Example Company</name>
+        <url>http://www.example.com/</url>
+    </organization>
+
+    <name>Confluence Pipeline Transformer Demo Plugin</name>
+    <description>A plugin that allows administrators to add custom banners to pages with a particular label</description>
+    <packaging>atlassian-plugin</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.atlassian.sal</groupId>
+            <artifactId>sal-api</artifactId>
+            <version>2.7.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.6</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.confluence</groupId>
+            <artifactId>confluence</artifactId>
+            <version>${confluence.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.confluence.plugin</groupId>
+            <artifactId>func-test</artifactId>
+            <version>2.3</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.sourceforge.jwebunit</groupId>
+            <artifactId>jwebunit-htmlunit-plugin</artifactId>
+            <version>2.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.sourceforge.nekohtml</groupId>
+            <artifactId>nekohtml</artifactId>
+            <version>1.9.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.8.5</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>com.atlassian.maven.plugins</groupId>
+                <artifactId>maven-confluence-plugin</artifactId>
+                <version>${amps.version}</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <productVersion>${confluence.version}</productVersion>
+                    <productDataVersion>${confluence.data.version}</productDataVersion>
+                    <instructions/>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.6</source>
+                    <target>1.6</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <properties>
+        <confluence.version>4.1.6</confluence.version>
+        <confluence.data.version>3.5</confluence.data.version>
+        <amps.version>3.11</amps.version>
+    </properties>
+</project>

File src/main/java/com/example/confluence/transformer/actions/ViewConfigurationAction.java

+package com.example.confluence.transformer.actions;
+
+import com.atlassian.confluence.core.ConfluenceActionSupport;
+import com.atlassian.confluence.labels.Label;
+import com.atlassian.confluence.labels.LabelManager;
+import com.example.confluence.transformer.storage.Banner;
+import com.example.confluence.transformer.storage.BannerStorage;
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.opensymphony.xwork.Action;
+
+import java.util.Collection;
+
+/**
+ * The action controller class for the "View" action of the plugin. It is a
+ * general convention to name all action classed with a suffix of "Action". It
+ * is also strongly recommended to have the action extend the
+ * {@link ConfluenceActionSupport} class.
+ */
+public class ViewConfigurationAction extends ConfluenceActionSupport
+{
+    private final BannerStorage bannerStorage;
+    private final LabelManager labelManager;
+
+    /**
+     * This is just a helper class to make it easier to reference important
+     * information from velocity templates.
+     */
+    public static class BannerView
+    {
+        private final Banner banner;
+        private final Label label;
+
+        BannerView(Banner banner, Label label)
+        {
+            this.banner = banner;
+            this.label = label;
+        }
+
+        public Banner getBanner()
+        {
+            return banner;
+        }
+
+        public Label getLabel()
+        {
+            return label;
+        }
+    }
+
+    /**
+     * Constructor called by Confluence before the action _is first invoked_.
+     * Note that this is different to some other plugins modules which are
+     * constructed as soon as the plugin is installed.
+     * @param bannerStorage A reference to our {@link BannerStorage} component
+     *                      in this plugin. Confluence automatically locates it
+     *                      and provides it as a constructor parameter here.
+     * @param labelManager  A reference to the Confuence {@link LabelManager}
+     *                      component.
+     */
+    public ViewConfigurationAction(BannerStorage bannerStorage, LabelManager labelManager)
+    {
+        this.bannerStorage = bannerStorage;
+        this.labelManager = labelManager;
+    }
+
+    /**
+     * This getter method allows the velocity template to access the banner
+     * configuration objects stored in the database. A velocity template can
+     * access the method by calling '$action.banners'.
+     *
+     * @return The collection of all configured banners in the plugin, wrapped
+     * in a helper object that also provides a reference to the full {@link Label}
+     * object that the Banner relates to.
+     */
+    public Collection<BannerView> getBanners()
+    {
+        return Collections2.transform(bannerStorage.getConfiguredBanners(), new Function<Banner, BannerView>()
+        {
+            @Override
+            public BannerView apply(Banner banner)
+            {
+                // Get the label associated with the banner
+                Label label = labelManager.getLabel(banner.getLabelId());
+                // Store them together in a helper object.
+                return new BannerView(banner, label);
+            }
+        });
+    }
+
+    /**
+     * The {@link #execute()} method is the main entry point for this class.
+     * When the user hits the appropriate URL in a browser, the execute method
+     * is called. In this method, the action can do whatever it needs to do in
+     * order to determine the result of the action.  The value returned from the
+     * execute method is then mapped to a corresponding <result> declaration in
+     * the atlassian-plugin.xml. The {@link Action} class has a variety of
+     * public, static variables for common return values; for example,
+     * Action.SUCCESS, Action.ERROR.
+     */
+    @Override
+    public String execute() throws Exception
+    {
+        // This is a very simple action, we just need to load the banners from
+        // the database and return. The loading is done in the getBanners()
+        // method, which will be called by the velocity template.
+        return Action.SUCCESS;
+    }
+}

File src/main/java/com/example/confluence/transformer/storage/Banner.java

+package com.example.confluence.transformer.storage;
+
+/**
+ * Represents an individual Banner that has been configured and saved to the
+ * database.
+ * See {@link BannerStorage}
+ */
+public class Banner
+{
+    private long labelId;
+    private BannerType bannerType;
+    private String bannerTitle;
+    private String bannerText;
+
+
+    /**
+     * A no-args constructor. This is necessary in order for the serialisation
+     * of this class to and from the database to function correctly.
+     */
+    public Banner()
+    {
+        // No-Op.
+    }
+
+    /**
+     * A convenience constructor so that the {@Link BannerStorageImpl} class
+     * can easily create new, pre-configured instances of {@link Banner}
+     *
+     * @param labelId The unique ID of the label that this banner applies to.
+     * @param type    The kind of banner (eg. info/note/tip)
+     * @param title   The optional title of the banner
+     * @param text    The body text contained within the banner
+     */
+    Banner(long labelId, BannerType type, String title, String text)
+    {
+        this.labelId = labelId;
+        this.bannerText = text;
+        this.bannerTitle = title;
+        this.bannerType = type;
+    }
+
+    public void setLabelId(long labelId)
+    {
+        this.labelId = labelId;
+    }
+
+    public long getLabelId()
+    {
+        return labelId;
+    }
+
+    public void setBannerText(String bannerText)
+    {
+        this.bannerText = bannerText;
+    }
+
+
+    public String getBannerText()
+    {
+        return bannerText;
+    }
+
+    public void setBannerType(BannerType bannerType)
+    {
+        this.bannerType = bannerType;
+    }
+
+
+    public BannerType getBannerType()
+    {
+        return bannerType;
+    }
+
+    public void setBannerTitle(String bannerTitle)
+    {
+        this.bannerTitle = bannerTitle;
+    }
+
+
+    public String getBannerTitle()
+    {
+        return bannerTitle;
+    }
+}

File src/main/java/com/example/confluence/transformer/storage/BannerStorage.java

+package com.example.confluence.transformer.storage;
+
+import com.atlassian.confluence.labels.Label;
+
+import java.util.Collection;
+
+/**
+ * Defines the operations available on the bannerStorage component (see the
+ * atlassian-plugin.xml). Responsible for saving and loading instances of
+ * {@link Banner} to and from the Confluence database.
+ */
+public interface BannerStorage
+{
+    /**
+     * Creates a new instance of {@link Banner} to the database, using the
+     * provided information. The newly-created {@link Banner} can be
+     * subsequently retrieved using the
+     * {@link #getBannerForLabel(com.atlassian.confluence.labels.Label)} method.
+     * <p/>
+     * If a {@link Banner} already exists with the same {@link Label}, the
+     * existing {@link Banner} will be overwritten and replaced by this one.
+     *
+     * @param label The {@link Label} to which this banner should apply.
+     * @param type  The {@link BannerType} of the new banner.
+     * @param title The optional title for the banner. Specify null or ""
+     *              to create a banner with no title.
+     * @param text  The body text of the new banner.
+     */
+    public void addBanner(Label label, BannerType type, String title, String text);
+
+    /**
+     * Returns all currently configured instances of {@link Banner}. If there
+     * are currently no banners configured, an empty collection will be
+     * returned.
+     *
+     * @return An un-ordered collection of all configured Banner instances.
+     */
+    public Collection<Banner> getConfiguredBanners();
+
+    /**
+     * Deletes the {@link Banner} associated with the specified {@link Label},
+     * if one exists. If none exists, this method takes no action.
+     *
+     * @param label The corresponding {@link Label} object.
+     */
+    public void removeBanner(Label label);
+
+    /**
+     * Retrieves the {@link Banner} associated with the specified {@link Label},
+     * if one exists. If none exists, this method will return {@code null}.
+     *
+     * @param label The corresponding {@link Label} object.
+     * @return The {@link Banner} associared with the specified {@link Label},
+     *         or {@code null} if no banner exists.
+     */
+    public Banner getBannerForLabel(Label label);
+}

File src/main/java/com/example/confluence/transformer/storage/BannerStorageImpl.java

+package com.example.confluence.transformer.storage;
+
+import com.atlassian.confluence.labels.Label;
+import com.atlassian.sal.api.pluginsettings.PluginSettings;
+import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
+import com.google.common.collect.Maps;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Provides a concrete implementation of the {@link BannerStorage} component
+ * interface.
+ */
+public class BannerStorageImpl implements BannerStorage
+{
+    private static final Logger log = LoggerFactory.getLogger(BannerStorageImpl.class);
+    private static final String BANNER_STORAGE_KEY = "banners";
+
+    private final PluginSettings pluginSettings;
+
+    /**
+     * Constructs a new instance of {@link BannerStorageImpl}. This constructor
+     * is called by Confluence when the plugin is initialised.
+     *
+     * @param pluginSettingsFactory The {@link PluginSettingsFactory} component
+     *                              provided by the Shared Access Layer ("SAL")
+     *                              plugin. Confluence will automatically find
+     *                              this component and set it as the
+     *                              constructor parameter for your component.
+     */
+    public BannerStorageImpl(PluginSettingsFactory pluginSettingsFactory)
+    {
+        // Use the SAL pluginSettingsFactory to create a new instance of
+        // the PluginSettings object. We can use this object to save simple
+        // Key/Value pairs in the Confluence database. The value objects must
+        // be serializable Java objects or primitive types.
+        this.pluginSettings = pluginSettingsFactory.createGlobalSettings();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void addBanner(Label label, BannerType bannerType, String bannerTitle, String bannerText)
+    {
+        log.debug(String.format("Adding new Banner (%s) for Label (%s)", bannerText, label.getDisplayTitle()));
+        Map<Long, Banner> banners = getBanners();
+        banners.put(label.getId(), new Banner(label.getId(), bannerType, bannerTitle, bannerText));
+        saveBanners(banners);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Collection<Banner> getConfiguredBanners()
+    {
+        // Return a collection that cannot be modified - this prevents other code
+        // from sneakily doing things that bypass the public Add and Remove
+        // methods on the BannerStorage interface.
+        return Collections.unmodifiableCollection(getBanners().values());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void removeBanner(Label label)
+    {
+        log.debug(String.format("Removing the banner associated with Label %s", label.getDisplayTitle()));
+        Map<Long, Banner> banners = getBanners();
+        banners.remove(label.getId());
+        saveBanners(banners);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Banner getBannerForLabel(Label label)
+    {
+        return getBanners().get(label.getId());
+    }
+
+    /**
+     * A private method to load the banners from the database. Because we are
+     * storing the banners in a {@link Map} as a single Key/Value pair in the
+     * database, we must load the entire map into memory from the database in
+     * order to add or remove a single entry, and then save the entire map
+     * back to the database again.
+     *
+     * Note that this approach to database persistence works fine for small
+     * sets of data, but would not scale well to thousands or millions of
+     * entries.
+     */
+    @SuppressWarnings("unchecked") // This should never happen.
+    private Map<Long, Banner> getBanners()
+    {
+        Object value = pluginSettings.get(BANNER_STORAGE_KEY);
+        if (value == null)
+            // Initialise the Map if it doesn't yet exist in the database.
+            return Maps.newHashMap();
+
+        return (Map<Long, Banner>)value;
+    }
+
+    /**
+     * A private method to save the configured banners back to the database.
+     */
+    private void saveBanners(Map<Long, Banner> banners)
+    {
+        pluginSettings.put(BANNER_STORAGE_KEY, banners);
+    }
+}

File src/main/java/com/example/confluence/transformer/storage/BannerType.java

+package com.example.confluence.transformer.storage;
+
+/**
+ * This enumeration is used to persist the 'kind' of banner being saved to the database.
+ * See {@link BannerStorage}
+ */
+public enum BannerType
+{
+    /**
+     * Indicates that the banner should be created using the {info} macro.
+     */
+    INFO,
+
+    /**
+     * Indicates that the banner should be created using the {warning} macro.
+     */
+    WARNING,
+
+    /**
+     * Indicates that the banner should be created using the {tip} macro.
+     */
+    TIP,
+
+    /**
+     * Indicates that the banner should be created using the {note} macro.
+     */
+    NOTE
+}

File src/main/resources/atlassian-plugin.xml

+<?xml version="1.0" encoding="UTF-8"?>
+
+<atlassian-plugin key="${project.groupId}.${project.artifactId}"
+                  name="${project.name}" plugins-version="2">
+    <plugin-info>
+        <description>${project.description}</description>
+        <version>${project.version}</version>
+        <vendor name="${project.organization.name}"
+                url="${project.organization.url}"/>
+    </plugin-info>
+
+    <component-import key="pluginSettingsFactory"
+                      interface="com.atlassian.sal.api.pluginsettings.PluginSettingsFactory"
+                      filter=""/>
+    <component key="bannerStorage" name="Configured Banner Storage"
+               class="com.example.confluence.transformer.storage.BannerStorageImpl">
+        <description>Stores configured banner information</description>
+        <interface>com.example.confluence.transformer.storage.BannerStorage
+        </interface>
+    </component>
+
+    <resource name="tutorial-confluence-transformer-demo-i18n" type="i18n"
+              location="i18n/plugin"/>
+
+    <!--
+    Defines a set of user interface actions that get mapped to URLs in Confluence.
+    The URL for each action is defined by the 'namespace' on the package, and the
+    action name. So in this example, where the namespace is "/admin/content-banner"
+    and the action name is "view", and the full Confluence URL is
+    "http://localhost:1990/confluence", then the full URL to this action would be
+    "http://localhost:1990/confluence/admin/content-banner/view.action"
+    -->
+    <xwork key="plugin-actions" name="Plugin XWork/WebWork Actions">
+        <package name="configuration" extends="default"
+                 namespace="/admin/content-banner">
+            <!--
+             Do not forget the default-interceptor-ref - things will start
+            breaking in weird ways if you leave this out!
+            -->
+            <default-interceptor-ref name="defaultStack"/>
+            <!--
+             A single action is controlled by a single Action class, which we
+             will build in the next step.
+            -->
+            <action name="view"
+                    class="com.example.confluence.transformer.actions.ViewConfigurationAction">
+                <!--
+                 You can map multiple results for a single action. When the
+                Action class runs, it is able to examine the incoming request
+                parameters, and various other aspects of the application, to
+                determine the 'outcome' of the action (for example, did it
+                succeed or fail?). However, the action class itself does not
+                directly control what the user sees as a result of that outcome.
+                Instead, that is controlled by this XML snippet - where we can
+                map the outcomes from the action to different responses.
+
+                In this example, we are setting an outcome of "Render a velocity
+                template" when the result of the action is "success".
+                -->
+                <result name="success" type="velocity">
+                    /templates/content-banner/view-config.vm
+                </result>
+            </action>
+        </package>
+    </xwork>
+
+    <web-item name="Admin Console Link" i18n-name-key="admin-console-link.name"
+              key="admin-console-link" section="system.admin/configuration"
+              weight="120">
+        <description key="admin-console-link.description">The Admin Console Link
+            Plugin
+        </description>
+        <label key="confluence-transformer-plugin.admin.link"/>
+        <link linkId="admin-console-link-link">
+            /admin/content-banner/view.action
+        </link>
+    </web-item>
+</atlassian-plugin>

File src/main/resources/i18n/plugin.properties

+## Text for the web-item Module
+confluence-transformer-plugin.admin.link=Configure Content Banners
+
+## Text used by the Velocity template
+confluence-transformer-plugin.admin.title=Configure Content Banner Plugin
+confluence-transformer-plugin.admin.description=Use this plugin to add panel macros to the content of a page without actually editing the content of a page. This relies on the Confluence Pipeline Transformer plugin module, which was added in Confluence 4.0.
+confluence-transformer-plugin.admin.banners.title=Configured Banners
+confluence-transformer-plugin.admin.banners.label=Label
+confluence-transformer-plugin.admin.banners.banner=Banner
+confluence-transformer-plugin.admin.banners.actions=Actions
+confluence-transformer-plugin.admin.banners.actions.delete=Delete
+
+admin-console-link.label=Admin Console Link
+admin-console-link.name=Admin Console Link
+admin-console-link.description=The Admin Console Link Plugin

File src/main/resources/templates/content-banner/view-config.vm

+<html>
+<head>
+    ## using '$i18n.getText()' looks up the text from your plugin.properties
+    ## file created in the previous step. The supplied parameter is the 'key'
+    ## of the text string to be included
+    <title>$i18n.getText("confluence-transformer-plugin.admin.title")</title>
+
+    ## The 'decorate' meta tag is an instruction to Confluence to render this
+    ## velocity template using a specific 'Decorator'. We are using the "admin"
+    ## decorator to add in the full Confluence Admin Console header, footer,
+    ## sidebar and general styling to this page - automatically.
+    <meta name="decorator" content="admin"/>
+</head>
+<body>
+<p>
+    $i18n.getText("confluence-transformer-plugin.admin.description")
+</p>
+<h2>$i18n.getText("confluence-transformer-plugin.admin.banners.title")</h2>
+## Setting the class of this table to "aui" (short for 'Atlassian User Interface')
+## applies the standard Atlassian cross-product table style to this table. For
+## more information, see https://developer.atlassian.com/display/AUI/Tables
+<table class="aui">
+    <thead>
+    <tr>
+        <th>$i18n.getText("confluence-transformer-plugin.admin.banners.label")</th>
+        <th>$i18n.getText("confluence-transformer-plugin.admin.banners.banner")</th>
+    </tr>
+    </thead>
+    <tbody>
+        ## The $action variable is automatically set to refer to the
+        ## XWork/WebWork Action object that rendered this template. We can use
+        ## this reference to the Action to access data that was loaded from the
+        ## database by the action - in this case we are doing a loop over all
+        ## the banners stored in the database by this plugin, and listing them
+        ## in the HTML Table.
+        #foreach($bannerView in $action.banners)
+        <tr>
+            ## The $req variable is automatically set to refer to the incoming
+            ## HttpServletRequest for this action. It provides a convenient way
+            ## to access the 'Context Path' being used by the Confluence server.
+            ## (For example, when logging in to Confuence via "http://localhost:1990/confluence"),
+            ## the 'context path' part of the URL is "/confluence". The
+            ## context path is configurable, and can even be omitted completely
+            ## by the system administrator. Thus, when writing plugins, it is
+            ## important to ensure that any URLs you generate will work in
+            ## situations where there is a context path, and situations where
+            ## there is no context path.  '$req.contextPath' will automatically
+            ## resolve to an empty string when there is no context path, so it
+            ## is safe (and recommended) to use in both circumstances.
+            <td><a href="$req.contextPath$bannerView.label.urlPath">$bannerView.label.displayTitle</a></td>
+            <td>$bannerView.banner.bannerText</td>
+        </tr>
+        #end
+    </tbody>
+</table>
+</body>
+</html>