Atlassian makes a Java REST client library to work with Jira Server, but what if you want to write code to talk to Jira Cloud? The APIs are different enough between Jira Server and Jira Cloud that the Jira Server client library does not work with Jira Cloud.
In this post I’ll take you through the process of generating Java code from the Open API specification that Atlassian publishes (using swagger-codegen), and then writing a simple Java application that creates an issue in Jira Cloud.
If you go to the documentation for the Jira Cloud REST API at https://developer.atlassian.com/cloud/jira/platform/rest/v3/ you’ll see the “…” in the top right. Click that, and you’ll see the link for “Download OpenAPI Spec”. This is a JSON file that describes the REST API for Jira Cloud.
The actual link to this file is https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json. Although swagger-codegen can generate APIs directly from a URL, for now we’ll save a copy locally.
Swagger is a way of documenting APIs in a standard way. I won’t discuss that here, but you can read more at https://swagger.io/
I am going to use the swagger-codegen tool to generate my client code. You can build this from source, but I just installed a version on my Mac using “brew install swagger-codegen”. Read more at https://github.com/swagger-api/swagger-codegen
In theory this is the minimum command you’d need to generate some source code in the current directory from the spec:
swagger-codegen generate \
-i https://developer.atlassian.com/.../swagger-v3.v3.json \
-l java \
-o .
If you run that now, you’ll find that there are some things about the spec that swagger-codegen doesn’t like.
I’m going to edit the swagger-v3.v3.json file I downloaded to make it work better with swagger-codegen.
The first problem is that there are a series of valid operators you can use in JQL. When swagger-codegen writes an enum to represent those values it gets confused, as the values are things like “>” and “~”.
The operator attribute for FieldValueClause is a string type, which is fine. It is the enum that swagger-codegen doesn’t like. I’m going to edit the spec to say that the operator is simply a string.
I have added formatting for readability, but in this file there is no formatting. It’s one long line!
Edit swagger-v3.v3.json, and search for:
"FieldValueClause": {
"required": [
"field","operand","operator"
]
Shortly after this you’ll see:
"operator": {
"type": "string",
"description": "The operator between the field and operand.",
"enum": [
"=","!=",">","<",">=","<=","in","not in",null,"~=","is","is not"
]
}
If you remove the enum section so that this section looks like this:
"operator": {
"type": "string",
"description": "The operator between the field and operand."
}
Don’t forget to remove the comma before “enum”.
The other problem is that the swagger file has an empty string for the server URL. You’ll find that swagger-codegen doesn’t like this. You can put your server URL in there, or you can just put any URL in there and supply the server URL in your code. I’m going to do the latter.
Near the top of the file you will find this. (Again, I have added formatting here for readability.)
"servers": [ {
"url": ""
} ]
Just put a valid URL in there, like this:
"servers": [ {
"url": "https://mysite.atlassian.net"
} ]
This problem won’t cause your code generation to fail, but it might cause the code not to compile later. The spec lists 4 values for project type, but practically there are only 2. You can fix it here, or you can just fix it in your generated code later if you find it doesn’t compile.
Again I have added formatting for clarity.
Find this section:
"enum": [
"CLASSIC",
"NEXTGEN",
"classic",
"next-gen"
]
Change it to read:
"enum": [
"classic",
"next-gen"
]
Now I’m almost ready. I didn’t want the default configuration, so I created a config.json for swagger-codegen. You’ll probably want to do the same, as you’ll want to customise the package names, and the Maven artifact/group names.
Create a file called config.json like this:
{
"modelPackage": "com.atlassian.jira.rest.client.model",
"apiPackage": "com.atlassian.jira.rest.client.api",
"invokerPackage": "com.atlassian.jira.rest.client",
"library": "jersey2",
"groupId": "com.atlassian.jira.rest",
"artifactId": "client"
}
You might want to change which client library it uses. Check the documentation for swagger-codegen. I’m using the “jersey2” flavour.
Some of the class and method names that are auto-generated are a bit difficult to deal with. You can customise swagger-codegen in many ways, but I won’t go into detail here.
Let’s run it. Now our swagger-codegen invocation looks like:
swagger-codegen generate \ -i ./swagger-v3.v3.json \ -l java \ -o ~/src/jira-client -c ./config.json
This generates a Maven project under ~/src/jira-client. You will want to customise the pom.xml a bit so suit your development practices, and how you build things that are going to use this code.
A suggestion: Commit the generated code into your version control, along with your config.json, AND the command you used to run it. This way, in the future when the API changes, if you re-generated the code, it’s easy to see what changed.
To test, I opened up the generated project directly in my IDE, and created a Main class.
In this example I’m going to look up my user, find a project, find the bug issue type for that project, and create an issue.
public static void main(String[] args) throws ApiException {
ApiClient apiClient = new ApiClient();
apiClient.setUsername("myemail@mycompany.com");
apiClient.setPassword("abcde My app token abcde");
apiClient.setDebugging(true);
apiClient.setBasePath("https://myjira.atlassian.net");
// Look up my user
MyselfApi myselfApi = new MyselfApi(apiClient);
User user = myselfApi.comAtlassianJiraRestV2IssueCurrentUserResourceGetCurrentUserGet(null);
// Find the project called "GDPR"
ProjectsApi projectsApi = new ProjectsApi(apiClient);
PageBeanProject projects = projectsApi.comAtlassianJiraRestV2IssueProjectResourceSearchProjectsGet(
null, // startAt
null, // maxResults
null, // orderBy
"GDPR", // query
null, // typeKey
null, // categoryId
null, // searchBy
null, // action
"issueTypes", // expand issue types
null // status
);
if (projects.getValues().size() > 0) {
// Find the first project
Project project = projects.getValues().get(0);
String issueTypeId = "0";
// Find the bug issue type
for (IssueTypeDetails issueTypeDetails : project.getIssueTypes()) {
if (issueTypeDetails.getName().equalsIgnoreCase("bug")) {
issueTypeId = issueTypeDetails.getId();
}
}
// Create an issue
IssuesApi issuesApi = new IssuesApi(apiClient);
Map<String, Object> issueCreateParams = new HashMap<>();
Map<String, Object> fields = new HashMap<>();
fields.put("summary", "Create a test issue");
Map<String, Object> issueTypeField = new HashMap<>();
issueTypeField.put("id", issueTypeId);
fields.put("issuetype", issueTypeField);
Map<String, Object> projectField = new HashMap<>();
projectField.put("id", project.getId());
fields.put("project", projectField);
// Assign to me
Map<String, Object> assigneeField = new HashMap<>();
assigneeField.put("accountId", user.getAccountId());
fields.put("assignee", assigneeField);
issueCreateParams.put("fields", fields);
CreatedIssue createdIssue = issuesApi.comAtlassianJiraRestV2IssueIssueResourceCreateIssuePost(issueCreateParams, true);
System.out.println("Created an issue, and assigned it to " + user.getDisplayName());
System.out.println("The issue key is " + createdIssue.getKey());
}
}
Ben Kelley
Senior Java Developer
Atlassian
Sydney
5 accepted answers
36 comments