Script REST Endpoints
You can define REST endpoints in ScriptRunner, for example to:
-
use in dashboard gadgets
-
receive notifications from external systems
-
plug gaps in the official REST API
-
allow all your XHRs to proxy through to other systems
Adding a REST Endpoint
Navigate to Admin → REST Endpoints.
Click a heading to add a handler. Choose Custom endpoint to add your own endpoint.
REST endpoints are configured programatically. There is currently one sample defined (click Expand Examples), which we’ll discuss in-depth:
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
@BaseScript CustomEndpointDelegate delegate (1)
doSomething( (2)
httpMethod: "GET", groups: ["jira-administrators"] (3)
) { MultivaluedMap queryParams, String body -> (4)
return Response.ok(new JsonBuilder([abc: 42]).toString()).build() (5)
}
1 | - this line makes methods in your script recognisable as endpoints, and is required |
2 | - the name of the REST endpoint, which forms part of the URL, in this case doSomething |
3 | - configuration of the endpoint, in this case which HTTP verb to handle, and what groups to allow |
4 | - parameters which are provided to your method body |
5 | - the body of your method, where you will return a javax.ws.rs.core.Response object |
Once this is added to the list of configured endpoints, either as an inline script, or by copying it to a file and adding as a script file, you should be able to test the endpoint by visiting in your browser:
<jira_base_url>/rest/scriptrunner/latest/custom/doSomething
Alternatively using a command line utility:
>curl -u admin:admin http://localhost:8080/jira/rest/scriptrunner/latest/custom/doSomething {"abc":42}
If you are using a file, you can try changing the response… when you refresh the URL in your browser your code will be automatically recompiled and you will see the new response.
admin:admin corresponds to a username and password. |
Configuration
The general format of a method defining a REST endpoint is:
methodName (Map configuration, Closure closure)
For the configuration only the following options are supported:
httpMethod |
One of GET, POST, PUT, DELETE |
groups |
One or more groups. If the requesting user is in any of these groups the request will be allowed |
Note that either or both of these can be omitted. If you omit the groups attribute, the endpoint will be available to unauthenticated users.
The closure can take these parameters:
|
corresponds to the URL parameters |
|
The body of the request, for POST and PUT etc |
|
The request object. You can use this to get the requesting user for instance. |
You can use any of these forms for your closure:
something() { MultivaluedMap queryParams -> something() { MultivaluedMap queryParams, String body -> something() { MultivaluedMap queryParams, String body, HttpServletRequest request ->
depending on what you need access to.
Access request URL
Sometimes you may need to use the URL path after your method name, for instance in the following call, you want to retrieve the /foo/bar
:
<base_url>/rest/scriptrunner/latest/custom/doSomething/foo/bar
To get this, use the 3-param form of the closure definition, and call the getAdditionalPath
method from the base class:
doSomething() { MultivaluedMap queryParams, String body, HttpServletRequest request ->
def extraPath = getAdditionalPath(request)
// extraPath will contain /foo/bar when called as above
}
In previous versions, an extraPath variable was injected into the scripts, but this is not thread-safe - use the above method instead.
|
Script Root Scanning
As well as manually adding scripts or files via the UI, ScriptRunner will also scan your script roots for scripts that contain REST endpoints, and automatically register them. To enable this you need to set the property plugin.rest.scripts.package
to a comma-delimited list of packages, for example
-Dplugin.rest.scripts.package=com.acme.rest
On plugin enablement, scripts/classes under this package will be scanned and registered, providing the scripts contain the line:
@BaseScript CustomEndpointDelegate delegate
It will also scan script plugins. To enable scanning the entirety of all your script roots set the property to an empty string, ie:
-Dplugin.rest.scripts.package=
Examples
Create Priority Object
This example demonstrates plugging a gap in the official REST API. As at the time of writing, there is no way to create a new Priority object.
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.PriorityManager
import com.atlassian.jira.issue.fields.rest.json.beans.JiraBaseUrls
import com.atlassian.jira.issue.fields.rest.json.beans.PriorityJsonBean
import com.atlassian.jira.issue.priority.Priority
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript
import org.codehaus.jackson.map.ObjectMapper
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
@BaseScript CustomEndpointDelegate delegate
def priorityManager = ComponentAccessor.getComponent(PriorityManager)
def baseUrls = ComponentAccessor.getComponent(JiraBaseUrls)
ComponentManager.instance
priority(
httpMethod: "POST", groups: ["jira-administrators"]
) {MultivaluedMap queryParams, String body ->
def mapper = new ObjectMapper()
def bean = mapper.readValue(body, PriorityJsonBean)
assert bean.name // must provide priority name
assert bean.description // must provide priority description
assert bean.iconUrl // must provide priority icon url
assert bean.statusColor // must provide priority statusColor
Priority priority
try {
priority = priorityManager.createPriority(bean.name, bean.description, bean.iconUrl, bean.statusColor)
} catch (e) {
return Response.serverError().entity([error: e.message]).build();
}
return Response.created(new URI("/rest/api/2/priority/${priority.id}")).build();
}
Most of this code is involved with validating the JSON that is passed to it, that is ensuring that all required fields for a priority are present. We use the appropriate method on the Response class to send the right status code, eg 500
(server error) if the priority already exists, and 201
(created) if we create the priority.
To test it you could use
curl -X POST -H "Content-type: text/json" -u admin:admin --data "@priority.json" \ <jira_base_url>/rest/scriptrunner/latest/custom/priority
where priority.json
is a text file containing:
{
"statusColor": "#009900",
"description": "Major loss of function.",
"iconUrl": "/images/icons/priorities/lowest.png",
"name": "Cosmetic"
}
Also note that you can have multiple methods with the same name in the same file, which is useful to do simple CRUD REST APIs, eg:
POST /priority - creates a priority PUT /priority - updates a priority DELETE /priority - deletes a priority GET /priority - gets a priority
Get the user making the request
We make use of com.atlassian.sal.api.user.UserManager
to get the current user from the http request.
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.sal.api.user.UserManager
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.Response
@BaseScript CustomEndpointDelegate delegate
getCurrentUser() { queryParams, body, HttpServletRequest request ->
def userManager = ComponentAccessor.getOSGiComponentInstanceOfType(UserManager)
def userProfile = userManager.getRemoteUser(request)
return Response.ok(new JsonBuilder([currentUser: userProfile?.username]).toString()).build();
}
For how-to questions please ask on Atlassian Answers where there is a very active community. Adaptavist staff are also likely to respond there.
Ask a question about ScriptRunner for JIRA, for for Bitbucket Server, or for Confluence.