Refactored to agents
[pithos-ms-client] / trunk / Pithos.Network / RestClient.cs
1 // -----------------------------------------------------------------------
2 // <copyright file="RestClient.cs" company="Microsoft">
3 // TODO: Update copyright text.
4 // </copyright>
5 // -----------------------------------------------------------------------
6
7 using System.Collections.Specialized;
8 using System.Diagnostics;
9 using System.Diagnostics.Contracts;
10 using System.IO;
11 using System.Net;
12 using System.Runtime.Serialization;
13 using System.Threading.Tasks;
14
15 namespace Pithos.Network
16 {
17     using System;
18     using System.Collections.Generic;
19     using System.Linq;
20     using System.Text;
21
22     /// <summary>
23     /// TODO: Update summary.
24     /// </summary>
25     public class RestClient:WebClient
26     {
27         public int Timeout { get; set; }
28
29         public bool TimedOut { get; set; }
30
31         public HttpStatusCode StatusCode { get; private set; }
32
33         public string StatusDescription { get; set; }
34
35
36         public int Retries { get; set; }
37
38         private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();
39         public Dictionary<string, string> Parameters
40         {
41             get { return _parameters; }            
42         }
43
44         public RestClient():base()
45         {
46             
47         }
48
49        
50         public RestClient(RestClient other)
51             : base()
52         {
53             CopyHeaders(other);
54             Timeout = other.Timeout;
55             Retries = other.Retries;
56             BaseAddress = other.BaseAddress;             
57
58             foreach (var parameter in other.Parameters)
59             {
60                 Parameters.Add(parameter.Key,parameter.Value);
61             }
62
63             this.Proxy = other.Proxy;
64         }
65
66         protected override WebRequest GetWebRequest(Uri address)
67         {
68             TimedOut = false;
69             var webRequest = base.GetWebRequest(address);
70             var request = webRequest as HttpWebRequest;
71             request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
72             if(Timeout>0)
73                 request.Timeout = Timeout;
74             return request; 
75         }
76
77         protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
78         {
79             var response = (HttpWebResponse) base.GetWebResponse(request, result);
80             StatusCode=response.StatusCode;
81             StatusDescription=response.StatusDescription;
82             return response;
83         }
84
85
86
87         protected override WebResponse GetWebResponse(WebRequest request)
88         {
89             try
90             {                
91                 var response = (HttpWebResponse)base.GetWebResponse(request);
92                 StatusCode = response.StatusCode;
93                 StatusDescription = response.StatusDescription;
94                 return response;
95             }
96             catch (WebException exc)
97             {
98                 if (exc.Response!=null && exc.Response.ContentLength > 0)
99                 {
100                     string content = GetContent(exc.Response);
101                     Trace.TraceError(content);
102                 }
103                 throw;
104             }
105         }
106
107         private static string GetContent(WebResponse webResponse)
108         {
109             string content;
110             using (var stream = webResponse.GetResponseStream())
111             using (var reader = new StreamReader(stream))
112             {
113                 content = reader.ReadToEnd();
114             }
115             return content;
116         }
117
118         public string DownloadStringWithRetry(string address,int retries=0)
119         {
120             if (address == null)
121                 throw new ArgumentNullException("address");
122
123             var actualAddress = GetActualAddress(address);
124
125             TraceStart("GET",actualAddress);            
126             
127             var actualRetries = (retries == 0) ? Retries : retries;
128             
129
130             
131             var task = Retry(() =>
132             {
133                 var uriString = String.Join("/", BaseAddress.TrimEnd('/'), actualAddress);                
134                 var content = base.DownloadString(uriString);
135
136                 if (StatusCode == HttpStatusCode.NoContent)
137                     return String.Empty;
138                 return content;
139
140             }, actualRetries);
141
142             var result = task.Result;
143             return result;
144         }
145
146         public void Head(string address,int retries=0)
147         {
148             RetryWithoutContent(address, retries, "HEAD");
149         }
150
151         public void PutWithRetry(string address, int retries = 0)
152         {
153             RetryWithoutContent(address, retries, "PUT");
154         }
155
156         public void DeleteWithRetry(string address,int retries=0)
157         {
158             RetryWithoutContent(address, retries, "DELETE");
159         }
160
161         public string GetHeaderValue(string headerName)
162         {
163             var values=this.ResponseHeaders.GetValues(headerName);
164             if (values == null)
165                 throw new WebException(String.Format("The {0}  header is missing", headerName));
166             else
167                 return values[0];
168         }
169
170         private void RetryWithoutContent(string address, int retries, string method)
171         {
172             if (address == null)
173                 throw new ArgumentNullException("address");
174
175             var actualAddress = GetActualAddress(address);            
176             var actualRetries = (retries == 0) ? Retries : retries;
177
178             var task = Retry(() =>
179             {
180                 var uriString = String.Join("/",BaseAddress ,actualAddress);
181                 var uri = new Uri(uriString);
182                 var request =  GetWebRequest(uri);
183                 request.Method = method;
184                 if (ResponseHeaders!=null)
185                     ResponseHeaders.Clear();
186
187                 TraceStart(method, uriString);
188
189                 var response = (HttpWebResponse)GetWebResponse(request);
190                 StatusCode = response.StatusCode;
191                 StatusDescription = response.StatusDescription;                
192                 
193
194                 return 0;
195             }, actualRetries);
196
197             task.Wait();
198         }
199         
200         /*private string RetryWithContent(string address, int retries, string method)
201         {
202             if (address == null)
203                 throw new ArgumentNullException("address");
204
205             var actualAddress = GetActualAddress(address);            
206             var actualRetries = (retries == 0) ? Retries : retries;
207
208             var task = Retry(() =>
209             {
210                 var uriString = String.Join("/",BaseAddress ,actualAddress);
211                 var uri = new Uri(uriString);
212                 
213                 var request =  GetWebRequest(uri);
214                 request.Method = method;                
215
216                 if (ResponseHeaders!=null)
217                     ResponseHeaders.Clear();
218
219                 TraceStart(method, uriString);
220
221                 var getResponse = request.GetResponseAsync();
222                 
223                 var setStatus= getResponse.ContinueWith(t =>
224                 {
225                     var response = (HttpWebResponse)t.Result;                    
226                     StatusCode = response.StatusCode;
227                     StatusDescription = response.StatusDescription;                
228                     return response;
229                 });
230
231                 var getData = setStatus.ContinueWith(t =>
232                 {
233                     var response = t.Result;
234                     return response.GetResponseStream()
235                         .ReadAllBytesAsync();
236                 }).Unwrap();
237
238                 var data = getData.Result;
239                 var content=Encoding.UTF8.GetString(data);
240
241 //                var response = (HttpWebResponse)GetWebResponse(request);
242                 
243                 
244 /*
245                 StatusCode = response.StatusCode;
246                 StatusDescription = response.StatusDescription;                
247 #1#
248                 
249
250                 return content;
251             }, actualRetries);
252
253             return task.Result;
254         }*/
255
256         private static void TraceStart(string method, string actualAddress)
257         {
258             Trace.WriteLine(String.Format("[{0}] {1} {2}", method, DateTime.Now, actualAddress));
259         }
260
261         private string GetActualAddress(string address)
262         {
263             if (Parameters.Count == 0)
264                 return address;
265             var addressBuilder=new StringBuilder(address);            
266
267             bool isFirst = true;
268             foreach (var parameter in Parameters)
269             {
270                 if(isFirst)
271                     addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
272                 else
273                     addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
274                 isFirst = false;
275             }
276             return addressBuilder.ToString();
277         }
278
279         public string DownloadStringWithRetry(Uri address,int retries=0)
280         {
281             if (address == null)
282                 throw new ArgumentNullException("address");
283
284             var actualRetries = (retries == 0) ? Retries : retries;            
285             var task = Retry(() =>
286             {
287                 var content = base.DownloadString(address);
288
289                 if (StatusCode == HttpStatusCode.NoContent)
290                     return String.Empty;
291                 return content;
292
293             }, actualRetries);
294
295             var result = task.Result;
296             return result;
297         }
298
299       
300         /// <summary>
301         /// Copies headers from another RestClient
302         /// </summary>
303         /// <param name="source">The RestClient from which the headers are copied</param>
304         public void CopyHeaders(RestClient source)
305         {
306             Contract.Requires(source != null, "source can't be null");
307             if (source == null)
308                 throw new ArgumentNullException("source", "source can't be null");
309             CopyHeaders(source.Headers,Headers);
310         }
311         
312         /// <summary>
313         /// Copies headers from one header collection to another
314         /// </summary>
315         /// <param name="source">The source collection from which the headers are copied</param>
316         /// <param name="target">The target collection to which the headers are copied</param>
317         public static void CopyHeaders(WebHeaderCollection source,WebHeaderCollection target)
318         {
319             Contract.Requires(source != null, "source can't be null");
320             Contract.Requires(target != null, "target can't be null");
321             if (source == null)
322                 throw new ArgumentNullException("source", "source can't be null");
323             if (target == null)
324                 throw new ArgumentNullException("target", "target can't be null");
325             for (int i = 0; i < source.Count; i++)
326             {
327                 target.Add(source.GetKey(i), source[i]);
328             }            
329         }
330
331         public void AssertStatusOK(string message)
332         {
333             if (StatusCode >= HttpStatusCode.BadRequest)
334                 throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
335         }
336
337
338         private Task<T> Retry<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)
339         {
340             if (tcs == null)
341                 tcs = new TaskCompletionSource<T>();
342             Task.Factory.StartNew(original).ContinueWith(_original =>
343                 {
344                     if (!_original.IsFaulted)
345                         tcs.SetFromTask(_original);
346                     else 
347                     {
348                         var e = _original.Exception.InnerException;
349                         var we = (e as WebException);
350                         if (we==null)
351                             tcs.SetException(e);
352                         else
353                         {
354                             var statusCode = GetStatusCode(we);
355
356                             //Return null for 404
357                             if (statusCode == HttpStatusCode.NotFound)
358                                 tcs.SetResult(default(T));
359                             //Retry for timeouts and service unavailable
360                             else if (we.Status == WebExceptionStatus.Timeout ||
361                                 (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))
362                             {
363                                 TimedOut = true;
364                                 if (retryCount == 0)
365                                 {
366                                     Trace.TraceError("[ERROR] Timed out too many times. {0}\n", e);
367                                     tcs.SetException(new RetryException("Timed out too many times.", e));                                    
368                                 }
369                                 else
370                                 {
371                                     Trace.TraceError(
372                                         "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
373                                         retryCount, e);
374                                     Retry(original, retryCount - 1, tcs);
375                                 }
376                             }
377                             else
378                                 tcs.SetException(e);
379                         }
380                     };
381                 });
382             return tcs.Task;
383         }
384
385         private HttpStatusCode GetStatusCode(WebException we)
386         {
387             var statusCode = HttpStatusCode.RequestTimeout;
388             if (we.Response != null)
389             {
390                 statusCode = ((HttpWebResponse) we.Response).StatusCode;
391                 this.StatusCode = statusCode;
392             }
393             return statusCode;
394         }
395     }
396
397     public class RetryException:Exception
398     {
399         public RetryException()
400             :base()
401         {
402             
403         }
404
405         public RetryException(string message)
406             :base(message)
407         {
408             
409         }
410
411         public RetryException(string message,Exception innerException)
412             :base(message,innerException)
413         {
414             
415         }
416
417         public RetryException(SerializationInfo info,StreamingContext context)
418             :base(info,context)
419         {
420             
421         }
422     }
423 }