User Search REST Resource

We will now create the User Search REST Resource so that our JS-Controller can search for usernames.

What is a REST Resource? It is simply just a basic Controller (MVC) that reacts to certain HTTP Requests with a specific response.

Atlassian uses JAX-WS and JAXB for it's built in REST Resources. The actual implementation is hidden from us, and we don't really care about it since we just need to know that somewhere inside JIRA are OSGi bundles which provide the JAX-WS and JAXB implementation.

Ok, enough of OSGi and theoretical jibber jabber! Let's create our REST Resource with an Atlassian SDK command, like we did create the Webwork Action before.

Execute the command and choose 14: REST Plugin Module and fill out the prompts as stated below.

atlas-create-jira-plugin-module
Executing: /Applications/Atlassian/atlassian-plugin-sdk-6.1.0/apache-maven-3.2.1/bin/mvn com.atlassian.maven.plugins:maven-jira-plugin:6.1.2:create-plugin-module -gs /Applications/Atlassian/atlassian-plugin-sdk-6.1.0/apache-maven-3.2.1/conf/settings.xml
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=256M; support was removed in 8.0
[INFO] Scanning for projects...
[INFO]
[INFO] Using the builder org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder with a thread count of 1
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building kitchen-duty-plugin 1.0.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-jira-plugin:6.1.2:create-plugin-module (default-cli) @ kitchen-duty-plugin ---
Choose Plugin Module:
1:  Component Import
...
14: REST Plugin Module
15: RPC Endpoint Plugin
...
Choose a number (...): 14

Enter New Classname MyRestResource: : UserSearchResource
Enter Package Name io.codeclou.rest: : io.codeclou.kitchen.duty.rest
Enter REST Path /usersearchresource: : /kitchenduty
Enter Version 1.0: : 1.0

Show Advanced Setup? (Y/y/N/n) N: : y

Module Name User Search Resource: : Kitchen Duty Resources
Module Key user-search-resource: : kitchen-duty-resources
Module Description The User Search Resource Plugin: : All Kitchen Duty REST Resources
i18n Name Key user-search-resource.name: : kitchen-duty-plugin.rest.resources.name
i18n Description Key user-search-resource.description: : kitchen-duty-plugin.rest.resources.description
Add Package To Scan? (Y/y/N/n) N: : y
Enter Package: io.codeclou.kitchen.duty.rest
...

Add Package To Scan? (Y/y/N/n) N: : n

Add Dispatcher? (Y/y/N/n) N: : n

[INFO] Adding the following items to the project:
[INFO]   [class: io.codeclou.kitchen.duty.rest.UserSearchResourceModel]
[INFO]   [class: io.codeclou.kitchen.duty.rest.UserSearchResource]
[INFO]   [class: it.io.codeclou.kitchen.duty.rest.UserSearchResourceFuncTest]
[INFO]   [class: ut.io.codeclou.kitchen.duty.rest.UserSearchResourceTest]
[INFO]   [dependency: com.atlassian.plugins.rest:atlassian-rest-common]
[INFO]   [dependency: com.atlassian.sal:sal-api]
[INFO]   [dependency: javax.servlet:servlet-api]
[INFO]   [dependency: javax.ws.rs:jsr311-api]
[INFO]   [dependency: javax.xml.bind:jaxb-api]
[INFO]   [dependency: org.apache.wink:wink-client]
[INFO]   [dependency: org.mockito:mockito-all]
[INFO]   [module: rest]
[INFO]   i18n strings: 2

Add Another Plugin Module? (Y/y/N/n) N: : n

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:52 min
[INFO] Finished at: 2016-01-16T20:07:35+01:00
[INFO] Final Memory: 24M/328M
[INFO] ------------------------------------------------------------------------

The SDK created and updated many files now and before we get into all the details let's fire up atlas-run again and see what has been created for us.

Once JIRA is started browse to http://server/jira/rest/kitchenduty/1.0/message and you should see the following.

That's nice. But now we need to understand what is going on. So here is a short recap of what has been created.

  1. The pom.xml has been altered to provide necessary dependencies for JAX-WS API, Servlet API and some testing Dependencies.

  2. The atlassian-plugin.xml has been altered to register the REST Resource to listen to the baseUrl http://server/jira/rest/kitchenduty/1.0/*

  3. There now is a UserSearchResource.java which is the actual REST Controller which delivers the response.

    In addition to the REST Controller there is UserSearchResourceModel.java which is the Model which is delivered as XML in the REST Controllers response.

  4. An unit-test UserSearchResourceTest and integration-test UserSearchResourceFuncTest file has be created.

We will now get into all the details and implement the needed features for the user search on the fly.

1. Dependencies (pom.xml)

The pom.xml has these new lines which are quite self explanatory. If you want to read everything in detail read the REST Plugin Module documentation.

pom.xml
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.4</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.1</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.atlassian.plugins.rest</groupId>
    <artifactId>atlassian-rest-common</artifactId>
    <version>1.0.2</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.apache.wink</groupId>
    <artifactId>wink-client</artifactId>
    <version>1.1.3-incubating</version>
    <scope>test</scope>
</dependency>

2. REST Resources Handler (atlassian-plugin.xml)

As already mentioned the atlassian-plugin.xml has been altered to register the REST Resource to listen to the baseUrl http://server/jira/rest/kitchenduty/1.0/*.

The path and version build the baseurl relative to http://server/jira/rest/*.

package will enable Component Scan (which I think is redundant, but doesn't hurt).

src/main/resources/.../atlassian-plugin.xml
<rest name="Kitchen Duty Resources"
      i18n-name-key="kitchen-duty-plugin.rest.resources.name"
      key="kitchen-duty-resources"
      path="/kitchenduty"
      version="1.0">
  <description key="kitchen-duty-plugin.rest.resources.description">All Kitchen Duty REST Resources</description>
  <package>io.codeclou.kitchen.duty.rest</package>
</rest>

You already know i18n-name-key and the other attributes from the Webwork Action and therefore are not wondering that there are some new i18n key/value pairs inside kitchen-duty-plugin.properties.

src/main/resources/.../kitchen-duty-plugin.properties
kitchen-duty-plugin.rest.resources.name=Kitchen Duty Resources
kitchen-duty-plugin.rest.resources.description=All Kitchen Duty REST Resources

3. REST Controller and Model

In the directory src/main/java/ in the package io.codeclou.kitchen.duty.rest there is now the UserSearchResource.java and the UserSearchResourceModel.java. You can see what has been created in the GitHub commit 95b8ebf.

We will change the code to provide GET Endpoint to search usernames.

src/main/java/com/codeclou/kitchen/duty/rest/.../UserSearchResource.java
package io.codeclou.kitchen.duty.rest;

import com.atlassian.jira.bc.user.search.UserSearchParams;
import com.atlassian.jira.bc.user.search.UserSearchService;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;

import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;

@Named 
@Path("/user")  
public class UserSearchResource {

    private UserSearchService userSearchService; 

    @Inject 
    public UserSearchResource(UserSearchService userSearchService) {
        this.userSearchService = userSearchService;
    }

    
    public UserSearchResource() {
    }

    @GET
    @Path("/health") 
    @Produces({MediaType.APPLICATION_JSON})
    @AnonymousAllowed 
    public Response health() {
        return Response.ok("ok").build();
    }

    /**
     * Call from select2 JS plugin
     * Response needs to look like this:
     * [{ 'id': 1, 'text': 'Demo' }, { 'id': 2, 'text': 'Demo 2'}]
     */
    @GET
    @Path("/search") 
    @Produces({MediaType.APPLICATION_JSON})
    public Response searchUsers(@QueryParam("query") final String userQuery, 
                                @Context HttpServletRequest request ) {
        List<UserSearchResourceModel> users = findUsers(userQuery);
        return Response.ok(users).build(); 
    }

    private List<UserSearchResourceModel> findUsers(String query) {
        List<UserSearchResourceModel> userSearchResourceModels =
                                           new ArrayList<UserSearchResourceModel>();
        UserSearchParams searchParams = UserSearchParams.builder()
            .includeActive(true)
            .sorted(true)
            .build();
        List<String> users = userSearchService.findUserNames(query, searchParams); 
        if (users != null) {
            for (String user : users) {
                userSearchResourceModels.add(new UserSearchResourceModel(user, user));
            }
        }
        return userSearchResourceModels;
    }
}
Our REST Controller should be a Spring-Bean and get its dependencies autowired. Therefore provide the JSR annotation @Named on classlevel.
We want that all REST Endpoints of our REST Controller will have an URL-prefix /user, therefore we use the @Path annotation.
The UserSearchService is a JIRA Service which will provide us methods to search for users. This Service needs to be imported from a foreign OSGi bundle, we will see about that later. Why not use @ComponentImport here you ask? Well try and if it works for you that would be nice. But I am getting some strange WADLGenerator Exceptions when running atlas-integration-test. Therefore I use a little trick and import the component somewhere else. Once it is imported somewhere inside our bundle we can use it here. We will see about that later.
We use @Inject for constructor injection, so that our UserSearchService will be injected by Spring on bean creation.
We provide a default constructor just in case.
Since our user search Endpoint will be rather complex we want a simple health endpoint to test the REST API in an easy way. Therefore we define a Method as @GET with @Path("/health") which produces JSON @Produces({MediaType.APPLICATION_JSON}). This endpoint will be reachable via http://localhost:2990/jira/rest/kitchenduty/1.0/user/health.
The health endpoint will be reachable without authentication via @AnonymousAllowed. So you can curl the URL easily and will get an ok as response.
Alright now we code the actual user search Endpoint with @Path("/search"). We do NOT use @AnonymousAllowed here because we don't want unauthenticated users search our user database.
As we want to search for username, somehow our search keyword needs to be passed as URL-parameter. This is done via @QueryParam("query") which leads to the following URL for our endpoint http://localhost:2990/jira/rest/kitchenduty/1.0/user/search?query=foo.

Why use QueryParam instead of PathParam you ask? I see you like nice URL-patterns and so do I, but we will see later that the AUI select2 widget will send the search keyword as query param, so we stick to this default behaviour.
With @Context HttpServletRequest request we get the HTTP-Request as paramater and could for example extract the authenticated user. We don't need it now but we might soon. @Context ensures that JIRA context (I think special headers and stuff) is wired into the request.
After we searched for users we build the response with Response.ok(someObject).build(). The JAXB/JAX-WS implementation ensures that our Model (we will see about that later) is converted to JSON and that a HTTP 200 is send together with the JSON representation of our Object as response body.
We provide a private method that searches for usernames using the UserSearchService. If your OSGi component imports are not working correctly you will get a NullPointerException here. After we searched the usernames we convert the username list into our model UserSearchResourceModel which will be serialized as JSON response body.

Now we have the REST Controller but you will have some compile errors now because the Model needs to be implemented.

src/main/java/com/codeclou/kitchen/duty/rest/.../UserSearchResourceModel.java
package io.codeclou.kitchen.duty.rest;

import javax.xml.bind.annotation.*;
@XmlRootElement(name = "users") 
@XmlAccessorType(XmlAccessType.FIELD) 
public class UserSearchResourceModel {

    @XmlElement 
    private String text;

    @XmlElement 
    private String id;

    public UserSearchResourceModel() {
    }

    public UserSearchResourceModel(String text, String id) {
        this.text = text;
        this.id = id;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}
Currently we only use JSON and have disabled XML as response format. But if you someday want to use XML you will need a name for the container holding users.
This basically tells the compiler to look for @XmlElement on the attribute level and not on the getters/setters.
Since the select2 widget from AUI wants to have a certain JSON format we just stick to that and define a property called text.
The same thing for the property called id.
Ok this will render us the following JSON format once we use it:
[ { "id": "foo", "text": "foo" }, { "id": "bar", "text": "bar" } ]

Ok we could run atlas-run now and see what we get, but as I told you we need to take care of the @ComponentImport of UserSearchService first. Since it doesn't matter where in our plugin we import foreign Interfaces we do it in the "dummy"-component so our imports will be available everywhere in our plugin.

src/main/java/com/codeclou/kitchen/duty/impl/.../MyPluginComponentImpl.java
package io.codeclou.kitchen.duty.rest;

import com.atlassian.jira.bc.user.search.UserSearchService;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
...

@Named ("myPluginComponent")
public class MyPluginComponentImpl implements MyPluginComponent {
   ...
   @ComponentImport
   private UserSearchService userSearchService;
   ...
}

4. Unit- and Integrationtests

Now that we have our code ready we need to take care of the Unit- and Integration-tests.

First we will change the Unitest which reside in /src/test/java/* to something that makes sense. So we mock the UserSearchService and provide two users bob and sue as mocks. Now we test if our searchUser Method works as expected.

src/test/java/ut/com/codeclou/kitchen/duty/rest/.../UserSearchResourceTest.java
package ut.io.codeclou.kitchen.duty.rest;

import com.atlassian.jira.bc.user.search.UserSearchParams;
import com.atlassian.jira.bc.user.search.UserSearchService;
import io.codeclou.kitchen.duty.rest.UserSearchResource;
import io.codeclou.kitchen.duty.rest.UserSearchResourceModel;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class UserSearchResourceTest {

    @Before
    public void setup() {

    }

    @After
    public void tearDown() {

    }

    @Test
    public void messageIsValid() {
        final List<String> mockedUsers = new ArrayList<>();
        mockedUsers.add("bob");
        mockedUsers.add("sue");

        UserSearchService mockedUserSearchService = Mockito.mock(UserSearchService.class);
        when(mockedUserSearchService.findUserNames(anyString(), any(UserSearchParams.class))).thenReturn(mockedUsers);

        UserSearchResource resource = new UserSearchResource(mockedUserSearchService);

        Response response = resource.searchUsers("bo", null);
        final List<UserSearchResourceModel> users = (List<UserSearchResourceModel>) response.getEntity();

        assertEquals("should contain bob", "bob", users.get(0).getText());
    }
}

Now we can run atlas-unit-test to run all our Unittests.

atlas-unit-test
[INFO] ------------------------------------------------------------------------
[INFO] Building kitchen-duty-plugin 1.0.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-jira-plugin:6.1.2:compress-resources (default-compress-resources) @ kitchen-duty-plugin ---
[INFO] Compiling javascript using YUI
...

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running ut.io.codeclou.kitchen.duty.MyComponentUnitTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.051 sec
Running ut.io.codeclou.kitchen.duty.rest.UserSearchResourceTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.148 sec
Running ut.io.codeclou.kitchen.duty.webwork.KitchenDutyPlanningWebworkActionTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.477 s
[INFO] Finished at: 2016-03-11T19:34:06+01:00
[INFO] Final Memory: 25M/320M
[INFO] ------------------------------------------------------------------------

Now that Unittests are working let us modify the created Integrationtest.

src/test/java/it/com/codeclou/kitchen/duty/rest/.../UserSearchResourceFuncTest.java
package it.io.codeclou.kitchen.duty.rest;

import org.apache.wink.client.ClientConfig;
import org.apache.wink.client.Resource;
import org.apache.wink.client.RestClient;
import org.apache.wink.client.handlers.BasicAuthSecurityHandler;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class UserSearchResourceFuncTest {

    private String baseUrl;

    @Before
    public void setup() {
        baseUrl = System.getProperty("baseurl"); 
    }

    @After
    public void tearDown() {

    }

    @Test
    public void testSearchUser__unAuthorized() { 
        String resourceUrl = baseUrl + "/rest/kitchenduty/1.0/user/search?query=adm";
        String response = httpGet(resourceUrl);
        assertNotNull("should not be null", response);
        assertEquals("should contain unauthorized message",
            toJSON("{'message':'Client must be authenticated to access this resource.','status-code':401}"),
            response);
    }
    @Test
    public void testSearchUser__authorized() { 
        String resourceUrl = baseUrl + "/rest/kitchenduty/1.0/user/search?query=adm";
        String response = httpGet(resourceUrl, "admin", "admin");
        assertNotNull("should not be null", response);
        assertEquals("should contain admin",
            toJSON("[{'text':'admin','id':'admin'}]"),
            response);
    }

    private String toJSON(String text) { 
        return text.replaceAll("'", "\"");
    }

    private String httpGet(String url) { 
        return _httpGet(url, null);
    }

    private String httpGet(String url, String username, String password) { 
        ClientConfig config = new ClientConfig();
        BasicAuthSecurityHandler basicAuthSecHandler = new BasicAuthSecurityHandler();
        basicAuthSecHandler.setUserName(username);
        basicAuthSecHandler.setPassword(password);
        config.handlers(basicAuthSecHandler);
        return _httpGet(url, config);
    }

    private String _httpGet(String url, ClientConfig config) { 
        RestClient client = new RestClient();
        if (config != null) {
            client = new RestClient(config);
        }
        Resource resource = client.resource(url);
        return resource
            .header("Accept", "application/json;q=1.0")
            .get()
            .getEntity(String.class);
    }
}
Since we will be running a special SDK command to execute our Integrationtests the SDK will provide a system property with the base URL to JIRA. We set that during @Before phase of our test.
Our first test should test authentication, therefore we call our REST Resource without credentials and expect a HTTP 401.
Our second test should test a valid response authenticated wit the user admin. We search for adm and expect to get admin as result.
This is just a little helper method to replace single quotes to double quotes, so that we can easily write JSON with single quotes in our tests.
A convenience Method to fire a HTTP GET request without authentication.
A convenience Method to fire a HTTP GET request with authentication (basic auth).
The low-level HTTP GET method using the wink RestClient. You will need to write more of it if your tests get more complex, but for us this is enough for now.

Now we can run atlas-integration-test to run all our Integrationtests. You might want to get another coffee during that run, it takes some time.

atlas-integration-test
[INFO] ------------------------------------------------------------------------
[INFO] Building kitchen-duty-plugin 1.0.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-jira-plugin:6.1.2:compress-resources (default-compress-resources) @ kitchen-duty-plugin ---
[INFO] Compiling javascript using YUI
[INFO] 0 Javascript file(s) were minified into target directory /Users/bg/git-work/kitchen-duty-MASTER/step-02-kitchen-duty-plugin/target/classes
[INFO] 0 CSS file(s) were minified into target directory /Users/bg/git-work/kitchen-duty-MASTER/step-02-kitchen-duty-plugin/target/classes
[INFO] Compressing XML files
[INFO] 0 XML file(s) were minified into target directory /Users/bg/git-work/kitchen-duty-MASTER/step-02-kitchen-duty-plugin/target/classes
[INFO]
....
[INFO] --- atlassian-spring-scanner-maven-plugin:1.2.6:atlassian-spring-scanner (default) @ kitchen-duty-plugin ---
[INFO] Starting Atlassian Spring Byte Code Scanner...
[INFO]

... 4 million lines later ...

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.codeclou.kitchen.duty.MyComponentWiredTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.248 sec
Running it.io.codeclou.kitchen.duty.rest.UserSearchResourceFuncTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.018 sec

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

[INFO] jira: Shutting down
[INFO] using codehaus cargo v1.4.7
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:10 min
[INFO] Finished at: 2016-03-11T19:38:26+01:00
[INFO] Final Memory: 32M/437M
[INFO] ------------------------------------------------------------------------

Finally let's do a manual test

Alright now it is time to startup and see what we get. Run the plugin with atlas-run and then we use Curl to search for users. Wait for atlas-run to startup JIRA and then fire a search request against our User Search REST Resource:

curl --user admin:admin http://localhost:2990/jira/rest/kitchenduty/1.0/user/search?query=adm
[{"text":"admin","id":"admin"}]

Since we provided ?query=adm we want a list of all user starting with adm which is currently only admin.

You have completed Step 2. Good Job! You can check out this step's solution on GitHub

The graphic shows marked green which components we implemented in this section.