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