View Javadoc

1   /*
2    * Copyright (C) 2010 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  
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   * Jersey-based implementation of IssueRestClient
86   *
87   * @since v0.1
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 		// just to avoid concurrency issues if this arg is mutable
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); // just to avoid concurrency issues if this arg is mutable
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 	 * {@inheritDoc}
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"); // this is required by server side REST API
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 }