1 package com.atlassian.marketplace.client.impl;
2
3 import java.io.ByteArrayInputStream;
4 import java.io.ByteArrayOutputStream;
5 import java.io.InputStream;
6 import java.net.URI;
7
8 import com.atlassian.fugue.Option;
9 import com.atlassian.marketplace.client.MpacException;
10 import com.atlassian.marketplace.client.api.AddonQuery;
11 import com.atlassian.marketplace.client.api.AddonVersionsQuery;
12 import com.atlassian.marketplace.client.api.ApplicationKey;
13 import com.atlassian.marketplace.client.api.EnumWithKey;
14 import com.atlassian.marketplace.client.api.Page;
15 import com.atlassian.marketplace.client.api.PageReference;
16 import com.atlassian.marketplace.client.api.ProductQuery;
17 import com.atlassian.marketplace.client.api.QueryBounds;
18 import com.atlassian.marketplace.client.api.QueryProperties;
19 import com.atlassian.marketplace.client.api.UriTemplate;
20 import com.atlassian.marketplace.client.api.VendorQuery;
21 import com.atlassian.marketplace.client.http.HttpTransport;
22 import com.atlassian.marketplace.client.http.RequestDecorator;
23 import com.atlassian.marketplace.client.http.SimpleHttpResponse;
24 import com.atlassian.marketplace.client.model.Entity;
25 import com.atlassian.marketplace.client.model.Link;
26 import com.atlassian.marketplace.client.model.Links;
27 import com.atlassian.marketplace.client.util.UriBuilder;
28
29 import com.google.common.collect.ImmutableMap;
30 import com.google.common.collect.Iterables;
31 import com.google.common.collect.Multimap;
32
33 import org.apache.commons.io.IOUtils;
34
35 import static com.atlassian.fugue.Option.none;
36 import static com.atlassian.fugue.Option.some;
37 import static com.google.common.collect.Iterables.isEmpty;
38
39
40
41
42 class ApiHelper
43 {
44 public static final String JSON = "application/json";
45
46 private static final RequestDecorator NO_CACHE =
47 RequestDecorator.Instances.forHeaders(ImmutableMap.of("Cache-Control", "no-cache"));
48
49 private final URI baseUri;
50 private final HttpTransport httpHelper;
51 private final EntityEncoding encoding;
52
53 public ApiHelper(URI baseUri, HttpTransport httpHelper, EntityEncoding encoding)
54 {
55 this.baseUri = baseUri;
56 this.httpHelper = httpHelper;
57 this.encoding = encoding;
58 }
59
60 public EntityEncoding getEncoding()
61 {
62 return encoding;
63 }
64
65 public HttpTransport getHttp()
66 {
67 return httpHelper;
68 }
69
70 public boolean checkReachable(URI resource)
71 {
72 SimpleHttpResponse response = null;
73 try
74 {
75 response = httpHelper.get(resource);
76 return !errorOrEmpty(response.getStatus());
77 }
78 catch (MpacException e)
79 {
80 return false;
81 }
82 finally
83 {
84 closeQuietly(response);
85 }
86 }
87
88 public static URI normalizeBaseUri(URI baseUri)
89 {
90 URI norm = baseUri.normalize();
91 if (norm.getPath().endsWith("/"))
92 {
93 return norm;
94 }
95 return URI.create(norm.toString() + "/");
96 }
97
98 public <T> T getEntity(URI uri, Class<T> type) throws MpacException
99 {
100 return getEntityInternal(httpHelper, uri, type);
101 }
102
103 public <T> T getEntityUncached(URI uri, Class<T> type) throws MpacException
104 {
105 return getEntityInternal(httpHelper.withRequestDecorator(NO_CACHE), uri, type);
106 }
107
108 private <T> T getEntityInternal(HttpTransport h, URI uri, Class<T> type) throws MpacException
109 {
110 SimpleHttpResponse response = null;
111 try
112 {
113 response = h.get(resolveLink(uri));
114 if (errorOrEmpty(response.getStatus()))
115 {
116 throw responseException(response);
117 }
118 return decode(response.getContentStream(), type);
119 }
120 finally
121 {
122 closeQuietly(response);
123 }
124 }
125
126 public <T> Option<T> getOptionalEntity(URI uri, Class<T> type) throws MpacException
127 {
128 SimpleHttpResponse response = null;
129 try
130 {
131 response = httpHelper.get(resolveLink(uri));
132 if ((response.getStatus() == 204) || (response.getStatus() == 404))
133 {
134 return none();
135 }
136 if (error(response.getStatus()))
137 {
138 throw responseException(response);
139 }
140 if (response.isEmpty())
141 {
142 return none();
143 }
144 else
145 {
146 return some(decode(response.getContentStream(), type));
147 }
148 }
149 finally
150 {
151 closeQuietly(response);
152 }
153 }
154
155 public void postParams(URI uri, Multimap<String, String> params) throws MpacException
156 {
157 SimpleHttpResponse response = null;
158 try
159 {
160 response = httpHelper.postParams(resolveLink(uri), params);
161 if (error(response.getStatus()))
162 {
163 throw responseException(response);
164 }
165 }
166 finally
167 {
168 closeQuietly(response);
169 }
170 }
171
172 public <T, U> T postEntity(URI uri, U entity, Class<T> type) throws MpacException
173 {
174 ByteArrayOutputStream bos = new ByteArrayOutputStream();
175 encoding.encode(bos, entity, false);
176 byte[] bytes = bos.toByteArray();
177 return postContent(uri, new ByteArrayInputStream(bytes), bytes.length, JSON, type);
178 }
179
180 public <T> T postContent(URI uri, InputStream content, long length, String contentType, Class<T> returnType) throws MpacException
181 {
182 SimpleHttpResponse response = null;
183 try
184 {
185 response = httpHelper.post(resolveLink(uri), content, length, contentType, contentType);
186 if (errorOrEmpty(response.getStatus()))
187 {
188 throw responseException(response);
189 }
190 return decode(response.getContentStream(), returnType);
191 }
192 finally
193 {
194 closeQuietly(response);
195 }
196 }
197
198 public <T> void putEntity(URI uri, T entity) throws MpacException
199 {
200 ByteArrayOutputStream bos = new ByteArrayOutputStream();
201 encoding.encode(bos, entity, false);
202 SimpleHttpResponse response = null;
203 try
204 {
205 response = httpHelper.put(resolveLink(uri), bos.toByteArray());
206 if (error(response.getStatus()))
207 {
208 throw responseException(response);
209 }
210 }
211 finally
212 {
213 closeQuietly(response);
214 }
215 }
216
217 public <T, U> T putEntity(URI uri, U entity, Class<T> type) throws MpacException
218 {
219 ByteArrayOutputStream bos = new ByteArrayOutputStream();
220 encoding.encode(bos, entity, false);
221 SimpleHttpResponse response = null;
222 try
223 {
224 response = httpHelper.put(resolveLink(uri), bos.toByteArray());
225 if (errorOrEmpty(response.getStatus()))
226 {
227 throw responseException(response);
228 }
229 return decode(response.getContentStream(), type);
230 }
231 finally
232 {
233 closeQuietly(response);
234 }
235 }
236
237 public void deleteEntity(URI uri) throws MpacException
238 {
239 SimpleHttpResponse response = null;
240 try
241 {
242 response = httpHelper.delete(resolveLink(uri));
243 if (error(response.getStatus()))
244 {
245 throw responseException(response);
246 }
247 }
248 finally
249 {
250 closeQuietly(response);
251 }
252 }
253
254 public URI resolveLink(URI href)
255 {
256 return href.isAbsolute() ? href : baseUri.resolve(href.toString());
257 }
258
259 public static Link requireLink(Links links, String rel, Class<?> entityClass) throws MpacException
260 {
261 for (Link l: links.getLink(rel))
262 {
263 return l;
264 }
265 throw new MpacException("Missing required API link \"" + rel + "\" from " + entityClass.getSimpleName());
266 }
267
268 public URI requireLinkUri(Links links, String rel, Class<?> entityClass) throws MpacException
269 {
270 return resolveLink(requireLink(links, rel, entityClass).getUri());
271 }
272
273 public <T> T decode(InputStream is, Class<T> type) throws MpacException
274 {
275 try
276 {
277 return encoding.decode(is, type);
278 }
279 finally
280 {
281 IOUtils.closeQuietly(is);
282 }
283 }
284
285 public boolean error(int statusCode)
286 {
287 return statusCode >= 400;
288 }
289
290 public boolean errorOrEmpty(int statusCode)
291 {
292 return statusCode >= 400 || statusCode == 204;
293 }
294
295 public MpacException responseException(SimpleHttpResponse response)
296 {
297 int status = response.getStatus();
298 try
299 {
300 String body = IOUtils.toString(response.getContentStream());
301 if (body.trim().startsWith("{"))
302 {
303 try
304 {
305 InternalModel.ErrorDetails ed = encoding.decode(new ByteArrayInputStream(body.getBytes()),
306 InternalModel.ErrorDetails.class);
307 return new MpacException.ServerError(status, ed.errors);
308 }
309 catch (Exception e)
310 {
311
312
313
314 }
315 }
316 return new MpacException.ServerError(status, body);
317 }
318 catch (Exception e)
319 {
320
321 return new MpacException.ServerError(status);
322 }
323 }
324
325 protected static void closeQuietly(SimpleHttpResponse response)
326 {
327 if (response != null)
328 {
329 response.close();
330 }
331 }
332
333 private static void addOptionalBoolean(UriBuilder uri, String name, boolean value)
334 {
335 if (value)
336 {
337 uri.queryParam(name, true);
338 }
339 }
340
341 private static <T extends EnumWithKey> void addOptionalEnumKey(UriBuilder uri, String name, Option<T> value)
342 {
343 for (T v: value)
344 {
345 uri.queryParam(name, v.getKey());
346 }
347 }
348
349 public static void addAddonQueryParams(AddonQuery query, UriBuilder uri)
350 {
351 addAccessTokenParams(query, uri);
352 addApplicationCriteriaParams(query, uri);
353 if (!isEmpty(query.getCategoryNames()))
354 {
355 uri.queryParam("category", Iterables.toArray(query.getCategoryNames(), Object.class));
356 }
357 addOptionalEnumKey(uri, "filter", query.getView());
358 addCostParam(query, uri);
359 addOptionalBoolean(uri, "forThisUser", query.isForThisUserOnly());
360 addHostingParam(query, uri);
361 addOptionalEnumKey(uri, "includeHidden", query.getIncludeHidden());
362 addOptionalBoolean(uri, "includePrivate", query.isIncludePrivate());
363 for (String searchText: query.getSearchText())
364 {
365 uri.queryParam("text", searchText);
366 }
367 addOptionalEnumKey(uri, "treatPartlyFreeAs", query.getTreatPartlyFreeAs());
368 addWithVersionParam(query, uri);
369 addBoundsParams(query, uri);
370 }
371
372 public static void addAddonVersionsQueryParams(AddonVersionsQuery query, UriBuilder uri)
373 {
374 addAccessTokenParams(query, uri);
375 addApplicationCriteriaParams(query, uri);
376 addCostParam(query, uri);
377 addHostingParam(query, uri);
378 for (String v: query.getAfterVersionName())
379 {
380 uri.queryParam("afterVersion", v);
381 }
382 addBoundsParams(query, uri);
383 }
384
385 public static void addProductQueryParams(ProductQuery query, UriBuilder uri)
386 {
387 addApplicationCriteriaParams(query, uri);
388 addCostParam(query, uri);
389 addHostingParam(query, uri);
390 addWithVersionParam(query, uri);
391 addBoundsParams(query, uri);
392 }
393
394 public static void addAccessTokenParams(QueryProperties.AccessToken q, UriBuilder uriBuilder)
395 {
396 for (String a: q.getAccessToken())
397 {
398 uriBuilder.queryParam("accessToken", a);
399 }
400 }
401
402 public static void addApplicationCriteriaParams(QueryProperties.ApplicationCriteria q, UriBuilder uriBuilder)
403 {
404 for (ApplicationKey a: q.getApplication())
405 {
406 uriBuilder.queryParam("application", a.getKey());
407 for (Integer b: q.getAppBuildNumber())
408 {
409 uriBuilder.queryParam("applicationBuild", b);
410 }
411 }
412 }
413
414 public static void addBoundsParams(QueryBounds b, UriBuilder uriBuilder)
415 {
416 if (b.getOffset() > 0)
417 {
418 uriBuilder.queryParam("offset", b.getOffset());
419 }
420 for (Integer l: b.getLimit())
421 {
422 uriBuilder.queryParam("limit", l);
423 }
424 }
425
426 public static void addBoundsParams(QueryProperties.Bounds q, UriBuilder uriBuilder)
427 {
428 addBoundsParams(q.getBounds(), uriBuilder);
429 }
430
431 public static void addCostParam(QueryProperties.Cost q, UriBuilder uriBuilder)
432 {
433 addOptionalEnumKey(uriBuilder, "cost", q.getCost());
434 }
435
436 public static void addHostingParam(QueryProperties.Hosting q, UriBuilder uriBuilder)
437 {
438 addOptionalEnumKey(uriBuilder, "hosting", q.getHosting());
439 }
440
441 public static void addWithVersionParam(QueryProperties.WithVersion q, UriBuilder uriBuilder)
442 {
443 addOptionalBoolean(uriBuilder, "withVersion", q.isWithVersion());
444 }
445
446 public static void addVendorQueryParams(VendorQuery q, UriBuilder uriBuilder)
447 {
448 addBoundsParams(q, uriBuilder);
449 addOptionalBoolean(uriBuilder, "forThisUser", q.isForThisUserOnly());
450 }
451
452 public static URI withAccessToken(URI u, String token)
453 {
454 return UriBuilder.fromUri(u).queryParam("accessToken", token).build();
455 }
456
457 public static URI withZeroLimit(URI u)
458 {
459 return UriBuilder.fromUri(u).queryParam("limit", 0).build();
460 }
461
462 public <T> Page<T> getMore(PageReference<T> ref) throws MpacException
463 {
464 SimpleHttpResponse response = null;
465 try
466 {
467 response = httpHelper.get(resolveLink(ref.getUri()));
468 if (errorOrEmpty(response.getStatus()))
469 {
470 throw responseException(response);
471 }
472 return ref.getReader().readPage(ref, response.getContentStream());
473 }
474 finally
475 {
476 closeQuietly(response);
477 }
478 }
479
480 public static UriTemplate requireLinkUriTemplate(Links links, String rel, Class<?> entityClass) throws MpacException
481 {
482 for (UriTemplate ut: links.getUriTemplate(rel))
483 {
484 return ut;
485 }
486 throw new MpacException("Missing required API link \"" + rel + "\" from " + entityClass.getSimpleName());
487 }
488
489 public static <A extends Entity> URI getTemplatedLink(A a, String rel, String paramName, String paramValue) throws MpacException
490 {
491 UriTemplate t = requireLinkUriTemplate(a.getLinks(), rel, a.getClass());
492 return t.resolve(ImmutableMap.of(paramName, paramValue));
493 }
494 }