Skip to end of metadata
Go to start of metadata

Applicable:

This tutorial describes how to create plugins that work across Atlassian products. It does so by creating a plugin that works with RefApp 2.12.0.

Level of experience:

This is an advanced tutorial. You should have completed at least one intermediate tutorial before working through this tutorial. See the list of tutorials in DAC.

Time estimate:

It should take you approximately 2 hours to complete this tutorial.

On this page:

Overview of the tutorial

Along the way, this tutorial introduces these concepts:

Your completed plugin will consist of:

  • Java classes encapsulating the plugin logic.
  • Resources for display of the plugin user interface (UI).
  • A plugin descriptor (XML file) to enable the plugin module in the Atlassian application.

Prerequisite knowledge

To complete this tutorial, you need to know the following: 

  • The basics of Java development: classes, interfaces, methods, how to use the compiler, and so on.
  • How to create an Atlassian plugin project using the Atlassian Plugin SDK. 

Plugin source

We encourage you to work through this tutorial. If you want to skip ahead or check your work when you have finished, you can find the plugin source code on Atlassian Bitbucket. Bitbucket serves a public Git repository containing the tutorial's code. To clone the repository, issue the following command:

git clone https://bitbucket.org/atlassian_tutorial/xproduct-admin-ui-plugin

Alternatively, you can download the source using the get source option here: https://bitbucket.org/atlassian_tutorial/xproduct-admin-ui-plugin.

About these Instructions

Icon

You can use any supported combination of OS and IDE to construct this plugin. These instructions were written using Ubuntu 10.04. If you are using another OS or an IDE, you should use the equivalent operations for your specific environment.

Step 1. Create the plugin project

In this step, you use an atlas- command to generate stub code for your plugin. The atlas- commands are part of the Atlassian Plugin SDK, and automate much of the work of plugin development for you.

  1. Open a terminal and navigate to the directory where you want to create your tutorial project directory.
  2. Enter the following command:

    atlas-create-refapp-plugin
    
  3. When prompted, enter the following information to identify your plugin:

    group-id

    com.atlassian.plugins.tutorial

    artifact-id

    xproduct-admin-ui-plugin

    version

    1.0

    package

    com.atlassian.plugins.tutorial

  4. Confirm your entries when prompted.

The command creates a directory named xproduct-admin-ui-plugin, which contains all your initial project files. This tutorial refers to this directory as your project home. Unless otherwise indicated, all directory paths are relative to the project home.

Step 2. Review and tweak the generated project

It's a good idea to familiarise yourself with the generated project files. In this section, we'll check a version value and tweak the generated files. We'll also start up the RefApp and have a look around.

Add plugin metadata to the POM

The POM (Project Object Model definition file) is located at the root of your project and declares the project dependencies and other information.

Add some metadata about your plugin and your company or organisation.

  1. Edit the pom.xml file in the root folder of your plugin.
  2. Add your company or organisation name and your website to the organization element:

  3. Update the description element:

  4. Save the file.

Verify your host application version

As mentioned in the overview, the host application for the plugin you are creating in this tutorial is the RefApp. When you generated the stub files, a default version specification was included in your pom.xml file for the RefApp. Take a moment and examine the dependency:

  1. If it's not already open, open the pom.xml file.
  2. Scroll to the bottom of the file.
  3. Find the properties element.
    This section lists the version of the RefApp and also the version of the atlas- commands you are running.
  4. Verify that the RefApp version is the one you want. If you are not sure which version you need, check the version table at the top of this tutorial.
  5. Save and close the pom.xml file.

Review the generated plugin descriptor

Your stub code contains a plugin descriptor file atlassian-plugin.xml. This is an XML file that identifies the plugin to the host application (HOSTAPP) and defines the required plugin functionality. In a text editor or IDE (integrated development environment, such as Eclipse or IDEA) open the descriptor file located in your project under src/main/resources. It should look like this:

Start and view the RefApp

Although you haven't modified the generated code yet, try starting the RefApp application now. It's always a good idea to check your work periodically by making sure the application compiles and starts up.

  1. At the command line, change directory to your plugin project home (the xproduct-admin-ui-plugin directory created earlier).

  2. Enter the atlas-run command.

This triggers the RefApp startup process. When finished, you should see output similar to:

Notice the URL for the RefApp. In this case, it is:

http://atlas-laptop:5990/refapp

Open the RefApp in a browser and have a look around, although there isn't much to see yet.  

Step 3. Add the web form servlet

Let's add a servlet to the RefApp that presents a webform in the RefApp UI. The form won't do anything yet, but it gives us something to add to as we go.

Create the servlet

Let's add a servlet to the project.

  1. Change directories to
    src/main/java/com/atlassian/plugins/tutorial/
  2. Make a new directory at that location, named xproductadminui.
  3. In the new directory, create a new file named AdminServlet.java with the following contents:

  4. Save and close the file.

Optionally, run the application again (using the atlas-run command from the project home), just to make sure everything compiles and starts up okay.

Add a credential check

Our web application first checks whether the user is logged in. If not, it redirects the user to the login page. We use the SAL User Manager feature to make sure that the current user is an administrator, so we need to add this dependency to our project file.

Open pom.xml and add the following dependency element alongside the other dependencies elements, immediately after the packaging declaration.

With SAL added as a build dependency, we can import SAL and use it in our servlet.

  1. Navigate to the directory:
    src/main/java/com/atlassian/plugins/tutorial/xproductadminui/
  2. Open AdminServlet.java for editing. 
  3. Add the following import statements alongside the existing statements:

  4. Replace the entire AdminServlet class declaration with the following:

In our doGet method, we check whether the user is logged in (that is, username is not null) and that the user is an administrator, thanks to the UserManager class. If either are false, we redirect the user to the application login page by calling the redirectToLogin method, which we haven't implemented yet. 

Add the following two methods, redirectToLogin and getUri methods to the AdminServlet class immediately after doGet:

Notice that the getUri method returns the current URI to the LoginUriProvider so that the user is redirected back to our admin UI when they are properly authenticated.

Render the form with the Atlassian Template Renderer

Now let's get to rendering! As mentioned in the Overview, we can use the Atlassian Template Renderer for that. Once again, before we can write the code, we need to tell Maven that we'll be using it.

Find the dependencies section in the pom.xml file and add the following dependency

In AdminServlet.java, add the following import statement to the existing ones:

import com.atlassian.templaterenderer.TemplateRenderer;

We need to instantiate the renderer object in the constructor and call it in the doGet method. Add the renderer variable declaration to the existing ones and replace the entire AdminServlet constructor and doGet method with the following:

We've added a dependency on a TemplateRenderer to our constructor. This is the renderer we use to display our form. In the doGet method, after applying our authorization check, we set the response content type to text/html and then make a call to the templateRenderer, telling it to render our admin.vm template. We will create the template shortly.

Modify the Atlassian plugin descriptor

Before we go any further, let's tell the plugin system about our servlet.

Open atlassian-plugin.xml for editing and add the following component import and servlet declaration statements. You can add it anywhere within the atlassian-plugin element, such as after the existing servlet declaration.

With that done, let's test the login redirect logic we've added. Start the RefApp by running the atlas-run command again and go to:

http://localhost:5990/refapp/plugins/servlet/xproduct/admin

A login prompt should appear. Log in using the credentials admin/admin. Once logged in, you won't see much except a big exception saying that the admin.vm file couldn't be found. Let's create it.

Step 4. Create the Velocity template

A Velocity template determines the HTML presentation produced by our application.

Create a Velocity template file named admin.vm in the src/main/resources directory. To start, we keep it simple. Add this to the file:

In your browser, refresh the admin page, http://localhost:5990/refapp/plugins/servlet/xproduct/admin

You should see something like this:

It's a start. But it's also, well, ugly. It's missing application template features, such as the Atlassian logo and menus. Let's fix that.

Add some decoration

To style the page, open admin.vm for editing again and add this meta tag. Add it to the head element, after the existing title element.

This tells the application that it needs to use the admin decorator around the body of this page.

Now the page looks like something the application would actually display.

Now we're getting somewhere. But the form itself still doesn't look so good. AUI to the rescue!

Style it

First we need to include AUI in our header. Add the following line after the meta tag in the admin.vm file.

What is $webResourceManager and where does it come from? It's the web resource manager from the plugin system used to manage CSS, JavaScript and other resources that are usually linked at the top of pages using <script>. Calling requireResource will include all the resources for the web-resource module along with its dependencies.

But where did the WebResourceManager come from? It was automatically added to the Velocity context by the TemplateRenderer. It makes a few things that are commonly useful available, like the WebResourceManager. We'll see another one that is included automatically below, when we get to internationalizing our template.

Next, we improve the appearance of the form by adding class attributes.

The changes were small. We added class="aui" to the form element, class="text" to each of our text input boxes, class="button" to our submit button, and class="field-group" to our div tags. And for such little work we get:

A nice looking form in any Atlassian application, and consistent with the application look and feel!

I18n it

We can also add internationalization support to our user interface pretty easily.

First, add a resource element specifying our i18n properties file to atlassian-plugin.xml

Next, create the properties file that holds the label text for the form. Under the src/main/resources/ directory, create the following directory path and file:

com/atlassian/tutorial/xproductadminui/i18n.properties

In the file, add this text:

These properties define the labels that will appear in our UI. For example, the label for the time field is 'Time'.

Next, replace the hard-coded text values in the admin.vm template with lookups for internationalized text.

$i18n is an instance of SAL's I18nResolver, which comes automatically with TemplateRenderer, just like the WebResourceManager. We use it to resolve our message keys to actual text.

Feel free to experiment with the labels in the properties file. To localize an application for an actual deployment, the only thing left to do would be to get the properties file translated.

Now we have our basic UI. It's time to make it do something!

Step 5. Make the JavaScript and REST happen

Our form is pretty simple. You can't post anything with it yet. To be able to post and display data in the form, we'll use JavaScript to make AJAX requests to a REST resource. The resource returns the data as JSON, which our form displays.

What's my address?

Before we can start writing our JavaScript in earnest, there are a few things we need to do. We need to know the application's base URL for making AJAX requests. We also need to tweak our template a bit to include the JavaScript we're writing in addition to the AUI stuff.

To discover the application's base URL, we'll use SAL's ApplicationProperties. But first we need to import it. Add this element to atlassian-plugin.xml:

We want to use the properties from our template as well, so also add this template context element:

The template context item element allows us to put objects into the Velocity (or other rendering context) so that we can use them in our templates. Alternatively, we could pass the ApplicationProperties component into our servlet (which is where we'll do the rendering from), and pass it into the rendering context when we call the render method. Using template-context-item just makes that automatic. And it makes the ApplicationProperties object available in all contexts, so that we can use the renderer in multiple places without duplicating code.

Now we're ready to add the base URL to our template. Add the following meta element to the admin.vm template head.

The meta tag calls the ApplicationProperties.getBaseUrl method to get the base URL for the application. This makes it available to our JavaScript code.

Add our JavaScript to the template

The recommended way to add JavaScript to pages is to create a web-resource module in your atlassian-plugin.xml file. Using this method, you can also specify dependencies on other web-resource modules, like AUI.

Add this to atlassian-plugin.xml:

Now edit the admin.vm template. Replace the existing $webResourceManager.requireResource call with:

Now we can write some JavaScript!

JavaScript for GETting the configuration

Populating our form is pretty easy. AUI supports jQuery, which we use to communicate with our REST resources. Start out by making a GET request to set the values of the input fields.

Create a file named admin.js in src/main/resources/, and add the following JavaScript code to the file:

This is pretty straightforward JavaScript, if you're familiar with jQuery. The AJS.toInit method is equivalent to using jQuery(function() {...}) or jQuery(document).ready(function() {...}), to have some JavaScript executed when the document is ready.

Its first task is to find the application base URL in the document that we added previously. Next, it queries the server to find the current config. Once done, it populates the attribute with our data.

Next we write the REST resource that serves the data.

Serve the configuration from a REST resource

The JavaScript we've added makes a request to a configuration resource for populating our form. Since it doesn't exist yet, we need to create this resource. But before we write the code for that, we need to add a few more dependencies in Maven. The Atlassian REST module uses Jersey, an implementation of the JAX-RS API. We'll add a dependency for this JAX-RS API. We'll also use JAXB to serialize our configuration object for us, to avoid having to manually construct JSON, so we need to add a dependency for that as well.

Open the pom.xml file and add these dependency elements to the dependencies section:

Now we can write our REST resource class.

Retrieve the configuration with GET

Again, start with a simple class and build it up from there.

Create a new file named ConfigResource.java in your project home directory:

src/main/java/com/atlassian/plugins/tutorial/xproductadminui/

Add the following:

This contains all the import statements we need, as well as a simple class constructor.

To populate the application web form, we issue a GET request to the resource that returns the data as JSON. JAXB serializes our configuration object for us, making life a bit easier.

First, add an inner class that encapsulates our configuration data.

Here we've created a class that will encapsulate a few properties, a String and an int. The @XmlRootElement and @XmlElement annotations are JAXB annotations. The @XmlRootElement annotation declares that instances of this type can be serialized to XML, and with a little magic in the REST module, to JSON as well. The @XmlElement annotations declare that the attributes should be treated as XML elements or JSON object properties.

If we were to serialize an instance of this class to XML, it would look like this:

And in JSON, like this:

Now let's implement the GET method, which returns a Response with an instance of the Config type we've just created as the entity.

This method first performs our now familiar user check.

We then construct a Response with an entity that is returned from a call to a funky transactionTemplate.execute method, providing it with an instance of TransactionCallback. If you've used Spring's TransactionTemplate, this should look familiar to you. For everyone spoiled by AOP, the summary is that we are accessing data that is coming from a database—well, it's not in Fisheye or Crucible, but work with me here!

To ensure that reads and writes don't clash and give us inconsistent data, we need to protect ourselves any time we access PluginSettings data. The TransactionTemplate frees us from needing to know the application-specific transaction creation and usage semantics. If we didn't use the TransactionTemplate, we'd need code something like this:

But TransactionTemplate takes care of managing the transaction for us, by calling TransactionCallback to perform the real work. The return value of the TransactionTemplate.execute method is the return value of calling TransactionCallback.doInTransaction.

Inside our TransactionCallback.doInTransaction method, we use the PluginSettingsFactory to create a reference to the global, application-wide settings data. We need to use the global settings object because we're configuring a plugin. If we were storing data associated with a project (in JIRA, Bamboo, Fisheye or Crucible) or a space (in Confluence), we'd instead use the PluginSettingsFactory.createSettingsForKey method, where the key is the project or space the configuration data should be associated with.

Once we have a reference to a PluginSettings object, things are pretty easy. PluginSettings can be thought of as a persistent map. Just use get to retrieve data and put to add or update values.

Namespace your keys!

Icon

Because this is global application configuration data, it is important that you do some kind of namespacing with your keys. Whether you use your plugin key, a class name or something else entirely is up to you. Just make sure it is unique enough that conflicts with other configuration data won't occur.

Step 6. Updating the configuration

Now that our form can be populated with our current configuration, we want to allow the admin user to update the configuration.

JavaScript for PUTting an updated configuration

Add the following function to admin.js, after the populateForm function definition but before the call to populateForm.

This is the function we call to update the server configuration. It's very minimal; we haven't included error checking and we really should be escaping quotes and other special characters when creating our data, but that is left as an exercise for the reader.

Now, we just need to hook that function up to our form. After the call to populateForm() in admin.js add:

Now, whenever the form is submitted, instead of the usual, default action of POSTing the form data, our updateConfig() method is called and our updated configuration is submitted as a PUT request to the configuration resource.

Update the configuration with PUT

To handle the PUT requests from the client, add this method to the ConfigResource.java file you created earlier:

The @Consumes annotation declares that we expect a request with a application/json body. We expect JSON in a form that matches the serialized form of the Config object we created earlier, so that the REST module can deserialize to a Config object, which it then passes to the put method.

Next, we do the authentication and authorization check to make sure no one can modify the configuration except admin users. Then, we wrap our modifications in TransactionCallback, which we pass to the transactionTemplate, just like we did when we were retrieving the configuration. Since PluginSettings is just a persistent map, we just use the put method to put the new configuration values into it and we're done.

Finally, we return 204 No Content, which tells the client that the update succeeded and there's nothing left to be said.

Wire things together

Finally, we need to add a few items to the atlassian-plugin.xml file. We have used a few new services from SAL, which we need to import. We also need to add the rest module, so that the plugin system knows about the REST resource and where to find it.

Add the following lines to atlassian-plugin.xml:

Step 7. Add menu items

Fortunately, the application attribute will come to our rescue (mostly)!

To add a link in the 'Global Settings' menu in JIRA's administration area, add the following web-item element.

To add a link in the 'Global Settings' menu in Fisheye's administration area, add the following web-item element.

Looking carefully, you'll notice that the label key attribute isn't really a key. This is because Fisheye doesn't do any internationalization yet, so the value of the key attribute will be the text that is displayed.

To add a link in the 'Plugins' menu in Bamboo's administration area, add the following web-item element.

Bamboo has the same limitation as Fisheye, so again the label element's key attribute is the textual value we want to display.

To add a link in the 'Configuration' menu in Confluence's administration area, add the following web-item element.

Astute readers will notice the suspicious lack of the application tag on that web-item element. Well, in Confluence, if you add the application attribute, your web-item won't show up at all. We'll leave it off here, which is safe because none of the other Atlassian applications have a system.admin/configuration section—at least, at the time of this writing they don't.

To add a link in the 'General menu' in the RefApp's administration area.

That's it! We're done! Let's run it!

Step 8. Build, install and run the plugin

Start your RefApp application again by entering the atlas-run command, or use a command below to try the application in another product. After the application starts, navigate to the plugin page by appending the plugin path to the base application URL. For example, for JIRA the path would be:

http://localhost:2990/jira/plugins/servlet/xproduct/admin

The startup commands are:

  • For JIRA

  • For Confluence

  • For Fisheye

  • For Bamboo

Congratulations, that's it

Icon

Have a chocolate!

RELATED TOPICS

About the Atlassian RefApp

SAL UserManager

WebResourceManager

AUI class attributes

 

 

 

15 Comments

  1. Hi,

    How can we render some parameters from servlet to velocity? I tried by adding this into AdminServlet.java file:

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException{
    ....
    Map params = new HashMap();
    params.put("name", "cychan");
    response.setContentType("text/html;charset=utf-8");
    renderer.render("admin.vm", params, response.getWriter());
    }
    

    and added this into admin.vm file:

    $params.keySet()
    #foreach( $key in $params.keySet() )
        <li>Key: $key -> Value: $params.get($key)</li>
    #end
    

    After re-running the plugin, it just can't render any value in velocity file. Could you please advise where could be my mistakes? How can I fix it?

    Thanks,

    Chai Ying

    1. Where did you get the renderer from? I usually start from the other end by displaying the contents of the ctx or action variables in Velocity and then working back to see where they were added in the action class.

      1. ahh...I see. It is working for me now. Thanks~

  2. I am trying to follow this tutorial and when I run via SDK (atlas run) I get the following:

    Full text message of error:

    ModuleDescriptor Cannot find ModuleDescriptor class for plugin of type 'component-import'

    I am trying to follow the 'Plugin tutorial - Writing an Admin Configuration Screen' and when I run the project via atlas-run I get the following exception:

    INFO talledLocalContainer 2011-02-25 13:41:49,520 ERROR main [atlassian.plugin.parsers.XmlDescriptorParModuleDescriptor Cannot find ModuleDescriptor class for plugin of type 'component-import'.
    INFO talledLocalContainer com.atlassian.plugin.PluginParseException: Cannot find ModuleDescriptor class for type 'component-import'.
    ....
    I am executing the command 'atlas-run' to test/run the tutorial.

    here's my atlassian-plugin.xml file
    ....
    <component-import key="userManager">
    <description>User Manager</description> 
    <interface>com.atlassian.sal.api.user.UserManager</interface>
    </component-import>
    <component-import key="loginUriProvider">
    <description>Login Uri Provider</description>
    <interface>com.atlassian.sal.api.auth.LoginUriProvider</interface>
    </component-import>
    <component-import key="renderer">
    <description>Renderer</description> 
    <interface>com.atlassian.templaterenderer.velocity.one.six.VelocityTemplateRenderer</interface>
    </component-import>
    ...

    Please help!!!  Any Suggestions?

    1. Hmm. Can you post the entire atlassian-plugin on pastie.org or something?

      1. before I post the code, has anyone tried this tutorial verbatim?  That is what I did and am having issues.  I suggest someone following this tutorial and the posting results.

  3. What are the java imports needed for the complete ConfigResource class ? (Very interesting tutorial by the way!) I am currently stuck there.

    Also, i am using JIRA 4.2.2 and the screen is not as nicely formatted as shown in the screenshot when I add the class="" in the HTML elements of the form. Is this normal ?

    1. I had to add the "field-group" class to the surounding div's to get the correct formating

  4. I can now see a nice HTML page and I also see the configuration link in the config menu, but saving and loading the config does not work.

    I see this error:

    A message body writer for Java type, class org.deblauwe.jira.plugin.databasevalues.adminui.ConfigResource$Config, and MIME media type, text/html, was not found
    

    Why is this? is there a zip file with the complete plugin to reference what I have done?

    1. Found the sources here: http://svn.atlassian.com/svn/public/contrib/tutorials/xproduct-admin-ui-plugin

      Seems there is an error in the page here

      This code:

      Should be:

      This makes it save the fields and read it correctly.

      One major annoyance: If you save the settings and navigate to another page, you get a warning:

      You have entered new data on this page. If you navigate away from this page without first saving your data, the changes will be lost.

      How can this be avoided?

      regards,

      Wim

  5. I tried getting this tutorial working for Confluence 3.5.2 and found that the decorators were removing the meta tag containing the baseUrl, causing all REST calls to fail.  A quick fix is to insert the following script tag right after body in admin.vm:

    And within the admin.js comment out the following:

    Hope that helps.

  6. Anonymous

    I am getting a 404 error when I do an atlas-run in step 2:

    type Status report

    message /refapp/

    description The requested resource (/refapp/) is not available.

    1. I got same error with latest SDK.

      Removing all test stuff(test related dependencies in pom.xml and /src/test directory)and atlas-clean did the job.

  7. The method getRemoteUsername(request) for the credential check in Step 3 is deprecated since SAL 2.10.
    Instead the following should be used:

     

    Cheers
    Martin