REST API is returning only 20 worklogs per issue

Dejan Nedic August 27, 2013

We are using the REST API to retrieve some information from JIRA for reporting purposes. Basically, we are getting the number of reported hours per project/project category in a specific time period.

Recently we have noticed that the numbers of reported hours are not correct i.e. that those are smaller than the real numbers in JIRA. The analysis has shown that the getWorklogs method called on an issue is returning only up to 20 worklogs. This looks like some kind of pagination, but we were not able to find this limitation anywhere in the documentation.

Can somebody please shed some light on this?

4 answers

1 accepted

4 votes
Answer accepted
fmeheus September 11, 2013

We had the same problem.

The restAPI (/rest/api/2/issue/{issueIdOrKey}) that is behind de IssueRestClient contains max. 20 worklogs per issue.

Te same for the SearchRestClient: the restAPI (/rest/api/2/search) also returns max. 20 worklogs per issue.

The only restAPI, that returns all the worklogs, is this one: /rest/api/2/issue/{issueIdOrKey}/worklog. Unfortunately, there is no corresponding Java RestClient in JRJC.

After looking into the JRJC code, I created some new classes, based on the existing code.

First I created a new IssueWorklogsRestClient interface with its class. There is one method getIssueWorklogs(BasicIssue issue) that returns a List of Worklogs per issue:

public interface IssueWorklogsRestClient 
{
    Promise<List<Worklog>> getIssueWorklogs(BasicIssue issue);
}

public class AsynchronousIssueWorklogsRestClient extends AbstractAsynchronousRestClient implements IssueWorklogsRestClient
{
    private final WorklogsJsonParser worklogsParser = new WorklogsJsonParser();
    private final URI baseUri;

    public AsynchronousIssueWorklogsRestClient(final URI baseUri, final HttpClient client)
    {
        super(client);
        this.baseUri = baseUri;
    }
  
    @Override
    public Promise<List<Worklog>> getIssueWorklogs(final BasicIssue issue)
    {
        final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri);
        uriBuilder.path("issue").path(issue.getKey()).path("worklog");
        worklogsParser.setIssue(issue);
        return getAndParse(uriBuilder.build(), worklogsParser);
    }
}


I also created a new WorklogsJsonParser, based on the existing IssueJsonParser

public class WorklogsJsonParser implements JsonObjectParser<List<Worklog>>
{
    private BasicIssue basicIssue;

    public void setIssue(BasicIssue issue)
    {
        this.basicIssue = issue;
    }

    @Nullable
    private <T> Collection<T> parseOptionalArray(final JSONObject json, final JsonWeakParser<T> jsonParser, final String... path)
            throws JSONException
    {
        final JSONArray jsonArray = JsonParseUtil.getNestedOptionalArray(json, path);
        if (jsonArray == null)
        {
            return null;
        }
        final Collection<T> res = new ArrayList<T>(jsonArray.length());
        for (int i = 0; i < jsonArray.length(); i++)
        {
            res.add(jsonParser.parse(jsonArray.get(i)));
        }
        return res;
    }

    @Override
    public List<Worklog> parse(JSONObject s) throws JSONException
    {
        final Collection<Worklog> worklogs;
        final URI selfUri = basicIssue.getSelf();
        worklogs = parseOptionalArray(s, new JsonWeakParserForJsonObject<Worklog>(new WorklogJsonParserV5(selfUri)), WORKLOGS_FIELD.id);
       
        if(worklogs==null)
        {
            return Collections.<Worklog>emptyList();
        }
        return new ArrayList(worklogs);
    }

    private static class JsonWeakParserForJsonObject<T> implements JsonWeakParser<T>
    {
        private final JsonObjectParser<T> jsonParser;

        public JsonWeakParserForJsonObject(JsonObjectParser<T> jsonParser)
        {
            this.jsonParser = jsonParser;
        }

        private <T> T convert(Object o, Class<T> clazz) throws JSONException
        {
            try
            {
                return clazz.cast(o);
            } catch (ClassCastException e)
            {
                throw new JSONException("Expected [" + clazz.getSimpleName() + "], but found [" + o.getClass().getSimpleName() + "]");
            }
        }

        @Override
        public T parse(Object o) throws JSONException
        {
            return jsonParser.parse(convert(o, JSONObject.class));
        }
    }

    private interface JsonWeakParser<T>
    {
        T parse(Object o) throws JSONException;
    }
}


finally I created a subclass+interface of the existing JiraRestClient that creates the new IssueWorklogsClient

public interface JiraRestClientPlus extends JiraRestClient
{
    IssueWorklogsRestClient getIssueWorklogRestClient();
}

public class AsynchronousJiraRestClientPlus extends AsynchronousJiraRestClient implements JiraRestClientPlus
{
    private final IssueWorklogsRestClient issueWorklogsRestClient;
   
    public AsynchronousJiraRestClientPlus(final URI serverUri, final DisposableHttpClient httpClient)
    {
        super(serverUri,httpClient);
        final URI baseUri = UriBuilder.fromUri(serverUri).path("/rest/api/latest").build(); // '/rest/api/latest' or '/rest/api/2'
        issueWorklogsRestClient = new AsynchronousIssueWorklogsRestClient(baseUri, httpClient);
    }
   
    @Override
    public IssueWorklogsRestClient getIssueWorklogRestClient()
    {
        return issueWorklogsRestClient;
    }
}


+ a subclass of the existing AsynchronousJiraRestClientFactory to create the new JiraRestClientPlus

public class AsynchronousJiraRestClientFactoryPlus extends AsynchronousJiraRestClientFactory
{
    @Override
    public JiraRestClientPlus create(final URI serverUri, final AuthenticationHandler authenticationHandler)
    {
        final DisposableHttpClient httpClient = new AsynchronousHttpClientFactory()
                .createClient(serverUri, authenticationHandler);
        return new AsynchronousJiraRestClientPlus(serverUri, httpClient);
    }

    @Override
    public JiraRestClientPlus createWithBasicHttpAuthentication(final URI serverUri, final String username, final String password)
    {
        return create(serverUri, new BasicHttpAuthenticationHandler(username, password));
    }

    @Override
    public JiraRestClientPlus create(final URI serverUri, final HttpClient httpClient)
    {
        final DisposableHttpClient disposableHttpClient = new AsynchronousHttpClientFactory().createClient(httpClient);
        return new AsynchronousJiraRestClientPlus(serverUri, disposableHttpClient);
    }
}


For the rest, I use my existing code, but added a extra check that when worklogs.size >=20, it will get the worklogs by using the new IssueWorklogsRestClient.

Stephan Krull January 24, 2017

If you ever try to make a copy of this great workaround you might stumble over line 32 of WorklogsJsonParser: The reference to WORKLOGS_FIELD is missing.

Replace it with com.atlassian.jira.rest.client.api.domain.IssueFieldId.WORKLOGS_FIELD.id

 

0 votes
Dario B
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
May 26, 2016

This issue is being fixed in: https://jira.atlassian.com/browse/JRA-34746

GlobalTools July 8, 2016

Do we know if this is an issue on JIRA 7?

Vertex September 26, 2018

It's "being fixed" for several years now

Like Agostino Fiorani likes this
Dario B
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
September 26, 2018

@Vertex,

The original bug ticket has been splitted in 2, one for Server and one for Cloud. As you can see the one for Cloud is already fixed since the limit has been added on purpose as you can read in:

We are sorry there was change in a behaviour. However as it was made due to performance reasons we want to keep it as it is.

If anyone is still willing to get full list of worklog items I would recommend using GET /rest/api/2/issue/{issueIdOrKey}/worklog]

 

If you have a look at the REST API documentation you can check that indeed there are new worklog related endpoints (per issue, per time interval, etc):

 

As regarding Jira Server, I am not even sure that issue has ever been there. So maybe it is just there open by mistake.

Vertex September 26, 2018

I'm using Jira Server, the issue is there. 

Problem is, Atlassian tries to decide for the end-users what's best for them: We are sorry there was change in a behaviour. However as it was made due to performance reasons we want to keep it as it is.

If there are any performance reasons (and I'm pretty sure there are!) with querying issues containing tonnes of worklogs then by all means warn me BUT let me decide if it's acceptable in my particular scenario.

In some cases it would be totally ok if Jira took its time and returned all changed worklogs in one giant and neat response, albeit after a week of processing (ok, we all understand it will take several minutes tops).

And I'm forced to call several consequent endpoints and create a process of parsing additional data, looping etc, though a perfectly working architecture for providing all the data I need is already there

0 votes
Dejan Nedic September 2, 2013

We will go with the Tempo API instead since it provides the data we need.

0 votes
Timothy
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 27, 2013

https://docs.atlassian.com/jira/REST/latest/#idp1939456

returns a collection of worklogs associated with the issue, with count and pagination information

I don't know if this will work but base on maxResults, you might have to do recursive calls with the startAt parameter added.

Dejan Nedic August 27, 2013

Thanks. Recursive calls would be fine, but I am not sure that I see a way to do it using only the Java API. We have something like this :

Issue issue = restClient.getIssueClient().getIssue(key).claim();

Iterable<Worklog> worklogs = issue.getWorklogs();

And there is no way, or at least I do not see it, to set the startAt or max Results parameters.

Timothy
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 27, 2013

Sorry. I don't use the rest client. I make my own REST calls. I'm sure the REST client does not expose the other results. Tough luck.

Suggest an answer

Log in or Sign up to answer