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.
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.
The pom.xml
has been altered to provide necessary dependencies for JAX-WS API, Servlet API and some testing Dependencies.
The atlassian-plugin.xml
has been altered to register the REST Resource to listen to the baseUrl http://server/jira/rest/kitchenduty/1.0/*
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.
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.
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.
<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>
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).
<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
.
kitchen-duty-plugin.rest.resources.name=Kitchen Duty Resources
kitchen-duty-plugin.rest.resources.description=All Kitchen Duty REST Resources
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.
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;
}
}
@Named
on classlevel.
/user
, therefore we use the @Path
annotation.
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.
@Inject
for constructor injection, so that our UserSearchService will be injected by Spring on bean creation.
@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
.
@AnonymousAllowed
.
So you can curl the URL easily and will get an ok
as response.
@Path("/search")
. We do NOT use @AnonymousAllowed
here
because we don't want unauthenticated users search our user database.
@QueryParam("query")
which leads to the following URL for our endpoint
http://localhost:2990/jira/rest/kitchenduty/1.0/user/search?query=foo
.
query
param, so we stick to this default behaviour.
@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.
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.
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.
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;
}
}
@XmlElement
on the attribute level and not on the getters/setters.
text
.
id
.
[ { "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.
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;
...
}
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.
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.
[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.
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);
}
}
@Before
phase of our test.
HTTP 401
.
adm
and expect to get admin
as result.
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.
[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] ------------------------------------------------------------------------
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:
Since we provided ?query=adm
we want a list of all user starting with adm
which is currently only admin
.
The graphic shows marked green which components we implemented in this section.