View Javadoc

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   * Common methods used by implementations of the 2.0 API.
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                     // Can't parse it, we'll just return the body as a string. We don't want
312                     // to throw any exceptions out of this method, since that would hide the
313                     // server error that we're trying to return.
314                 }
315             }
316             return new MpacException.ServerError(status, body);
317         }
318         catch (Exception e)
319         {
320             // Error response body couldn't be read - ignore (see comment above).
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);  // note that this is different from the v1 parameter name
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 }