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