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.Response;
21 import com.atlassian.httpclient.api.ResponsePromise;
22 import com.atlassian.jira.rest.client.api.*;
23 import com.atlassian.jira.rest.client.api.domain.*;
24 import com.atlassian.jira.rest.client.api.domain.input.*;
25 import com.atlassian.jira.rest.client.internal.ServerVersionConstants;
26 import com.atlassian.jira.rest.client.internal.json.*;
27 import com.atlassian.jira.rest.client.internal.json.gen.*;
28 import com.atlassian.util.concurrent.Promise;
29 import com.google.common.base.Function;
30 import com.google.common.base.Joiner;
31 import com.google.common.base.Strings;
32 import com.google.common.collect.Iterables;
33 import org.apache.http.entity.mime.HttpMultipartMode;
34 import org.apache.http.entity.mime.MultipartEntity;
35 import org.apache.http.entity.mime.content.FileBody;
36 import org.apache.http.entity.mime.content.InputStreamBody;
37 import org.codehaus.jettison.json.JSONArray;
38 import org.codehaus.jettison.json.JSONException;
39 import org.codehaus.jettison.json.JSONObject;
40
41 import javax.annotation.Nullable;
42 import javax.ws.rs.core.UriBuilder;
43 import java.io.File;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.net.URI;
47 import java.nio.charset.Charset;
48 import java.util.*;
49
50
51
52
53
54
55 public class AsynchronousIssueRestClient extends AbstractAsynchronousRestClient implements IssueRestClient {
56
57 private static final EnumSet<Expandos> DEFAULT_EXPANDS = EnumSet.of(Expandos.NAMES, Expandos.SCHEMA, Expandos.TRANSITIONS);
58 private static final Function<IssueRestClient.Expandos, String> EXPANDO_TO_PARAM = new Function<Expandos, String>() {
59 @Override
60 public String apply(Expandos from) {
61 return from.name().toLowerCase();
62 }
63 };
64 private final SessionRestClient sessionRestClient;
65 private final MetadataRestClient metadataRestClient;
66
67 private final IssueJsonParser issueParser = new IssueJsonParser();
68 private final BasicIssueJsonParser basicIssueParser = new BasicIssueJsonParser();
69 private final JsonObjectParser<Watchers> watchersParser = WatchersJsonParserBuilder.createWatchersParser();
70 private final TransitionJsonParser transitionJsonParser = new TransitionJsonParser();
71 private final JsonObjectParser<Transition> transitionJsonParserV5 = new TransitionJsonParserV5();
72 private final VotesJsonParser votesJsonParser = new VotesJsonParser();
73 private final CreateIssueMetadataJsonParser createIssueMetadataJsonParser = new CreateIssueMetadataJsonParser();
74 private static final String FILE_BODY_TYPE = "file";
75 private final URI baseUri;
76 private ServerInfo serverInfo;
77
78 public AsynchronousIssueRestClient(final URI baseUri, final HttpClient client, final SessionRestClient sessionRestClient,
79 final MetadataRestClient metadataRestClient) {
80 super(client);
81 this.baseUri = baseUri;
82 this.sessionRestClient = sessionRestClient;
83 this.metadataRestClient = metadataRestClient;
84 }
85
86 private synchronized ServerInfo getVersionInfo() {
87 if (serverInfo == null) {
88 serverInfo = metadataRestClient.getServerInfo().claim();
89 }
90 return serverInfo;
91 }
92
93 @Override
94 public Promise<BasicIssue> createIssue(final IssueInput issue) {
95 final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path("issue");
96 return postAndParse(uriBuilder.build(), issue, new IssueInputJsonGenerator(), basicIssueParser);
97 }
98
99 @Override
100 public Promise<BulkOperationResult<BasicIssue>> createIssues(Collection<IssueInput> issues) {
101 final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path("issue/bulk");
102
103 return postAndParse(uriBuilder.build(), issues, new IssuesInputJsonGenerator(), new BasicIssuesJsonParser());
104 }
105
106 @Override
107 public Promise<Iterable<CimProject>> getCreateIssueMetadata(@Nullable GetCreateIssueMetadataOptions options) {
108 final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path("issue/createmeta");
109
110 if (options != null) {
111 if (options.projectIds != null) {
112 uriBuilder.queryParam("projectIds", Joiner.on(",").join(options.projectIds));
113 }
114
115 if (options.projectKeys != null) {
116 uriBuilder.queryParam("projectKeys", Joiner.on(",").join(options.projectKeys));
117 }
118
119 if (options.issueTypeIds != null) {
120 uriBuilder.queryParam("issuetypeIds", Joiner.on(",").join(options.issueTypeIds));
121 }
122
123 final Iterable<String> issueTypeNames = options.issueTypeNames;
124 if (issueTypeNames != null) {
125 for (final String name : issueTypeNames) {
126 uriBuilder.queryParam("issuetypeNames", name);
127 }
128 }
129
130 final Iterable<String> expandos = options.expandos;
131 if (expandos != null && expandos.iterator().hasNext()) {
132 uriBuilder.queryParam("expand", Joiner.on(",").join(expandos));
133 }
134 }
135
136 return getAndParse(uriBuilder.build(), createIssueMetadataJsonParser);
137 }
138
139 @Override
140 public Promise<Issue> getIssue(final String issueKey) {
141 return getIssue(issueKey, Collections.<Expandos>emptyList());
142 }
143
144 @Override
145 public Promise<Issue> getIssue(final String issueKey, final Iterable<Expandos> expand) {
146 final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri);
147 final Iterable<Expandos> expands = Iterables.concat(DEFAULT_EXPANDS, expand);
148 uriBuilder.path("issue").path(issueKey).queryParam("expand",
149 Joiner.on(',').join(Iterables.transform(expands, EXPANDO_TO_PARAM)));
150 return getAndParse(uriBuilder.build(), issueParser);
151 }
152
153 @Override
154 public Promise<Watchers> getWatchers(final URI watchersUri) {
155 return getAndParse(watchersUri, watchersParser);
156 }
157
158 @Override
159 public Promise<Votes> getVotes(final URI votesUri) {
160 return getAndParse(votesUri, votesJsonParser);
161 }
162
163 @Override
164 public Promise<Iterable<Transition>> getTransitions(final URI transitionsUri) {
165 return callAndParse(client().newRequest(transitionsUri).get(),
166 new AbstractAsynchronousRestClient.ResponseHandler<Iterable<Transition>>() {
167 @Override
168 public Iterable<Transition> handle(Response response) throws JSONException, IOException {
169 final JSONObject jsonObject = new JSONObject(response.getEntity());
170 if (jsonObject.has("transitions")) {
171 return JsonParseUtil.parseJsonArray(jsonObject.getJSONArray("transitions"), transitionJsonParserV5);
172 } else {
173 final Collection<Transition> transitions = new ArrayList<Transition>(jsonObject.length());
174 @SuppressWarnings("unchecked")
175 final Iterator<String> iterator = jsonObject.keys();
176 while (iterator.hasNext()) {
177 final String key = iterator.next();
178 try {
179 final int id = Integer.parseInt(key);
180 final Transition transition = transitionJsonParser.parse(jsonObject.getJSONObject(key), id);
181 transitions.add(transition);
182 } catch (JSONException e) {
183 throw new RestClientException(e);
184 } catch (NumberFormatException e) {
185 throw new RestClientException(
186 "Transition id should be an integer, but found [" + key + "]", e);
187 }
188 }
189 return transitions;
190 }
191 }
192 }
193 );
194 }
195
196 @Override
197 public Promise<Iterable<Transition>> getTransitions(final Issue issue) {
198 if (issue.getTransitionsUri() != null) {
199 return getTransitions(issue.getTransitionsUri());
200 } else {
201 final UriBuilder transitionsUri = UriBuilder.fromUri(issue.getSelf());
202 return getTransitions(transitionsUri.path("transitions").queryParam("expand", "transitions.fields").build());
203 }
204 }
205
206 @Override
207 public Promise<Void> transition(final URI transitionsUri, final TransitionInput transitionInput) {
208 final int buildNumber = getVersionInfo().getBuildNumber();
209 try {
210 JSONObject jsonObject = new JSONObject();
211 if (buildNumber >= ServerVersionConstants.BN_JIRA_5) {
212 jsonObject.put("transition", new JSONObject().put("id", transitionInput.getId()));
213 } else {
214 jsonObject.put("transition", transitionInput.getId());
215 }
216 if (transitionInput.getComment() != null) {
217 if (buildNumber >= ServerVersionConstants.BN_JIRA_5) {
218 jsonObject.put("update", new JSONObject().put("comment",
219 new JSONArray().put(new JSONObject().put("add",
220 new CommentJsonGenerator(getVersionInfo())
221 .generate(transitionInput.getComment())))));
222 } else {
223 jsonObject.put("comment", new CommentJsonGenerator(getVersionInfo())
224 .generate(transitionInput.getComment()));
225 }
226 }
227 final Iterable<FieldInput> fields = transitionInput.getFields();
228 final JSONObject fieldsJs = new IssueUpdateJsonGenerator().generate(fields);
229 if (fieldsJs.keys().hasNext()) {
230 jsonObject.put("fields", fieldsJs);
231 }
232 if (fieldsJs.keys().hasNext()) {
233 jsonObject.put("fields", fieldsJs);
234 }
235 return post(transitionsUri, jsonObject);
236 } catch (JSONException ex) {
237 throw new RestClientException(ex);
238 }
239 }
240
241 @Override
242 public Promise<Void> transition(final Issue issue, final TransitionInput transitionInput) {
243 if (issue.getTransitionsUri() != null) {
244 return transition(issue.getTransitionsUri(), transitionInput);
245 } else {
246 final UriBuilder uriBuilder = UriBuilder.fromUri(issue.getSelf());
247 uriBuilder.path("transitions");
248 return transition(uriBuilder.build(), transitionInput);
249 }
250 }
251
252 @Override
253 public Promise<Void> vote(final URI votesUri) {
254 return post(votesUri);
255 }
256
257 @Override
258 public Promise<Void> unvote(final URI votesUri) {
259 return delete(votesUri);
260 }
261
262 @Override
263 public Promise<Void> watch(final URI watchersUri) {
264 return post(watchersUri);
265 }
266
267 @Override
268 public Promise<Void> unwatch(final URI watchersUri) {
269 return removeWatcher(watchersUri, getLoggedUsername());
270 }
271
272 @Override
273 public Promise<Void> addWatcher(final URI watchersUri, final String username) {
274 return post(watchersUri, JSONObject.quote(username));
275 }
276
277 @Override
278 public Promise<Void> removeWatcher(final URI watchersUri, final String username) {
279 final UriBuilder uriBuilder = UriBuilder.fromUri(watchersUri);
280 if (getVersionInfo().getBuildNumber() >= ServerVersionConstants.BN_JIRA_4_4) {
281 uriBuilder.queryParam("username", username);
282 } else {
283 uriBuilder.path(username).build();
284 }
285 return delete(uriBuilder.build());
286 }
287
288 @Override
289 public Promise<Void> linkIssue(final LinkIssuesInput linkIssuesInput) {
290 final URI uri = UriBuilder.fromUri(baseUri).path("issueLink").build();
291 return post(uri, linkIssuesInput, new LinkIssuesInputGenerator(getVersionInfo()));
292 }
293
294 @Override
295 public Promise<Void> addAttachment(final URI attachmentsUri, final InputStream inputStream, final String filename) {
296 final MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, null, Charset.defaultCharset());
297 entity.addPart(FILE_BODY_TYPE, new InputStreamBody(inputStream, filename));
298 return postAttachments(attachmentsUri, entity);
299 }
300
301 @Override
302 public Promise<Void> addAttachments(final URI attachmentsUri, final AttachmentInput... attachments) {
303 final MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, null, Charset.defaultCharset());
304 for (final AttachmentInput attachmentInput : attachments) {
305 entity.addPart(FILE_BODY_TYPE, new InputStreamBody(attachmentInput.getInputStream(), attachmentInput.getFilename()));
306 }
307 return postAttachments(attachmentsUri, entity);
308 }
309
310 @Override
311 public Promise<Void> addAttachments(final URI attachmentsUri, final File... files) {
312 final MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, null, Charset.defaultCharset());
313 for (final File file : files) {
314 entity.addPart(FILE_BODY_TYPE, new FileBody(file));
315 }
316 return postAttachments(attachmentsUri, entity);
317 }
318
319 @Override
320 public Promise<Void> addComment(final URI commentsUri, final Comment comment) {
321 return post(commentsUri, comment, new CommentJsonGenerator(getVersionInfo()));
322 }
323
324 @Override
325 public Promise<InputStream> getAttachment(URI attachmentUri) {
326 return callAndParse(client().newRequest(attachmentUri).get(),
327 new ResponseHandler<InputStream>() {
328 @Override
329 public InputStream handle(final Response request) throws JSONException, IOException {
330 return request.getEntityStream();
331 }
332 }
333 );
334 }
335
336 @Override
337 public Promise<Void> addWorklog(URI worklogUri, WorklogInput worklogInput) {
338 final UriBuilder uriBuilder = UriBuilder.fromUri(worklogUri)
339 .queryParam("adjustEstimate", worklogInput.getAdjustEstimate().restValue);
340
341 switch (worklogInput.getAdjustEstimate()) {
342 case NEW:
343 uriBuilder.queryParam("newEstimate", Strings.nullToEmpty(worklogInput.getAdjustEstimateValue()));
344 break;
345 case MANUAL:
346 uriBuilder.queryParam("reduceBy", Strings.nullToEmpty(worklogInput.getAdjustEstimateValue()));
347 break;
348 }
349
350 return post(uriBuilder.build(), worklogInput, new WorklogInputJsonGenerator());
351 }
352
353 private Promise<Void> postAttachments(final URI attachmentsUri, final MultipartEntity multipartEntity) {
354 final ResponsePromise responsePromise = client()
355 .newRequest(attachmentsUri)
356 .setEntity(new MultiPartEntityBuilder(multipartEntity))
357 .setHeader("X-Atlassian-Token", "nocheck")
358 .post();
359 return call(responsePromise);
360 }
361
362 private String getLoggedUsername() {
363 return sessionRestClient.getCurrentSession().claim().getUsername();
364 }
365 }