1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.atlassian.jira.rest.client.internal.jersey;
18
19 import com.atlassian.jira.rest.client.GetCreateIssueMetadataOptions;
20 import com.atlassian.jira.rest.client.IssueRestClient;
21 import com.atlassian.jira.rest.client.MetadataRestClient;
22 import com.atlassian.jira.rest.client.ProgressMonitor;
23 import com.atlassian.jira.rest.client.RestClientException;
24 import com.atlassian.jira.rest.client.SessionRestClient;
25 import com.atlassian.jira.rest.client.domain.BasicIssue;
26 import com.atlassian.jira.rest.client.domain.CimProject;
27 import com.atlassian.jira.rest.client.domain.Comment;
28 import com.atlassian.jira.rest.client.domain.Issue;
29 import com.atlassian.jira.rest.client.domain.ServerInfo;
30 import com.atlassian.jira.rest.client.domain.Session;
31 import com.atlassian.jira.rest.client.domain.Transition;
32 import com.atlassian.jira.rest.client.domain.Votes;
33 import com.atlassian.jira.rest.client.domain.Watchers;
34 import com.atlassian.jira.rest.client.domain.input.AttachmentInput;
35 import com.atlassian.jira.rest.client.domain.input.FieldInput;
36 import com.atlassian.jira.rest.client.domain.input.IssueInput;
37 import com.atlassian.jira.rest.client.domain.input.LinkIssuesInput;
38 import com.atlassian.jira.rest.client.domain.input.TransitionInput;
39 import com.atlassian.jira.rest.client.domain.input.WorklogInput;
40 import com.atlassian.jira.rest.client.internal.ServerVersionConstants;
41 import com.atlassian.jira.rest.client.internal.json.BasicIssueJsonParser;
42 import com.atlassian.jira.rest.client.internal.json.CreateIssueMetadataJsonParser;
43 import com.atlassian.jira.rest.client.internal.json.IssueJsonParser;
44 import com.atlassian.jira.rest.client.internal.json.JsonObjectParser;
45 import com.atlassian.jira.rest.client.internal.json.JsonParseUtil;
46 import com.atlassian.jira.rest.client.internal.json.TransitionJsonParser;
47 import com.atlassian.jira.rest.client.internal.json.TransitionJsonParserV5;
48 import com.atlassian.jira.rest.client.internal.json.VotesJsonParser;
49 import com.atlassian.jira.rest.client.internal.json.WatchersJsonParserBuilder;
50 import com.atlassian.jira.rest.client.internal.json.gen.CommentJsonGenerator;
51 import com.atlassian.jira.rest.client.internal.json.gen.ComplexIssueInputFieldValueJsonGenerator;
52 import com.atlassian.jira.rest.client.internal.json.gen.IssueInputJsonGenerator;
53 import com.atlassian.jira.rest.client.internal.json.gen.LinkIssuesInputGenerator;
54 import com.atlassian.jira.rest.client.internal.json.gen.WorklogInputJsonGenerator;
55 import com.google.common.base.Function;
56 import com.google.common.base.Joiner;
57 import com.google.common.base.Strings;
58 import com.google.common.collect.Iterables;
59 import com.google.common.collect.Lists;
60 import com.sun.jersey.api.client.WebResource;
61 import com.sun.jersey.client.apache.ApacheHttpClient;
62 import com.sun.jersey.core.header.FormDataContentDisposition;
63 import com.sun.jersey.multipart.BodyPart;
64 import com.sun.jersey.multipart.MultiPart;
65 import com.sun.jersey.multipart.MultiPartMediaTypes;
66 import com.sun.jersey.multipart.file.FileDataBodyPart;
67 import org.codehaus.jettison.json.JSONArray;
68 import org.codehaus.jettison.json.JSONException;
69 import org.codehaus.jettison.json.JSONObject;
70
71 import javax.annotation.Nullable;
72 import javax.ws.rs.core.MediaType;
73 import javax.ws.rs.core.UriBuilder;
74 import java.io.File;
75 import java.io.InputStream;
76 import java.net.URI;
77 import java.util.ArrayList;
78 import java.util.Collection;
79 import java.util.Collections;
80 import java.util.EnumSet;
81 import java.util.Iterator;
82 import java.util.concurrent.Callable;
83
84
85
86
87
88
89 public class JerseyIssueRestClient extends AbstractJerseyRestClient implements IssueRestClient {
90
91 private static final String FILE_ATTACHMENT_CONTROL_NAME = "file";
92 private static final EnumSet<Expandos> DEFAULT_EXPANDS = EnumSet.of(Expandos.NAMES, Expandos.SCHEMA, Expandos.TRANSITIONS);
93 private static final Function<Expandos, String> EXPANDO_TO_PARAM = new Function<Expandos, String>() {
94 @Override
95 public String apply(Expandos from) {
96 return from.name().toLowerCase();
97 }
98 };
99 private final SessionRestClient sessionRestClient;
100 private final MetadataRestClient metadataRestClient;
101
102 private final IssueJsonParser issueParser = new IssueJsonParser();
103 private final BasicIssueJsonParser basicIssueParser = new BasicIssueJsonParser();
104 private final JsonObjectParser<Watchers> watchersParser = WatchersJsonParserBuilder.createWatchersParser();
105 private final TransitionJsonParser transitionJsonParser = new TransitionJsonParser();
106 private final JsonObjectParser<Transition> transitionJsonParserV5 = new TransitionJsonParserV5();
107 private final VotesJsonParser votesJsonParser = new VotesJsonParser();
108 private final CreateIssueMetadataJsonParser createIssueMetadataJsonParser = new CreateIssueMetadataJsonParser();
109 private ServerInfo serverInfo;
110
111 public JerseyIssueRestClient(URI baseUri, ApacheHttpClient client, SessionRestClient sessionRestClient, MetadataRestClient metadataRestClient) {
112 super(baseUri, client);
113 this.sessionRestClient = sessionRestClient;
114 this.metadataRestClient = metadataRestClient;
115 }
116
117 private synchronized ServerInfo getVersionInfo(ProgressMonitor progressMonitor) {
118 if (serverInfo == null) {
119 serverInfo = metadataRestClient.getServerInfo(progressMonitor);
120 }
121 return serverInfo;
122 }
123
124 @Override
125 public Watchers getWatchers(URI watchersUri, ProgressMonitor progressMonitor) {
126 return getAndParse(watchersUri, watchersParser, progressMonitor);
127 }
128
129
130 @Override
131 public Votes getVotes(URI votesUri, ProgressMonitor progressMonitor) {
132 return getAndParse(votesUri, votesJsonParser, progressMonitor);
133 }
134
135 @Override
136 public Issue getIssue(final String issueKey, ProgressMonitor progressMonitor) {
137 return getIssue(issueKey, Collections.<Expandos>emptyList(), progressMonitor);
138 }
139
140 @Override
141 public Issue getIssue(final String issueKey, Iterable<Expandos> expand, ProgressMonitor progressMonitor) {
142 final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri);
143 final Iterable<Expandos> expands = Iterables.concat(DEFAULT_EXPANDS, expand);
144 uriBuilder.path("issue").path(issueKey).queryParam("expand",
145 Joiner.on(',').join(Iterables.transform(expands, EXPANDO_TO_PARAM)));
146 return getAndParse(uriBuilder.build(), issueParser, progressMonitor);
147 }
148
149 @Override
150 public Iterable<Transition> getTransitions(final URI transitionsUri, ProgressMonitor progressMonitor) {
151 return invoke(new Callable<Iterable<Transition>>() {
152 @Override
153 public Iterable<Transition> call() throws Exception {
154 final WebResource transitionsResource = client.resource(transitionsUri);
155 final JSONObject jsonObject = transitionsResource.get(JSONObject.class);
156 if (jsonObject.has("transitions")) {
157 return JsonParseUtil.parseJsonArray(jsonObject.getJSONArray("transitions"), transitionJsonParserV5);
158 } else {
159 final Collection<Transition> transitions = new ArrayList<Transition>(jsonObject.length());
160 @SuppressWarnings("unchecked")
161 final Iterator<String> iterator = jsonObject.keys();
162 while (iterator.hasNext()) {
163 final String key = iterator.next();
164 try {
165 final int id = Integer.parseInt(key);
166 final Transition transition = transitionJsonParser.parse(jsonObject.getJSONObject(key), id);
167 transitions.add(transition);
168 } catch (JSONException e) {
169 throw new RestClientException(e);
170 } catch (NumberFormatException e) {
171 throw new RestClientException("Transition id should be an integer, but found [" + key + "]", e);
172 }
173 }
174 return transitions;
175 }
176 }
177 });
178 }
179
180 @Override
181 public Iterable<Transition> getTransitions(final Issue issue, ProgressMonitor progressMonitor) {
182 return getTransitions(issue.getTransitionsUri(), progressMonitor);
183 }
184
185 @Override
186 public void transition(final URI transitionsUri, final TransitionInput transitionInput, final ProgressMonitor progressMonitor) {
187 final int buildNumber = getVersionInfo(progressMonitor).getBuildNumber();
188 invoke(new Callable<Void>() {
189 @Override
190 public Void call() throws Exception {
191 JSONObject jsonObject = new JSONObject();
192 if (buildNumber >= ServerVersionConstants.BN_JIRA_5) {
193 jsonObject.put("transition", new JSONObject().put("id", transitionInput.getId()));
194 } else {
195 jsonObject.put("transition", transitionInput.getId());
196 }
197 if (transitionInput.getComment() != null) {
198 if (buildNumber >= ServerVersionConstants.BN_JIRA_5) {
199 jsonObject.put("update", new JSONObject().put("comment",
200 new JSONArray().put(new JSONObject().put("add",
201 new CommentJsonGenerator(getVersionInfo(progressMonitor))
202 .generate(transitionInput.getComment())))));
203 } else {
204 jsonObject.put("comment", new CommentJsonGenerator(getVersionInfo(progressMonitor))
205 .generate(transitionInput.getComment()));
206 }
207 }
208 JSONObject fieldsJs = new JSONObject();
209 final Iterable<FieldInput> fields = transitionInput.getFields();
210 final ComplexIssueInputFieldValueJsonGenerator fieldValueGenerator = new ComplexIssueInputFieldValueJsonGenerator();
211 if (fields.iterator().hasNext()) {
212 for (FieldInput fieldInput : fields) {
213 fieldsJs.put(fieldInput.getId(), fieldValueGenerator.generateFieldValueForJson(fieldInput.getValue()));
214 }
215 }
216 if (fieldsJs.keys().hasNext()) {
217 jsonObject.put("fields", fieldsJs);
218 }
219 final WebResource issueResource = client.resource(transitionsUri);
220 issueResource.post(jsonObject);
221 return null;
222 }
223 });
224 }
225
226 @Override
227 public void transition(final Issue issue, final TransitionInput transitionInput, final ProgressMonitor progressMonitor) {
228 transition(issue.getTransitionsUri(), transitionInput, progressMonitor);
229 }
230
231
232 @Override
233 public void vote(final URI votesUri, ProgressMonitor progressMonitor) {
234 invoke(new Callable<Void>() {
235 @Override
236 public Void call() throws Exception {
237 final WebResource votesResource = client.resource(votesUri);
238 votesResource.post();
239 return null;
240 }
241 });
242 }
243
244 @Override
245 public void unvote(final URI votesUri, ProgressMonitor progressMonitor) {
246 invoke(new Callable<Void>() {
247 @Override
248 public Void call() throws Exception {
249 final WebResource votesResource = client.resource(votesUri);
250 votesResource.delete();
251 return null;
252 }
253 });
254
255 }
256
257 @Override
258 public void addWatcher(final URI watchersUri, @Nullable final String username, ProgressMonitor progressMonitor) {
259 invoke(new Callable<Void>() {
260 @Override
261 public Void call() throws Exception {
262 final WebResource.Builder builder = client.resource(watchersUri).type(MediaType.APPLICATION_JSON_TYPE);
263 if (username != null) {
264 builder.post(JSONObject.quote(username));
265 } else {
266 builder.post();
267 }
268 return null;
269 }
270 });
271
272 }
273
274 private String getLoggedUsername(ProgressMonitor progressMonitor) {
275 final Session session = sessionRestClient.getCurrentSession(progressMonitor);
276 return session.getUsername();
277 }
278
279 @Override
280 public void removeWatcher(final URI watchersUri, final String username, final ProgressMonitor progressMonitor) {
281 final UriBuilder uriBuilder = UriBuilder.fromUri(watchersUri);
282 if (getVersionInfo(progressMonitor).getBuildNumber() >= ServerVersionConstants.BN_JIRA_4_4) {
283 uriBuilder.queryParam("username", username);
284 } else {
285 uriBuilder.path(username).build();
286 }
287 delete(uriBuilder.build(), progressMonitor);
288 }
289
290 @Override
291 public void linkIssue(final LinkIssuesInput linkIssuesInput, final ProgressMonitor progressMonitor) {
292 final URI uri = UriBuilder.fromUri(baseUri).path("issueLink").build();
293 post(uri, new Callable<JSONObject>() {
294
295 @Override
296 public JSONObject call() throws Exception {
297 return new LinkIssuesInputGenerator(getVersionInfo(progressMonitor)).generate(linkIssuesInput);
298 }
299 }, progressMonitor);
300 }
301
302 @Override
303 public void addAttachment(ProgressMonitor progressMonitor, final URI attachmentsUri, final InputStream in, final String filename) {
304 addAttachments(progressMonitor, attachmentsUri, new AttachmentInput(filename, in));
305 }
306
307 @Override
308 public void addAttachments(ProgressMonitor progressMonitor, final URI attachmentsUri, AttachmentInput... attachments) {
309
310 final ArrayList<AttachmentInput> myAttachments = Lists.newArrayList(attachments);
311 invoke(new Callable<Void>() {
312 @Override
313 public Void call() throws Exception {
314 final MultiPart multiPartInput = new MultiPart();
315 for (AttachmentInput attachment : myAttachments) {
316 BodyPart bp = new BodyPart(attachment.getInputStream(), MediaType.APPLICATION_OCTET_STREAM_TYPE);
317 FormDataContentDisposition.FormDataContentDispositionBuilder dispositionBuilder =
318 FormDataContentDisposition.name(FILE_ATTACHMENT_CONTROL_NAME);
319 dispositionBuilder.fileName(attachment.getFilename());
320 final FormDataContentDisposition formDataContentDisposition = dispositionBuilder.build();
321 bp.setContentDisposition(formDataContentDisposition);
322 multiPartInput.bodyPart(bp);
323 }
324
325 postFileMultiPart(multiPartInput, attachmentsUri);
326 return null;
327 }
328
329 });
330 }
331
332 @Override
333 public InputStream getAttachment(ProgressMonitor pm, final URI attachmentUri) {
334 return invoke(new Callable<InputStream>() {
335 @Override
336 public InputStream call() throws Exception {
337 final WebResource attachmentResource = client.resource(attachmentUri);
338 return attachmentResource.get(InputStream.class);
339 }
340 });
341 }
342
343 @Override
344 public void addAttachments(ProgressMonitor progressMonitor, final URI attachmentsUri, File... files) {
345 final ArrayList<File> myFiles = Lists.newArrayList(files);
346 invoke(new Callable<Void>() {
347 @Override
348 public Void call() throws Exception {
349 final MultiPart multiPartInput = new MultiPart();
350 for (File file : myFiles) {
351 FileDataBodyPart fileDataBodyPart = new FileDataBodyPart(FILE_ATTACHMENT_CONTROL_NAME, file);
352 multiPartInput.bodyPart(fileDataBodyPart);
353 }
354 postFileMultiPart(multiPartInput, attachmentsUri);
355 return null;
356 }
357
358 });
359
360 }
361
362
363
364
365 @Override
366 public void addComment(final ProgressMonitor progressMonitor, final URI commentsUri, final Comment comment) {
367 post(commentsUri, new Callable<JSONObject>() {
368 @Override
369 public JSONObject call() throws Exception {
370 return new CommentJsonGenerator(getVersionInfo(progressMonitor)).generate(comment);
371 }
372 }, progressMonitor);
373 }
374
375 private void postFileMultiPart(MultiPart multiPartInput, URI attachmentsUri) {
376 final WebResource attachmentsResource = client.resource(attachmentsUri);
377 final WebResource.Builder builder = attachmentsResource.type(MultiPartMediaTypes.createFormData());
378 builder.header("X-Atlassian-Token", "nocheck");
379 builder.post(multiPartInput);
380 }
381
382
383 @Override
384 public void watch(final URI watchersUri, ProgressMonitor progressMonitor) {
385 addWatcher(watchersUri, null, progressMonitor);
386 }
387
388 @Override
389 public void unwatch(final URI watchersUri, ProgressMonitor progressMonitor) {
390 removeWatcher(watchersUri, getLoggedUsername(progressMonitor), progressMonitor);
391 }
392
393 @Override
394 public BasicIssue createIssue(IssueInput issue, ProgressMonitor progressMonitor) {
395 final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri);
396 uriBuilder.path("issue");
397
398 return postAndParse(uriBuilder.build(),
399 InputGeneratorCallable.create(new IssueInputJsonGenerator(), issue),
400 basicIssueParser, progressMonitor);
401 }
402
403 @Override
404 public Iterable<CimProject> getCreateIssueMetadata(@Nullable GetCreateIssueMetadataOptions options, ProgressMonitor progressMonitor) {
405
406 final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path("issue/createmeta");
407
408 if (options != null) {
409 if (options.projectIds != null) {
410 uriBuilder.queryParam("projectIds", Joiner.on(",").join(options.projectIds));
411 }
412
413 if (options.projectKeys != null) {
414 uriBuilder.queryParam("projectKeys", Joiner.on(",").join(options.projectKeys));
415 }
416
417 if (options.issueTypeIds != null) {
418 uriBuilder.queryParam("issuetypeIds", Joiner.on(",").join(options.issueTypeIds));
419 }
420
421 final Iterable<String> issueTypeNames = options.issueTypeNames;
422 if (issueTypeNames != null) {
423 for (final String name : issueTypeNames) {
424 uriBuilder.queryParam("issuetypeNames", name);
425 }
426 }
427
428 final Iterable<String> expandos = options.expandos;
429 if (expandos != null && expandos.iterator().hasNext()) {
430 uriBuilder.queryParam("expand", Joiner.on(",").join(expandos));
431 }
432 }
433
434 return getAndParse(uriBuilder.build(), createIssueMetadataJsonParser, progressMonitor);
435 }
436
437 @Override
438 public void addWorklog(final URI worklogUri, final WorklogInput worklogInput, final ProgressMonitor progressMonitor) {
439 final UriBuilder uriBuilder = UriBuilder.fromUri(worklogUri)
440 .queryParam("adjustEstimate", worklogInput.getAdjustEstimate().restValue);
441
442 switch (worklogInput.getAdjustEstimate()) {
443 case NEW:
444 uriBuilder.queryParam("newEstimate", Strings.nullToEmpty(worklogInput.getAdjustEstimateValue()));
445 break;
446 case MANUAL:
447 uriBuilder.queryParam("reduceBy", Strings.nullToEmpty(worklogInput.getAdjustEstimateValue()));
448 break;
449 }
450
451 post(uriBuilder.build(), new Callable<JSONObject>() {
452 @Override
453 public JSONObject call() throws Exception {
454 return new WorklogInputJsonGenerator().generate(worklogInput);
455 }
456 }, progressMonitor);
457 }
458 }