View Javadoc

1   /*
2    * Copyright (C) 2012 Atlassian
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.atlassian.jira.rest.client.internal.async;
17  
18  import com.atlassian.httpclient.apache.httpcomponents.MultiPartEntityBuilder;
19  import com.atlassian.httpclient.api.HttpClient;
20  import com.atlassian.httpclient.api.Message;
21  import com.atlassian.httpclient.api.ResponsePromise;
22  import com.atlassian.jira.rest.client.api.GetCreateIssueMetadataOptions;
23  import com.atlassian.jira.rest.client.api.IssueRestClient;
24  import com.atlassian.jira.rest.client.api.MetadataRestClient;
25  import com.atlassian.jira.rest.client.api.RestClientException;
26  import com.atlassian.jira.rest.client.api.SessionRestClient;
27  import com.atlassian.jira.rest.client.api.domain.BasicIssue;
28  import com.atlassian.jira.rest.client.api.domain.BulkOperationResult;
29  import com.atlassian.jira.rest.client.api.domain.CimProject;
30  import com.atlassian.jira.rest.client.api.domain.Comment;
31  import com.atlassian.jira.rest.client.api.domain.Issue;
32  import com.atlassian.jira.rest.client.api.domain.ServerInfo;
33  import com.atlassian.jira.rest.client.api.domain.Transition;
34  import com.atlassian.jira.rest.client.api.domain.Votes;
35  import com.atlassian.jira.rest.client.api.domain.Watchers;
36  import com.atlassian.jira.rest.client.api.domain.input.AttachmentInput;
37  import com.atlassian.jira.rest.client.api.domain.input.FieldInput;
38  import com.atlassian.jira.rest.client.api.domain.input.IssueInput;
39  import com.atlassian.jira.rest.client.api.domain.input.LinkIssuesInput;
40  import com.atlassian.jira.rest.client.api.domain.input.TransitionInput;
41  import com.atlassian.jira.rest.client.api.domain.input.WorklogInput;
42  import com.atlassian.jira.rest.client.internal.ServerVersionConstants;
43  import com.atlassian.jira.rest.client.internal.json.BasicIssueJsonParser;
44  import com.atlassian.jira.rest.client.internal.json.BasicIssuesJsonParser;
45  import com.atlassian.jira.rest.client.internal.json.CreateIssueMetadataJsonParser;
46  import com.atlassian.jira.rest.client.internal.json.IssueJsonParser;
47  import com.atlassian.jira.rest.client.internal.json.JsonObjectParser;
48  import com.atlassian.jira.rest.client.internal.json.JsonParseUtil;
49  import com.atlassian.jira.rest.client.internal.json.TransitionJsonParser;
50  import com.atlassian.jira.rest.client.internal.json.TransitionJsonParserV5;
51  import com.atlassian.jira.rest.client.internal.json.VotesJsonParser;
52  import com.atlassian.jira.rest.client.internal.json.WatchersJsonParserBuilder;
53  import com.atlassian.jira.rest.client.internal.json.gen.CommentJsonGenerator;
54  import com.atlassian.jira.rest.client.internal.json.gen.IssueInputJsonGenerator;
55  import com.atlassian.jira.rest.client.internal.json.gen.IssueUpdateJsonGenerator;
56  import com.atlassian.jira.rest.client.internal.json.gen.IssuesInputJsonGenerator;
57  import com.atlassian.jira.rest.client.internal.json.gen.LinkIssuesInputGenerator;
58  import com.atlassian.jira.rest.client.internal.json.gen.WorklogInputJsonGenerator;
59  import com.atlassian.util.concurrent.Promise;
60  import com.google.common.base.Joiner;
61  import com.google.common.base.Strings;
62  import com.google.common.collect.Iterables;
63  import org.apache.http.entity.mime.MultipartEntityBuilder;
64  import org.codehaus.jettison.json.JSONArray;
65  import org.codehaus.jettison.json.JSONException;
66  import org.codehaus.jettison.json.JSONObject;
67  
68  import javax.annotation.Nullable;
69  import javax.ws.rs.core.UriBuilder;
70  import java.io.File;
71  import java.io.InputStream;
72  import java.net.URI;
73  import java.util.ArrayList;
74  import java.util.Collection;
75  import java.util.EnumSet;
76  import java.util.Iterator;
77  import java.util.function.Function;
78  import java.util.stream.Collectors;
79  import java.util.stream.StreamSupport;
80  
81  import static java.nio.charset.StandardCharsets.UTF_8;
82  import static java.util.Collections.emptyList;
83  import static org.apache.http.entity.ContentType.DEFAULT_BINARY;
84  
85  /**
86   * Asynchronous implementation of IssueRestClient.
87   *
88   * @since v2.0
89   */
90  public class AsynchronousIssueRestClient extends AbstractAsynchronousRestClient implements IssueRestClient {
91  
92      private static final EnumSet<Expandos> DEFAULT_EXPANDS = EnumSet.of(Expandos.NAMES, Expandos.SCHEMA, Expandos.TRANSITIONS);
93      private static final Function<IssueRestClient.Expandos, String> EXPANDO_TO_PARAM = from -> from.name().toLowerCase();
94      private final SessionRestClient sessionRestClient;
95      private final MetadataRestClient metadataRestClient;
96  
97      private final IssueJsonParser issueParser = new IssueJsonParser();
98      private final BasicIssueJsonParser basicIssueParser = new BasicIssueJsonParser();
99      private final JsonObjectParser<Watchers> watchersParser = WatchersJsonParserBuilder.createWatchersParser();
100     private final TransitionJsonParser transitionJsonParser = new TransitionJsonParser();
101     private final JsonObjectParser<Transition> transitionJsonParserV5 = new TransitionJsonParserV5();
102     private final VotesJsonParser votesJsonParser = new VotesJsonParser();
103     private final CreateIssueMetadataJsonParser createIssueMetadataJsonParser = new CreateIssueMetadataJsonParser();
104     private static final String FILE_BODY_TYPE = "file";
105     private final URI baseUri;
106     private ServerInfo serverInfo;
107 
108     public AsynchronousIssueRestClient(final URI baseUri, final HttpClient client, final SessionRestClient sessionRestClient,
109                                        final MetadataRestClient metadataRestClient) {
110         super(client);
111         this.baseUri = baseUri;
112         this.sessionRestClient = sessionRestClient;
113         this.metadataRestClient = metadataRestClient;
114     }
115 
116     private synchronized ServerInfo getVersionInfo() {
117         if (serverInfo == null) {
118             serverInfo = metadataRestClient.getServerInfo().claim();
119         }
120         return serverInfo;
121     }
122 
123     @Override
124     public Promise<BasicIssue> createIssue(final IssueInput issue) {
125         final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path("issue");
126         return postAndParse(uriBuilder.build(), issue, new IssueInputJsonGenerator(), basicIssueParser);
127     }
128 
129     @Override
130     public Promise<Void> updateIssue(final String issueKey, final IssueInput issue) {
131         final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path("issue").path(issueKey);
132         return put(uriBuilder.build(), issue, new IssueInputJsonGenerator());
133     }
134 
135     @Override
136     public Promise<BulkOperationResult<BasicIssue>> createIssues(Collection<IssueInput> issues) {
137         final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path("issue/bulk");
138 
139         return postAndParse(uriBuilder.build(), issues, new IssuesInputJsonGenerator(), new BasicIssuesJsonParser());
140     }
141 
142     @Override
143     public Promise<Iterable<CimProject>> getCreateIssueMetadata(@Nullable GetCreateIssueMetadataOptions options) {
144         final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path("issue/createmeta");
145 
146         if (options != null) {
147             if (options.projectIds != null) {
148                 uriBuilder.queryParam("projectIds", Joiner.on(",").join(options.projectIds));
149             }
150 
151             if (options.projectKeys != null) {
152                 uriBuilder.queryParam("projectKeys", Joiner.on(",").join(options.projectKeys));
153             }
154 
155             if (options.issueTypeIds != null) {
156                 uriBuilder.queryParam("issuetypeIds", Joiner.on(",").join(options.issueTypeIds));
157             }
158 
159             final Iterable<String> issueTypeNames = options.issueTypeNames;
160             if (issueTypeNames != null) {
161                 for (final String name : issueTypeNames) {
162                     uriBuilder.queryParam("issuetypeNames", name);
163                 }
164             }
165 
166             final Iterable<String> expandos = options.expandos;
167             if (expandos != null && expandos.iterator().hasNext()) {
168                 uriBuilder.queryParam("expand", Joiner.on(",").join(expandos));
169             }
170         }
171 
172         return getAndParse(uriBuilder.build(), createIssueMetadataJsonParser);
173     }
174 
175     @Override
176     public Promise<Issue> getIssue(final String issueKey) {
177         return getIssue(issueKey, emptyList());
178     }
179 
180     @Override
181     public Promise<Issue> getIssue(final String issueKey, final Iterable<Expandos> expand) {
182         final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri);
183         final Iterable<Expandos> expands = Iterables.concat(DEFAULT_EXPANDS, expand);
184         uriBuilder.path("issue").path(issueKey).queryParam("expand",
185                 StreamSupport.stream(expands.spliterator(), false).map(EXPANDO_TO_PARAM).collect(Collectors.joining(",")));
186         return getAndParse(uriBuilder.build(), issueParser);
187     }
188 
189     @Override
190     public Promise<Void> deleteIssue(String issueKey, boolean deleteSubtasks) {
191         return delete(UriBuilder.fromUri(baseUri).path("issue").path(issueKey)
192                 .queryParam("deleteSubtasks", deleteSubtasks).build());
193     }
194 
195     @Override
196     public Promise<Watchers> getWatchers(final URI watchersUri) {
197         return getAndParse(watchersUri, watchersParser);
198     }
199 
200     @Override
201     public Promise<Votes> getVotes(final URI votesUri) {
202         return getAndParse(votesUri, votesJsonParser);
203     }
204 
205     @Override
206     public Promise<Iterable<Transition>> getTransitions(final URI transitionsUri) {
207         return callAndParse(client().newRequest(transitionsUri).get(),
208                 (ResponseHandler<Iterable<Transition>>) response -> {
209                     final JSONObject jsonObject = new JSONObject(response.getEntity());
210                     if (jsonObject.has("transitions")) {
211                         return JsonParseUtil.parseJsonArray(jsonObject.getJSONArray("transitions"), transitionJsonParserV5);
212                     } else {
213                         final Collection<Transition> transitions = new ArrayList<>(jsonObject.length());
214                         @SuppressWarnings("unchecked") final Iterator<String> iterator = jsonObject.keys();
215                         while (iterator.hasNext()) {
216                             final String key = iterator.next();
217                             try {
218                                 final int id = Integer.parseInt(key);
219                                 final Transition transition = transitionJsonParser.parse(jsonObject.getJSONObject(key), id);
220                                 transitions.add(transition);
221                             } catch (JSONException e) {
222                                 throw new RestClientException(e);
223                             } catch (NumberFormatException e) {
224                                 throw new RestClientException(
225                                         "Transition id should be an integer, but found [" + key + "]", e);
226                             }
227                         }
228                         return transitions;
229                     }
230                 }
231         );
232     }
233 
234     @Override
235     public Promise<Iterable<Transition>> getTransitions(final Issue issue) {
236         if (issue.getTransitionsUri() != null) {
237             return getTransitions(issue.getTransitionsUri());
238         } else {
239             final UriBuilder transitionsUri = UriBuilder.fromUri(issue.getSelf());
240             return getTransitions(transitionsUri.path("transitions").queryParam("expand", "transitions.fields").build());
241         }
242     }
243 
244     @Override
245     public Promise<Void> transition(final URI transitionsUri, final TransitionInput transitionInput) {
246         final int buildNumber = getVersionInfo().getBuildNumber();
247         try {
248             JSONObject jsonObject = new JSONObject();
249             if (buildNumber >= ServerVersionConstants.BN_JIRA_5) {
250                 jsonObject.put("transition", new JSONObject().put("id", transitionInput.getId()));
251             } else {
252                 jsonObject.put("transition", transitionInput.getId());
253             }
254             if (transitionInput.getComment() != null) {
255                 if (buildNumber >= ServerVersionConstants.BN_JIRA_5) {
256                     jsonObject.put("update", new JSONObject().put("comment",
257                             new JSONArray().put(new JSONObject().put("add",
258                                     new CommentJsonGenerator(getVersionInfo())
259                                             .generate(transitionInput.getComment())))));
260                 } else {
261                     jsonObject.put("comment", new CommentJsonGenerator(getVersionInfo())
262                             .generate(transitionInput.getComment()));
263                 }
264             }
265             final Iterable<FieldInput> fields = transitionInput.getFields();
266             final JSONObject fieldsJs = new IssueUpdateJsonGenerator().generate(fields);
267             if (fieldsJs.keys().hasNext()) {
268                 jsonObject.put("fields", fieldsJs);
269             }
270             if (fieldsJs.keys().hasNext()) {
271                 jsonObject.put("fields", fieldsJs);
272             }
273             return post(transitionsUri, jsonObject);
274         } catch (JSONException ex) {
275             throw new RestClientException(ex);
276         }
277     }
278 
279     @Override
280     public Promise<Void> transition(final Issue issue, final TransitionInput transitionInput) {
281         if (issue.getTransitionsUri() != null) {
282             return transition(issue.getTransitionsUri(), transitionInput);
283         } else {
284             final UriBuilder uriBuilder = UriBuilder.fromUri(issue.getSelf());
285             uriBuilder.path("transitions");
286             return transition(uriBuilder.build(), transitionInput);
287         }
288     }
289 
290     @Override
291     public Promise<Void> vote(final URI votesUri) {
292         return post(votesUri);
293     }
294 
295     @Override
296     public Promise<Void> unvote(final URI votesUri) {
297         return delete(votesUri);
298     }
299 
300     @Override
301     public Promise<Void> watch(final URI watchersUri) {
302         return post(watchersUri);
303     }
304 
305     @Override
306     public Promise<Void> unwatch(final URI watchersUri) {
307         return removeWatcher(watchersUri, getLoggedUsername());
308     }
309 
310     @Override
311     public Promise<Void> addWatcher(final URI watchersUri, final String username) {
312         return post(watchersUri, JSONObject.quote(username));
313     }
314 
315     @Override
316     public Promise<Void> removeWatcher(final URI watchersUri, final String username) {
317         final UriBuilder uriBuilder = UriBuilder.fromUri(watchersUri);
318         if (getVersionInfo().getBuildNumber() >= ServerVersionConstants.BN_JIRA_4_4) {
319             uriBuilder.queryParam("username", username);
320         } else {
321             uriBuilder.path(username).build();
322         }
323         return delete(uriBuilder.build());
324     }
325 
326     @Override
327     public Promise<Void> linkIssue(final LinkIssuesInput linkIssuesInput) {
328         final URI uri = UriBuilder.fromUri(baseUri).path("issueLink").build();
329         return post(uri, linkIssuesInput, new LinkIssuesInputGenerator(getVersionInfo()));
330     }
331 
332     @Override
333     public Promise<Void> addAttachment(final URI attachmentsUri, final InputStream inputStream, final String filename) {
334         final MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create()
335                 .setLaxMode()
336                 .setCharset(UTF_8)
337                 .addBinaryBody(
338                         FILE_BODY_TYPE,
339                         inputStream,
340                         DEFAULT_BINARY,
341                         filename);
342         return postAttachments(attachmentsUri, entityBuilder);
343     }
344 
345     @Override
346     public Promise<Void> addAttachments(final URI attachmentsUri, final AttachmentInput... attachments) {
347         final MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create()
348                 .setLaxMode()
349                 .setCharset(UTF_8);
350         for (final AttachmentInput attachmentInput : attachments) {
351             entityBuilder.addBinaryBody(
352                     FILE_BODY_TYPE,
353                     attachmentInput.getInputStream(),
354                     DEFAULT_BINARY,
355                     attachmentInput.getFilename());
356         }
357         return postAttachments(attachmentsUri, entityBuilder);
358     }
359 
360     @Override
361     public Promise<Void> addAttachments(final URI attachmentsUri, final File... files) {
362         final MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create()
363                 .setLaxMode()
364                 .setCharset(UTF_8);
365         for (final File file : files) {
366             entityBuilder.addBinaryBody(FILE_BODY_TYPE, file);
367         }
368         return postAttachments(attachmentsUri, entityBuilder);
369     }
370 
371     @Override
372     public Promise<Void> addComment(final URI commentsUri, final Comment comment) {
373         return post(commentsUri, comment, new CommentJsonGenerator(getVersionInfo()));
374     }
375 
376     @Override
377     public Promise<InputStream> getAttachment(URI attachmentUri) {
378         return callAndParse(client().newRequest(attachmentUri).get(), Message::getEntityStream);
379     }
380 
381     @Override
382     public Promise<Void> addWorklog(URI worklogUri, WorklogInput worklogInput) {
383         final UriBuilder uriBuilder = UriBuilder.fromUri(worklogUri)
384                 .queryParam("adjustEstimate", worklogInput.getAdjustEstimate().restValue);
385 
386         switch (worklogInput.getAdjustEstimate()) {
387             case NEW:
388                 uriBuilder.queryParam("newEstimate", Strings.nullToEmpty(worklogInput.getAdjustEstimateValue()));
389                 break;
390             case MANUAL:
391                 uriBuilder.queryParam("reduceBy", Strings.nullToEmpty(worklogInput.getAdjustEstimateValue()));
392                 break;
393         }
394 
395         return post(uriBuilder.build(), worklogInput, new WorklogInputJsonGenerator());
396     }
397 
398     private Promise<Void> postAttachments(final URI attachmentsUri, final MultipartEntityBuilder multipartEntityBuilder) {
399         final ResponsePromise responsePromise = client()
400                 .newRequest(attachmentsUri)
401                 .setEntity(new MultiPartEntityBuilder(multipartEntityBuilder.build()))
402                 .setHeader("X-Atlassian-Token", "nocheck")
403                 .post();
404         return call(responsePromise);
405     }
406 
407     private String getLoggedUsername() {
408         return sessionRestClient.getCurrentSession().claim().getUsername();
409     }
410 }