1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
87
88
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 }