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.IssueRestClient;
20  import com.atlassian.jira.rest.client.MetadataRestClient;
21  import com.atlassian.jira.rest.client.ProgressMonitor;
22  import com.atlassian.jira.rest.client.RestClientException;
23  import com.atlassian.jira.rest.client.SessionRestClient;
24  import com.atlassian.jira.rest.client.domain.Issue;
25  import com.atlassian.jira.rest.client.domain.ServerInfo;
26  import com.atlassian.jira.rest.client.domain.Session;
27  import com.atlassian.jira.rest.client.domain.Transition;
28  import com.atlassian.jira.rest.client.domain.Votes;
29  import com.atlassian.jira.rest.client.domain.Watchers;
30  import com.atlassian.jira.rest.client.domain.input.AttachmentInput;
31  import com.atlassian.jira.rest.client.domain.input.FieldInput;
32  import com.atlassian.jira.rest.client.domain.input.LinkIssuesInput;
33  import com.atlassian.jira.rest.client.domain.input.TransitionInput;
34  import com.atlassian.jira.rest.client.internal.ServerVersionConstants;
35  import com.atlassian.jira.rest.client.internal.json.IssueJsonParser;
36  import com.atlassian.jira.rest.client.internal.json.JsonParseUtil;
37  import com.atlassian.jira.rest.client.internal.json.JsonParser;
38  import com.atlassian.jira.rest.client.internal.json.TransitionJsonParser;
39  import com.atlassian.jira.rest.client.internal.json.TransitionJsonParserV5;
40  import com.atlassian.jira.rest.client.internal.json.VotesJsonParser;
41  import com.atlassian.jira.rest.client.internal.json.WatchersJsonParserBuilder;
42  import com.atlassian.jira.rest.client.internal.json.gen.CommentJsonGenerator;
43  import com.atlassian.jira.rest.client.internal.json.gen.LinkIssuesInputGenerator;
44  import com.google.common.collect.Lists;
45  import com.sun.jersey.api.client.WebResource;
46  import com.sun.jersey.client.apache.ApacheHttpClient;
47  import com.sun.jersey.core.header.FormDataContentDisposition;
48  import com.sun.jersey.multipart.BodyPart;
49  import com.sun.jersey.multipart.MultiPart;
50  import com.sun.jersey.multipart.MultiPartMediaTypes;
51  import com.sun.jersey.multipart.file.FileDataBodyPart;
52  import org.codehaus.jettison.json.JSONArray;
53  import org.codehaus.jettison.json.JSONException;
54  import org.codehaus.jettison.json.JSONObject;
55  
56  import javax.annotation.Nullable;
57  import javax.ws.rs.core.MediaType;
58  import javax.ws.rs.core.UriBuilder;
59  import java.io.File;
60  import java.io.InputStream;
61  import java.net.URI;
62  import java.util.ArrayList;
63  import java.util.Collection;
64  import java.util.Iterator;
65  import java.util.concurrent.Callable;
66  
67  /**
68   * Jersey-based implementation of IssueRestClient
69   *
70   * @since v0.1
71   */
72  public class JerseyIssueRestClient extends AbstractJerseyRestClient implements IssueRestClient {
73  
74  	private static final String FILE_ATTACHMENT_CONTROL_NAME = "file";
75  	private final SessionRestClient sessionRestClient;
76  	private final MetadataRestClient metadataRestClient;
77  
78  	private final IssueJsonParser issueParser = new IssueJsonParser();
79  	private final JsonParser<Watchers> watchersParser = WatchersJsonParserBuilder.createWatchersParser();
80  	private final TransitionJsonParser transitionJsonParser = new TransitionJsonParser();
81  	private final JsonParser<Transition> transitionJsonParserV5 = new TransitionJsonParserV5();
82  	private final VotesJsonParser votesJsonParser = new VotesJsonParser();
83  	private ServerInfo serverInfo;
84  
85  	public JerseyIssueRestClient(URI baseUri, ApacheHttpClient client, SessionRestClient sessionRestClient, MetadataRestClient metadataRestClient) {
86  		super(baseUri, client);
87  		this.sessionRestClient = sessionRestClient;
88  		this.metadataRestClient = metadataRestClient;
89  	}
90  
91  	private synchronized ServerInfo getVersionInfo(ProgressMonitor progressMonitor) {
92  		if (serverInfo == null) {
93  			serverInfo = metadataRestClient.getServerInfo(progressMonitor);
94  		}
95  		return serverInfo;
96  	}
97  
98  	@Override
99  	public Watchers getWatchers(URI watchersUri, ProgressMonitor progressMonitor) {
100 		return getAndParse(watchersUri, watchersParser, progressMonitor);
101 	}
102 
103 
104 	@Override
105 	public Votes getVotes(URI votesUri, ProgressMonitor progressMonitor) {
106 		return getAndParse(votesUri, votesJsonParser, progressMonitor);
107 	}
108 
109 	@Override
110 	public Issue getIssue(final String issueKey, ProgressMonitor progressMonitor) {
111 		final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri);
112 		uriBuilder.path("issue").path(issueKey).queryParam("expand", IssueJsonParser.NAMES_SECTION +"," + IssueJsonParser.SCHEMA_SECTION + "," + IssueJsonParser.TRANSITIONS_SECTION);
113 		return getAndParse(uriBuilder.build(), issueParser, progressMonitor);
114 	}
115 
116 	@Override
117 	public Iterable<Transition> getTransitions(final URI transitionsUri, ProgressMonitor progressMonitor) {
118 		return invoke(new Callable<Iterable<Transition>>() {
119 			@Override
120 			public Iterable<Transition> call() throws Exception {
121 				final WebResource transitionsResource = client.resource(transitionsUri);
122 				final JSONObject jsonObject = transitionsResource.get(JSONObject.class);
123 				if (jsonObject.has("transitions")) {
124 					return JsonParseUtil.parseJsonArray(jsonObject.getJSONArray("transitions"), transitionJsonParserV5);
125 				} else {
126 					final Collection<Transition> transitions = new ArrayList<Transition>(jsonObject.length());
127 					@SuppressWarnings("unchecked")
128 					final Iterator<String> iterator = jsonObject.keys();
129 					while (iterator.hasNext()) {
130 						final String key = iterator.next();
131 						try {
132 							final int id = Integer.parseInt(key);
133 							final Transition transition = transitionJsonParser.parse(jsonObject.getJSONObject(key), id);
134 							transitions.add(transition);
135 						} catch (JSONException e) {
136 							throw new RestClientException(e);
137 						} catch (NumberFormatException e) {
138 							throw new RestClientException("Transition id should be an integer, but found [" + key + "]", e);
139 						}
140 					}
141 					return transitions;
142 				}
143 			}
144 		});
145 	}
146 
147 	@Override
148 	public Iterable<Transition> getTransitions(final Issue issue, ProgressMonitor progressMonitor) {
149 		if (issue.getTransitionsUri() != null) {
150 			return getTransitions(issue.getTransitionsUri(), progressMonitor);
151 		} else {
152 			final UriBuilder transitionsUri = UriBuilder.fromUri(issue.getSelf());
153 			return getTransitions(transitionsUri.path("transitions").queryParam("expand", "transitions.fields").build(), progressMonitor);
154 		}
155 	}
156 
157 	@Override
158 	public void transition(final URI transitionsUri, final TransitionInput transitionInput, final ProgressMonitor progressMonitor) {
159 		final int buildNumber = getVersionInfo(progressMonitor).getBuildNumber();
160 		invoke(new Callable<Void>() {
161 			@Override
162 			public Void call() throws Exception {
163 				JSONObject jsonObject = new JSONObject();
164 				if (buildNumber >= ServerVersionConstants.BN_JIRA_5) {
165 					jsonObject.put("transition", new JSONObject().put("id", transitionInput.getId()));
166 				} else {
167 					jsonObject.put("transition", transitionInput.getId());
168 				}
169 				if (transitionInput.getComment() != null) {
170 					if (buildNumber >= ServerVersionConstants.BN_JIRA_5) {
171 						jsonObject.put("update", new JSONObject().put("comment",
172 								new JSONArray().put(new JSONObject().put("add",
173 										new CommentJsonGenerator(getVersionInfo(progressMonitor), "group")
174 												.generate(transitionInput.getComment())))));
175 					} else {
176 						jsonObject.put("comment", new CommentJsonGenerator(getVersionInfo(progressMonitor), "group").generate(transitionInput.getComment()));
177 					}
178 				}
179 				JSONObject fieldsJs = new JSONObject();
180 				final Iterable<FieldInput> fields = transitionInput.getFields();
181 				if (fields.iterator().hasNext()) {
182 					for (FieldInput fieldInput : fields) {
183 						fieldsJs.put(fieldInput.getId(), fieldInput.getValue());
184 					}
185 				}
186 				if (fieldsJs.keys().hasNext()) {
187 					jsonObject.put("fields", fieldsJs);
188 				}
189 				final WebResource issueResource = client.resource(transitionsUri);
190 				issueResource.post(jsonObject);
191 				return null;
192 			}
193 		});
194 	}
195 
196 	@Override
197 	public void transition(final Issue issue, final TransitionInput transitionInput, final ProgressMonitor progressMonitor) {
198 		if (issue.getTransitionsUri() != null) {
199 			transition(issue.getTransitionsUri(), transitionInput, progressMonitor);
200 		} else {
201 			final UriBuilder uriBuilder = UriBuilder.fromUri(issue.getSelf());
202 			uriBuilder.path("transitions");
203 			transition(uriBuilder.build(), transitionInput, progressMonitor);
204 		}
205 	}
206 
207 
208 	@Override
209 	public void vote(final URI votesUri, ProgressMonitor progressMonitor) {
210 		invoke(new Callable<Void>() {
211 			@Override
212 			public Void call() throws Exception {
213 				final WebResource votesResource = client.resource(votesUri);
214 				votesResource.post();
215 				return null;
216 			}
217 		});
218 	}
219 
220 	@Override
221 	public void unvote(final URI votesUri, ProgressMonitor progressMonitor) {
222 		invoke(new Callable<Void>() {
223 			@Override
224 			public Void call() throws Exception {
225 				final WebResource votesResource = client.resource(votesUri);
226 				votesResource.delete();
227 				return null;
228 			}
229 		});
230 
231 	}
232 
233 	@Override
234 	public void addWatcher(final URI watchersUri, @Nullable final String username, ProgressMonitor progressMonitor) {
235 		invoke(new Callable<Void>() {
236 			@Override
237 			public Void call() throws Exception {
238 				final WebResource.Builder builder = client.resource(watchersUri).type(MediaType.APPLICATION_JSON_TYPE);
239 				if (username != null) {
240 					builder.post(JSONObject.quote(username));
241 				} else {
242 					builder.post();
243 				}
244 				return null;
245 			}
246 		});
247 
248 	}
249 
250 	private String getLoggedUsername(ProgressMonitor progressMonitor) {
251 		final Session session = sessionRestClient.getCurrentSession(progressMonitor);
252 		return session.getUsername();
253 	}
254 
255 	@Override
256 	public void removeWatcher(final URI watchersUri, final String username, final ProgressMonitor progressMonitor) {
257 		final UriBuilder uriBuilder = UriBuilder.fromUri(watchersUri);
258 		if (getVersionInfo(progressMonitor).getBuildNumber() >= ServerVersionConstants.BN_JIRA_4_4) {
259 			uriBuilder.queryParam("username", username);
260 		} else {
261 			uriBuilder.path(username).build();
262 		}
263 		delete(uriBuilder.build(), progressMonitor);
264 	}
265 
266 	@Override
267 	public void linkIssue(final LinkIssuesInput linkIssuesInput, final ProgressMonitor progressMonitor) {
268 		final URI uri = UriBuilder.fromUri(baseUri).path("issueLink").build();
269 		post(uri, new Callable<JSONObject>() {
270 
271 			@Override
272 			public JSONObject call() throws Exception {
273 				return new LinkIssuesInputGenerator(getVersionInfo(progressMonitor)).generate(linkIssuesInput);
274 			}
275 		}, progressMonitor);
276 	}
277 
278 	@Override
279 	public void addAttachment(ProgressMonitor progressMonitor, final URI attachmentsUri, final InputStream in, final String filename) {
280 		addAttachments(progressMonitor, attachmentsUri, new AttachmentInput(filename, in));
281 	}
282 
283 	@Override
284 	public void addAttachments(ProgressMonitor progressMonitor, final URI attachmentsUri, AttachmentInput... attachments) {
285 		final ArrayList<AttachmentInput> myAttachments = Lists.newArrayList(attachments); // just to avoid concurrency issues if this arg is mutable
286 		invoke(new Callable<Void>() {
287 			@Override
288 			public Void call() throws Exception {
289 				final MultiPart multiPartInput = new MultiPart();
290 				for (AttachmentInput attachment : myAttachments) {
291 					BodyPart bp = new BodyPart(attachment.getInputStream(), MediaType.APPLICATION_OCTET_STREAM_TYPE);
292 					FormDataContentDisposition.FormDataContentDispositionBuilder dispositionBuilder =
293 							FormDataContentDisposition.name(FILE_ATTACHMENT_CONTROL_NAME);
294 					dispositionBuilder.fileName(attachment.getFilename());
295 					final FormDataContentDisposition formDataContentDisposition = dispositionBuilder.build();
296 					bp.setContentDisposition(formDataContentDisposition);
297 					multiPartInput.bodyPart(bp);
298 				}
299 
300 				postFileMultiPart(multiPartInput, attachmentsUri);
301 				return null;
302 			}
303 
304 		});
305 	}
306 
307 	@Override
308 	public InputStream getAttachment(ProgressMonitor pm, final URI attachmentUri) {
309 		return invoke(new Callable<InputStream>() {
310 			@Override
311 			public InputStream call() throws Exception {
312 				final WebResource attachmentResource = client.resource(attachmentUri);
313 				return attachmentResource.get(InputStream.class);
314 			}
315 		});
316 	}
317 
318 	@Override
319 	public void addAttachments(ProgressMonitor progressMonitor, final URI attachmentsUri, File... files) {
320 		final ArrayList<File> myFiles = Lists.newArrayList(files); // just to avoid concurrency issues if this arg is mutable
321 		invoke(new Callable<Void>() {
322 			@Override
323 			public Void call() throws Exception {
324 				final MultiPart multiPartInput = new MultiPart();
325 				for (File file : myFiles) {
326 					FileDataBodyPart fileDataBodyPart = new FileDataBodyPart(FILE_ATTACHMENT_CONTROL_NAME, file);
327 					multiPartInput.bodyPart(fileDataBodyPart);
328 				}
329 				postFileMultiPart(multiPartInput, attachmentsUri);
330 				return null;
331 			}
332 
333 		});
334 
335 	}
336 
337 	private void postFileMultiPart(MultiPart multiPartInput, URI attachmentsUri) {
338 		final WebResource attachmentsResource = client.resource(attachmentsUri);
339 		final WebResource.Builder builder = attachmentsResource.type(MultiPartMediaTypes.createFormData());
340 		builder.header("X-Atlassian-Token", "nocheck"); // this is required by server side REST API
341 		builder.post(multiPartInput);
342 	}
343 
344 
345 	@Override
346 	public void watch(final URI watchersUri, ProgressMonitor progressMonitor) {
347 		addWatcher(watchersUri, null, progressMonitor);
348 	}
349 
350 	@Override
351 	public void unwatch(final URI watchersUri, ProgressMonitor progressMonitor) {
352 		removeWatcher(watchersUri, getLoggedUsername(progressMonitor), progressMonitor);
353 	}
354 
355 }