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