REST API is returning only 20 worklogs per issue

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

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.

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 vote
Timothy Chin Community Champion Aug 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.

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 Chin Community Champion Aug 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.

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

0 vote

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

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

Suggest an answer

Log in or Sign up to answer
How to earn badges on the Atlassian Community

How to earn badges on the Atlassian Community

Badges are a great way to show off community activity, whether you’re a newbie or a Champion.

Learn more
Community showcase
Published Monday in Jira Software

How large do you think Jira Software can grow?

Hi Atlassian Community! My name is Shana, and I’m on the Jira Software team. One of the many reasons this Community exists is to connect you to others on similar product journeys or with comparabl...

468 views 6 11
Read article

Atlassian User Groups

Connect with like-minded Atlassian users at free events near you!

Find a group

Connect with like-minded Atlassian users at free events near you!

Find my local user group

Unfortunately there are no AUG chapters near you at the moment.

Start an AUG

You're one step closer to meeting fellow Atlassian users at your local meet up. Learn more about AUGs

Groups near you