Package updates, added test server
[pithos-ms-client] / trunk / Pithos.Network / RestHttpClient.cs
1 #region\r
2 /* -----------------------------------------------------------------------\r
3  * <copyright file="RestClient.cs" company="GRNet">\r
4  * \r
5  * Copyright 2011-2012 GRNET S.A. All rights reserved.\r
6  *\r
7  * Redistribution and use in source and binary forms, with or\r
8  * without modification, are permitted provided that the following\r
9  * conditions are met:\r
10  *\r
11  *   1. Redistributions of source code must retain the above\r
12  *      copyright notice, this list of conditions and the following\r
13  *      disclaimer.\r
14  *\r
15  *   2. Redistributions in binary form must reproduce the above\r
16  *      copyright notice, this list of conditions and the following\r
17  *      disclaimer in the documentation and/or other materials\r
18  *      provided with the distribution.\r
19  *\r
20  *\r
21  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS\r
22  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\r
23  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
24  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR\r
25  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r
26  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r
27  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF\r
28  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED\r
29  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\r
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\r
31  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r
32  * POSSIBILITY OF SUCH DAMAGE.\r
33  *\r
34  * The views and conclusions contained in the software and\r
35  * documentation are those of the authors and should not be\r
36  * interpreted as representing official policies, either expressed\r
37  * or implied, of GRNET S.A.\r
38  * </copyright>\r
39  * -----------------------------------------------------------------------\r
40  */\r
41 #endregion\r
42 using System.Collections.Specialized;\r
43 using System.Diagnostics;\r
44 using System.Diagnostics.Contracts;\r
45 using System.IO;\r
46 using System.Net;\r
47 using System.Net.Http;\r
48 using System.Reflection;\r
49 using System.Runtime.Serialization;\r
50 using System.Threading;\r
51 using System.Threading.Tasks;\r
52 using log4net;\r
53 \r
54 \r
55 namespace Pithos.Network\r
56 {\r
57     using System;\r
58     using System.Collections.Generic;\r
59     using System.Linq;\r
60     using System.Text;\r
61 \r
62     /// <summary>\r
63     /// TODO: Update summary.\r
64     /// </summary>\r
65     public class RestHttpClient:HttpClient\r
66     {\r
67         private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);\r
68 \r
69         //public bool TimedOut { get; set; }\r
70 \r
71         //public HttpStatusCode StatusCode { get; private set; }\r
72 \r
73         public string StatusDescription { get; set; }\r
74 \r
75         public long? RangeFrom { get; set; }\r
76         public long? RangeTo { get; set; }\r
77 \r
78         public int Retries { get; set; }\r
79 \r
80         private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();\r
81         public Dictionary<string, string> Parameters\r
82         {\r
83             get\r
84             {\r
85                 Contract.Ensures(_parameters!=null);\r
86                 return _parameters;\r
87             }            \r
88         }\r
89 \r
90         public RestHttpClient(HttpMessageHandler handler)\r
91             :base(handler)\r
92         {}\r
93 \r
94         public RestHttpClient():base(new HttpClientHandler\r
95                                          {\r
96                                              AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,\r
97                                              UseCookies=true\r
98                                          })\r
99         {            \r
100 \r
101             //The maximum error response must be large because missing server hashes are return as a Conflivt (409) error response\r
102             //Any value above 2^21-1 will result in an empty response.\r
103             //-1 essentially ignores the maximum length\r
104             HttpWebRequest.DefaultMaximumErrorResponseLength = -1;               \r
105             //this.MaxResponseContentBufferSize = -1;\r
106         }\r
107 \r
108        \r
109         \r
110 \r
111 \r
112 \r
113         private readonly List<HttpStatusCode> _allowedStatusCodes=new List<HttpStatusCode>{HttpStatusCode.NotModified};        \r
114 \r
115         public List<HttpStatusCode> AllowedStatusCodes\r
116         {\r
117             get\r
118             {\r
119                 return _allowedStatusCodes;\r
120             }            \r
121         }\r
122 \r
123         public DateTime LastModified { get; private set; }\r
124 \r
125         private static string LogContent(WebResponse webResponse)\r
126         {\r
127             if (webResponse == null)\r
128                 throw new ArgumentNullException("webResponse");\r
129             Contract.EndContractBlock();\r
130 \r
131             //The response stream must be copied to avoid affecting other code by disposing of the \r
132             //original response stream.\r
133             var stream = webResponse.GetResponseStream();            \r
134             using(var memStream=new MemoryStream())\r
135             using (var reader = new StreamReader(memStream))\r
136             {\r
137                 stream.CopyTo(memStream);                \r
138                 string content = reader.ReadToEnd();\r
139 \r
140                 stream.Seek(0,SeekOrigin.Begin);\r
141                 return content;\r
142             }\r
143         }\r
144 \r
145         \r
146         public string GetStringWithRetry(Uri address, int retries = 0)\r
147         {\r
148             if (address == null)\r
149                 throw new ArgumentNullException("address");\r
150 \r
151             var actualRetries = (retries == 0) ? Retries : retries;\r
152             var task = GetAsync(address).WithRetries(Timeout,actualRetries)\r
153                 .ContinueWith(async r =>\r
154                 {\r
155                     var response = r.Result;\r
156                     if (response.StatusCode == HttpStatusCode.NoContent)\r
157                         return String.Empty;\r
158                     if (response.StatusCode == HttpStatusCode.NotFound)\r
159                         return null;\r
160                     return await response.Content.ReadAsStringAsync();\r
161                 }).Unwrap();\r
162 \r
163             var result = task.Result;\r
164             return result;\r
165         }\r
166 \r
167 \r
168 /*\r
169         public string DownloadStringWithRetry(string address,int retries=0)\r
170         {\r
171             \r
172             if (address == null)\r
173                 throw new ArgumentNullException("address");\r
174 \r
175             var actualAddress = GetActualAddress(address);\r
176 \r
177             TraceStart("GET",actualAddress);            \r
178             \r
179             var actualRetries = (retries == 0) ? Retries : retries;\r
180 \r
181             \r
182             var task = Retry(GetAsync(actualAddress)).ContinueWith(async () =>\r
183             {\r
184                 var response= await GetAsync(actualAddress);                \r
185 \r
186                 if (response.StatusCode== HttpStatusCode.NoContent)\r
187                     return String.Empty;\r
188                 return await response.Content.ReadAsStringAsync();\r
189 \r
190             }, actualRetries);\r
191 \r
192             try\r
193             {\r
194                     var result = task.Result;\r
195                 return result;\r
196 \r
197             }\r
198             catch (AggregateException exc)\r
199             {\r
200                 //If the task fails, propagate the original exception\r
201                 if (exc.InnerException!=null)\r
202                     throw exc.InnerException;\r
203                 throw;\r
204             }\r
205         }\r
206 */\r
207 \r
208        /* public void Head(string address,int retries=0)\r
209         {\r
210             AllowedStatusCodes.Add(HttpStatusCode.NotFound);\r
211             RetryWithoutContent(address, retries, "HEAD");            \r
212         }\r
213 \r
214         public void PutWithRetry(string address, int retries = 0, string contentType=null)\r
215         {\r
216             RetryWithoutContent(address, retries, "PUT",contentType);\r
217         }\r
218 \r
219         public void PostWithRetry(string address,string contentType)\r
220         {            \r
221             RetryWithoutContent(address, 0, "POST",contentType);\r
222         }\r
223 \r
224         public void DeleteWithRetry(string address,int retries=0)\r
225         {\r
226             RetryWithoutContent(address, retries, "DELETE");\r
227         }*/\r
228 \r
229       /*  public string GetHeaderValue(string headerName,bool optional=false)\r
230         {\r
231             if (this.ResponseHeaders==null)\r
232                 throw new InvalidOperationException("ResponseHeaders are null");\r
233             Contract.EndContractBlock();\r
234 \r
235             var values=this.ResponseHeaders.GetValues(headerName);\r
236             if (values != null)\r
237                 return values[0];\r
238 \r
239             if (optional)            \r
240                 return null;            \r
241             //A required header was not found\r
242             throw new WebException(String.Format("The {0}  header is missing", headerName));\r
243         }*/\r
244 \r
245         public void SetNonEmptyHeaderValue(string headerName, string value)\r
246         {\r
247             if (String.IsNullOrWhiteSpace(value))\r
248                 return;\r
249             DefaultRequestHeaders.Add(headerName,value);\r
250         }\r
251 \r
252      /*   private void RetryWithoutContent(string address, int retries, string method,string contentType=null)\r
253         {\r
254             if (address == null)\r
255                 throw new ArgumentNullException("address");\r
256 \r
257             var actualAddress = GetActualAddress(address);            \r
258             var actualRetries = (retries == 0) ? Retries : retries;\r
259 \r
260             var task = Retry(() =>\r
261             {\r
262                 var uriString = String.Join("/",BaseAddress ,actualAddress);\r
263                 var uri = new Uri(uriString);\r
264                 var request =  GetWebRequest(uri);\r
265                 if (contentType!=null)\r
266                 {\r
267                     request.ContentType = contentType;\r
268                     request.ContentLength = 0;\r
269                 }\r
270                 request.Method = method;\r
271                 if (ResponseHeaders!=null)\r
272                     ResponseHeaders.Clear();\r
273 \r
274                 TraceStart(method, uriString);\r
275                 if (method == "PUT")\r
276                     request.ContentLength = 0;\r
277 \r
278                 //Have to use try/finally instead of using here, because WebClient needs a valid WebResponse object\r
279                 //in order to return response headers\r
280                 var response = (HttpWebResponse)GetWebResponse(request);\r
281                 try\r
282                 {\r
283                     LastModified = response.LastModified;\r
284                     StatusCode = response.StatusCode;\r
285                     StatusDescription = response.StatusDescription;\r
286                 }\r
287                 finally\r
288                 {\r
289                     response.Close();\r
290                 }\r
291                 \r
292 \r
293                 return 0;\r
294             }, actualRetries);\r
295 \r
296             try\r
297             {\r
298                 task.Wait();\r
299             }\r
300             catch (AggregateException ex)\r
301             {\r
302                 var exc = ex.InnerException;\r
303                 if (exc is RetryException)\r
304                 {\r
305                     Log.ErrorFormat("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);\r
306                 }\r
307                 else\r
308                 {\r
309                     Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, exc);\r
310                 }\r
311                 throw exc;\r
312 \r
313             }\r
314             catch(Exception ex)\r
315             {\r
316                 Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, ex);\r
317                 throw;\r
318             }\r
319         }\r
320      */   \r
321         private static void TraceStart(string method, string actualAddress)\r
322         {\r
323             Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);\r
324         }\r
325 \r
326         private string GetActualAddress(string address)\r
327         {\r
328             if (Parameters.Count == 0)\r
329                 return address;\r
330             var addressBuilder=new StringBuilder(address);            \r
331 \r
332             bool isFirst = true;\r
333             foreach (var parameter in Parameters)\r
334             {\r
335                 if(isFirst)\r
336                     addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);\r
337                 else\r
338                     addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);\r
339                 isFirst = false;\r
340             }\r
341             return addressBuilder.ToString();\r
342         }\r
343 \r
344 \r
345       \r
346        /*public void AssertStatusOK(string message)\r
347         {\r
348             if (StatusCode >= HttpStatusCode.BadRequest)\r
349                 throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));\r
350         }*/\r
351 \r
352 \r
353         \r
354 /*\r
355         private Task<T> Retry2<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)\r
356         {\r
357             if (original==null)\r
358                 throw new ArgumentNullException("original");\r
359             Contract.EndContractBlock();\r
360 \r
361             if (tcs == null)\r
362                 tcs = new TaskCompletionSource<T>();\r
363             Task.Factory.StartNew(original).ContinueWith(_original =>\r
364                 {\r
365                     if (!_original.IsFaulted)\r
366                         tcs.SetFromTask(_original);\r
367                     else \r
368                     {\r
369                         var e = _original.Exception.InnerException;\r
370                         var we = (e as WebException);\r
371                         if (we==null)\r
372                             tcs.SetException(e);\r
373                         else\r
374                         {\r
375                             var statusCode = GetStatusCode(we);\r
376 \r
377                             //Return null for 404\r
378                             if (statusCode == HttpStatusCode.NotFound)\r
379                                 tcs.SetResult(default(T));\r
380                             //Retry for timeouts and service unavailable\r
381                             else if (we.Status == WebExceptionStatus.Timeout ||\r
382                                 (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))\r
383                             {\r
384                                 TimedOut = true;\r
385                                 if (retryCount == 0)\r
386                                 {                                    \r
387                                     Log.ErrorFormat("[ERROR] Timed out too many times. \n{0}\n",e);\r
388                                     tcs.SetException(new RetryException("Timed out too many times.", e));                                    \r
389                                 }\r
390                                 else\r
391                                 {\r
392                                     Log.ErrorFormat(\r
393                                         "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,\r
394                                         retryCount, e);\r
395                                     Retry(original, retryCount - 1, tcs);\r
396                                 }\r
397                             }\r
398                             else\r
399                                 tcs.SetException(e);\r
400                         }\r
401                     };\r
402                 });\r
403             return tcs.Task;\r
404         }\r
405 */\r
406 \r
407      /*   private HttpStatusCode GetStatusCode(WebException we)\r
408         {\r
409             if (we==null)\r
410                 throw new ArgumentNullException("we");\r
411             var statusCode = HttpStatusCode.RequestTimeout;\r
412             if (we.Response != null)\r
413             {\r
414                 statusCode = ((HttpWebResponse) we.Response).StatusCode;\r
415                 this.StatusCode = statusCode;\r
416             }\r
417             return statusCode;\r
418         }*/\r
419 \r
420         public UriBuilder GetAddressBuilder(string container, string objectName)\r
421         {\r
422             var builder = new UriBuilder(String.Join("/", BaseAddress, container, objectName));\r
423             return builder;\r
424         }\r
425 \r
426       /*  public Dictionary<string, string> GetMeta(string metaPrefix)\r
427         {\r
428             if (String.IsNullOrWhiteSpace(metaPrefix))\r
429                 throw new ArgumentNullException("metaPrefix");\r
430             Contract.EndContractBlock();\r
431 \r
432             var keys = ResponseHeaders.AllKeys.AsQueryable();\r
433             var dict = (from key in keys\r
434                         where key.StartsWith(metaPrefix)\r
435                         let name = key.Substring(metaPrefix.Length)\r
436                         select new { Name = name, Value = ResponseHeaders[key] })\r
437                         .ToDictionary(t => t.Name, t => t.Value);\r
438             return dict;\r
439         }\r
440 \r
441 \r
442         internal Task DownloadFileTaskAsync(Uri uri, string fileName, CancellationToken cancellationToken, IProgress<DownloadProgressChangedEventArgs> progress)\r
443         {\r
444             cancellationToken.Register(CancelAsync);\r
445             DownloadProgressChangedEventHandler onDownloadProgressChanged = (o, e) => progress.Report(e);\r
446             this.DownloadProgressChanged += onDownloadProgressChanged;\r
447             return this.DownloadFileTaskAsync(uri, fileName).ContinueWith(t=>\r
448                 {\r
449                     this.DownloadProgressChanged -= onDownloadProgressChanged;\r
450                 });\r
451         }\r
452 \r
453         internal Task<byte[]> DownloadDataTaskAsync(Uri uri, CancellationToken cancellationToken, IProgress<DownloadProgressChangedEventArgs> progress)\r
454         {\r
455             cancellationToken.Register(CancelAsync);\r
456             DownloadProgressChangedEventHandler onDownloadProgressChanged = (o, e) => progress.Report(e);\r
457             this.DownloadProgressChanged += onDownloadProgressChanged;\r
458             return this.DownloadDataTaskAsync(uri).ContinueWith(t =>\r
459             {\r
460                 this.DownloadProgressChanged -= onDownloadProgressChanged;\r
461                 return t.Result;\r
462             });\r
463         }\r
464 */    }\r
465 \r
466 }\r