View Javadoc

1   /*
2    * Copyright (C) 2012 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  package com.atlassian.jira.rest.client.internal.async;
17  
18  import com.atlassian.httpclient.apache.httpcomponents.MultiPartEntityBuilder;
19  import com.atlassian.httpclient.api.HttpClient;
20  import com.atlassian.httpclient.api.Response;
21  import com.atlassian.httpclient.api.ResponsePromise;
22  import com.atlassian.jira.rest.client.api.*;
23  import com.atlassian.jira.rest.client.api.domain.*;
24  import com.atlassian.jira.rest.client.api.domain.input.*;
25  import com.atlassian.jira.rest.client.internal.ServerVersionConstants;
26  import com.atlassian.jira.rest.client.internal.json.*;
27  import com.atlassian.jira.rest.client.internal.json.gen.*;
28  import com.atlassian.util.concurrent.Promise;
29  import com.google.common.base.Function;
30  import com.google.common.base.Joiner;
31  import com.google.common.base.Strings;
32  import com.google.common.collect.Iterables;
33  import org.apache.http.entity.mime.HttpMultipartMode;
34  import org.apache.http.entity.mime.MultipartEntity;
35  import org.apache.http.entity.mime.content.FileBody;
36  import org.apache.http.entity.mime.content.InputStreamBody;
37  import org.codehaus.jettison.json.JSONArray;
38  import org.codehaus.jettison.json.JSONException;
39  import org.codehaus.jettison.json.JSONObject;
40  
41  import javax.annotation.Nullable;
42  import javax.ws.rs.core.UriBuilder;
43  import java.io.File;
44  import java.io.IOException;
45  import java.io.InputStream;
46  import java.net.URI;
47  import java.nio.charset.Charset;
48  import java.util.*;
49  
50  /**
51   * Asynchronous implementation of IssueRestClient.
52   *
53   * @since v2.0
54   */
55  public class AsynchronousIssueRestClient extends AbstractAsynchronousRestClient implements IssueRestClient {
56  
57  	private static final EnumSet<Expandos> DEFAULT_EXPANDS = EnumSet.of(Expandos.NAMES, Expandos.SCHEMA, Expandos.TRANSITIONS);
58  	private static final Function<IssueRestClient.Expandos, String> EXPANDO_TO_PARAM = new Function<Expandos, String>() {
59  		@Override
60  		public String apply(Expandos from) {
61  			return from.name().toLowerCase();
62  		}
63  	};
64  	private final SessionRestClient sessionRestClient;
65  	private final MetadataRestClient metadataRestClient;
66  
67  	private final IssueJsonParser issueParser = new IssueJsonParser();
68  	private final BasicIssueJsonParser basicIssueParser = new BasicIssueJsonParser();
69  	private final JsonObjectParser<Watchers> watchersParser = WatchersJsonParserBuilder.createWatchersParser();
70  	private final TransitionJsonParser transitionJsonParser = new TransitionJsonParser();
71  	private final JsonObjectParser<Transition> transitionJsonParserV5 = new TransitionJsonParserV5();
72  	private final VotesJsonParser votesJsonParser = new VotesJsonParser();
73  	private final CreateIssueMetadataJsonParser createIssueMetadataJsonParser = new CreateIssueMetadataJsonParser();
74  	private static final String FILE_BODY_TYPE = "file";
75  	private final URI baseUri;
76  	private ServerInfo serverInfo;
77  
78  	public AsynchronousIssueRestClient(final URI baseUri, final HttpClient client, final SessionRestClient sessionRestClient,
79  			final MetadataRestClient metadataRestClient) {
80  		super(client);
81  		this.baseUri = baseUri;
82  		this.sessionRestClient = sessionRestClient;
83  		this.metadataRestClient = metadataRestClient;
84  	}
85  
86  	private synchronized ServerInfo getVersionInfo() {
87  		if (serverInfo == null) {
88  			serverInfo = metadataRestClient.getServerInfo().claim();
89  		}
90  		return serverInfo;
91  	}
92  
93  	@Override
94  	public Promise<BasicIssue> createIssue(final IssueInput issue) {
95  		final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path("issue");
96  		return postAndParse(uriBuilder.build(), issue, new IssueInputJsonGenerator(), basicIssueParser);
97  	}
98  
99  	@Override
100 	public Promise<BulkOperationResult<BasicIssue>> createIssues(Collection<IssueInput> issues) {
101 		final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path("issue/bulk");
102 
103 		return postAndParse(uriBuilder.build(), issues, new IssuesInputJsonGenerator(), new BasicIssuesJsonParser());
104 	}
105 
106 	@Override
107 	public Promise<Iterable<CimProject>> getCreateIssueMetadata(@Nullable GetCreateIssueMetadataOptions options) {
108 		final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path("issue/createmeta");
109 
110 		if (options != null) {
111 			if (options.projectIds != null) {
112 				uriBuilder.queryParam("projectIds", Joiner.on(",").join(options.projectIds));
113 			}
114 
115 			if (options.projectKeys != null) {
116 				uriBuilder.queryParam("projectKeys", Joiner.on(",").join(options.projectKeys));
117 			}
118 
119 			if (options.issueTypeIds != null) {
120 				uriBuilder.queryParam("issuetypeIds", Joiner.on(",").join(options.issueTypeIds));
121 			}
122 
123 			final Iterable<String> issueTypeNames = options.issueTypeNames;
124 			if (issueTypeNames != null) {
125 				for (final String name : issueTypeNames) {
126 					uriBuilder.queryParam("issuetypeNames", name);
127 				}
128 			}
129 
130 			final Iterable<String> expandos = options.expandos;
131 			if (expandos != null && expandos.iterator().hasNext()) {
132 				uriBuilder.queryParam("expand", Joiner.on(",").join(expandos));
133 			}
134 		}
135 
136 		return getAndParse(uriBuilder.build(), createIssueMetadataJsonParser);
137 	}
138 
139 	@Override
140 	public Promise<Issue> getIssue(final String issueKey) {
141 		return getIssue(issueKey, Collections.<Expandos>emptyList());
142 	}
143 
144 	@Override
145 	public Promise<Issue> getIssue(final String issueKey, final Iterable<Expandos> expand) {
146 		final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri);
147 		final Iterable<Expandos> expands = Iterables.concat(DEFAULT_EXPANDS, expand);
148 		uriBuilder.path("issue").path(issueKey).queryParam("expand",
149 				Joiner.on(',').join(Iterables.transform(expands, EXPANDO_TO_PARAM)));
150 		return getAndParse(uriBuilder.build(), issueParser);
151 	}
152 
153 	@Override
154 	public Promise<Watchers> getWatchers(final URI watchersUri) {
155 		return getAndParse(watchersUri, watchersParser);
156 	}
157 
158 	@Override
159 	public Promise<Votes> getVotes(final URI votesUri) {
160 		return getAndParse(votesUri, votesJsonParser);
161 	}
162 
163 	@Override
164 	public Promise<Iterable<Transition>> getTransitions(final URI transitionsUri) {
165 		return callAndParse(client().newRequest(transitionsUri).get(),
166 				new AbstractAsynchronousRestClient.ResponseHandler<Iterable<Transition>>() {
167 					@Override
168 					public Iterable<Transition> handle(Response response) throws JSONException, IOException {
169 						final JSONObject jsonObject = new JSONObject(response.getEntity());
170 						if (jsonObject.has("transitions")) {
171 							return JsonParseUtil.parseJsonArray(jsonObject.getJSONArray("transitions"), transitionJsonParserV5);
172 						} else {
173 							final Collection<Transition> transitions = new ArrayList<Transition>(jsonObject.length());
174 							@SuppressWarnings("unchecked")
175 							final Iterator<String> iterator = jsonObject.keys();
176 							while (iterator.hasNext()) {
177 								final String key = iterator.next();
178 								try {
179 									final int id = Integer.parseInt(key);
180 									final Transition transition = transitionJsonParser.parse(jsonObject.getJSONObject(key), id);
181 									transitions.add(transition);
182 								} catch (JSONException e) {
183 									throw new RestClientException(e);
184 								} catch (NumberFormatException e) {
185 									throw new RestClientException(
186 											"Transition id should be an integer, but found [" + key + "]", e);
187 								}
188 							}
189 							return transitions;
190 						}
191 					}
192 				}
193 		);
194 	}
195 
196 	@Override
197 	public Promise<Iterable<Transition>> getTransitions(final Issue issue) {
198 		if (issue.getTransitionsUri() != null) {
199 			return getTransitions(issue.getTransitionsUri());
200 		} else {
201 			final UriBuilder transitionsUri = UriBuilder.fromUri(issue.getSelf());
202 			return getTransitions(transitionsUri.path("transitions").queryParam("expand", "transitions.fields").build());
203 		}
204 	}
205 
206 	@Override
207 	public Promise<Void> transition(final URI transitionsUri, final TransitionInput transitionInput) {
208 		final int buildNumber = getVersionInfo().getBuildNumber();
209 		try {
210 			JSONObject jsonObject = new JSONObject();
211 			if (buildNumber >= ServerVersionConstants.BN_JIRA_5) {
212 				jsonObject.put("transition", new JSONObject().put("id", transitionInput.getId()));
213 			} else {
214 				jsonObject.put("transition", transitionInput.getId());
215 			}
216 			if (transitionInput.getComment() != null) {
217 				if (buildNumber >= ServerVersionConstants.BN_JIRA_5) {
218 					jsonObject.put("update", new JSONObject().put("comment",
219 							new JSONArray().put(new JSONObject().put("add",
220 									new CommentJsonGenerator(getVersionInfo())
221 											.generate(transitionInput.getComment())))));
222 				} else {
223 					jsonObject.put("comment", new CommentJsonGenerator(getVersionInfo())
224 							.generate(transitionInput.getComment()));
225 				}
226 			}
227 			final Iterable<FieldInput> fields = transitionInput.getFields();
228 			final JSONObject fieldsJs = new IssueUpdateJsonGenerator().generate(fields);
229 			if (fieldsJs.keys().hasNext()) {
230 				jsonObject.put("fields", fieldsJs);
231 			}
232 			if (fieldsJs.keys().hasNext()) {
233 				jsonObject.put("fields", fieldsJs);
234 			}
235 			return post(transitionsUri, jsonObject);
236 		} catch (JSONException ex) {
237 			throw new RestClientException(ex);
238 		}
239 	}
240 
241 	@Override
242 	public Promise<Void> transition(final Issue issue, final TransitionInput transitionInput) {
243 		if (issue.getTransitionsUri() != null) {
244 			return transition(issue.getTransitionsUri(), transitionInput);
245 		} else {
246 			final UriBuilder uriBuilder = UriBuilder.fromUri(issue.getSelf());
247 			uriBuilder.path("transitions");
248 			return transition(uriBuilder.build(), transitionInput);
249 		}
250 	}
251 
252 	@Override
253 	public Promise<Void> vote(final URI votesUri) {
254 		return post(votesUri);
255 	}
256 
257 	@Override
258 	public Promise<Void> unvote(final URI votesUri) {
259 		return delete(votesUri);
260 	}
261 
262 	@Override
263 	public Promise<Void> watch(final URI watchersUri) {
264 		return post(watchersUri);
265 	}
266 
267 	@Override
268 	public Promise<Void> unwatch(final URI watchersUri) {
269 		return removeWatcher(watchersUri, getLoggedUsername());
270 	}
271 
272 	@Override
273 	public Promise<Void> addWatcher(final URI watchersUri, final String username) {
274 		return post(watchersUri, JSONObject.quote(username));
275 	}
276 
277 	@Override
278 	public Promise<Void> removeWatcher(final URI watchersUri, final String username) {
279 		final UriBuilder uriBuilder = UriBuilder.fromUri(watchersUri);
280 		if (getVersionInfo().getBuildNumber() >= ServerVersionConstants.BN_JIRA_4_4) {
281 			uriBuilder.queryParam("username", username);
282 		} else {
283 			uriBuilder.path(username).build();
284 		}
285 		return delete(uriBuilder.build());
286 	}
287 
288 	@Override
289 	public Promise<Void> linkIssue(final LinkIssuesInput linkIssuesInput) {
290 		final URI uri = UriBuilder.fromUri(baseUri).path("issueLink").build();
291 		return post(uri, linkIssuesInput, new LinkIssuesInputGenerator(getVersionInfo()));
292 	}
293 
294 	@Override
295 	public Promise<Void> addAttachment(final URI attachmentsUri, final InputStream inputStream, final String filename) {
296 		final MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, null, Charset.defaultCharset());
297 		entity.addPart(FILE_BODY_TYPE, new InputStreamBody(inputStream, filename));
298 		return postAttachments(attachmentsUri, entity);
299 	}
300 
301 	@Override
302 	public Promise<Void> addAttachments(final URI attachmentsUri, final AttachmentInput... attachments) {
303 		final MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, null, Charset.defaultCharset());
304 		for (final AttachmentInput attachmentInput : attachments) {
305 			entity.addPart(FILE_BODY_TYPE, new InputStreamBody(attachmentInput.getInputStream(), attachmentInput.getFilename()));
306 		}
307 		return postAttachments(attachmentsUri, entity);
308 	}
309 
310 	@Override
311 	public Promise<Void> addAttachments(final URI attachmentsUri, final File... files) {
312 		final MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, null, Charset.defaultCharset());
313 		for (final File file : files) {
314 			entity.addPart(FILE_BODY_TYPE, new FileBody(file));
315 		}
316 		return postAttachments(attachmentsUri, entity);
317 	}
318 
319 	@Override
320 	public Promise<Void> addComment(final URI commentsUri, final Comment comment) {
321 		return post(commentsUri, comment, new CommentJsonGenerator(getVersionInfo()));
322 	}
323 
324 	@Override
325 	public Promise<InputStream> getAttachment(URI attachmentUri) {
326 		return callAndParse(client().newRequest(attachmentUri).get(),
327 				new ResponseHandler<InputStream>() {
328 					@Override
329 					public InputStream handle(final Response request) throws JSONException, IOException {
330 						return request.getEntityStream();
331 					}
332 				}
333 		);
334 	}
335 
336 	@Override
337 	public Promise<Void> addWorklog(URI worklogUri, WorklogInput worklogInput) {
338 		final UriBuilder uriBuilder = UriBuilder.fromUri(worklogUri)
339 				.queryParam("adjustEstimate", worklogInput.getAdjustEstimate().restValue);
340 
341 		switch (worklogInput.getAdjustEstimate()) {
342 			case NEW:
343 				uriBuilder.queryParam("newEstimate", Strings.nullToEmpty(worklogInput.getAdjustEstimateValue()));
344 				break;
345 			case MANUAL:
346 				uriBuilder.queryParam("reduceBy", Strings.nullToEmpty(worklogInput.getAdjustEstimateValue()));
347 				break;
348 		}
349 
350 		return post(uriBuilder.build(), worklogInput, new WorklogInputJsonGenerator());
351 	}
352 
353 	private Promise<Void> postAttachments(final URI attachmentsUri, final MultipartEntity multipartEntity) {
354 		final ResponsePromise responsePromise = client()
355 				.newRequest(attachmentsUri)
356 				.setEntity(new MultiPartEntityBuilder(multipartEntity))
357 				.setHeader("X-Atlassian-Token", "nocheck")
358 				.post();
359 		return call(responsePromise);
360 	}
361 
362 	private String getLoggedUsername() {
363 		return sessionRestClient.getCurrentSession().claim().getUsername();
364 	}
365 }