Statistics
| Branch: | Revision:

root / trunk / Pithos.Network / RestClient.cs @ c561991c

History | View | Annotate | Download (23.3 kB)

1
#region
2
/* -----------------------------------------------------------------------
3
 * <copyright file="RestClient.cs" company="GRNet">
4
 * 
5
 * Copyright 2011-2012 GRNET S.A. All rights reserved.
6
 *
7
 * Redistribution and use in source and binary forms, with or
8
 * without modification, are permitted provided that the following
9
 * conditions are met:
10
 *
11
 *   1. Redistributions of source code must retain the above
12
 *      copyright notice, this list of conditions and the following
13
 *      disclaimer.
14
 *
15
 *   2. Redistributions in binary form must reproduce the above
16
 *      copyright notice, this list of conditions and the following
17
 *      disclaimer in the documentation and/or other materials
18
 *      provided with the distribution.
19
 *
20
 *
21
 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
 * POSSIBILITY OF SUCH DAMAGE.
33
 *
34
 * The views and conclusions contained in the software and
35
 * documentation are those of the authors and should not be
36
 * interpreted as representing official policies, either expressed
37
 * or implied, of GRNET S.A.
38
 * </copyright>
39
 * -----------------------------------------------------------------------
40
 */
41
#endregion
42
using System.Collections.Specialized;
43
using System.Diagnostics;
44
using System.Diagnostics.Contracts;
45
using System.IO;
46
using System.Net;
47
using System.Reflection;
48
using System.Runtime.Serialization;
49
using System.Threading;
50
using System.Threading.Tasks;
51
using log4net;
52

    
53

    
54
namespace Pithos.Network
55
{
56
    using System;
57
    using System.Collections.Generic;
58
    using System.Linq;
59
    using System.Text;
60

    
61
    /// <summary>
62
    /// TODO: Update summary.
63
    /// </summary>
64
    public class RestClient:WebClient
65
    {
66
        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
67

    
68
        public int Timeout { get; set; }
69

    
70
        public bool TimedOut { get; set; }
71

    
72
        public HttpStatusCode StatusCode { get; private set; }
73

    
74
        public string StatusDescription { get; set; }
75

    
76
        public long? RangeFrom { get; set; }
77
        public long? RangeTo { get; set; }
78

    
79
        public int Retries { get; set; }
80

    
81
        private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();
82
        public Dictionary<string, string> Parameters
83
        {
84
            get
85
            {
86
                Contract.Ensures(_parameters!=null);
87
                return _parameters;
88
            }            
89
        }
90

    
91

    
92
        [ContractInvariantMethod]
93
        private void Invariants()
94
        {
95
            Contract.Invariant(Headers!=null);    
96
        }
97

    
98
        public RestClient():base()
99
        {
100
            //The maximum error response must be large because missing server hashes are return as a Conflivt (409) error response
101
            //Any value above 2^21-1 will result in an empty response.
102
            //-1 essentially ignores the maximum length
103
            HttpWebRequest.DefaultMaximumErrorResponseLength = -1;
104
        }
105

    
106
       
107
        public RestClient(RestClient other)
108
            : base()
109
        {
110
            if (other==null)
111
                //Log.ErrorFormat("[ERROR] No parameters provided to the rest client. \n{0}\n", other);
112
                throw new ArgumentNullException("other");
113
            Contract.EndContractBlock();
114

    
115
            //The maximum error response must be large because missing server hashes are return as a Conflivt (409) error response
116
            //Any value above 2^21-1 will result in an empty response.
117
            //-1 essentially ignores the maximum length
118
            HttpWebRequest.DefaultMaximumErrorResponseLength = -1;
119
            
120

    
121
            CopyHeaders(other);
122
            Timeout = other.Timeout;
123
            Retries = other.Retries;
124
            BaseAddress = other.BaseAddress;             
125

    
126
            foreach (var parameter in other.Parameters)
127
            {
128
                Parameters.Add(parameter.Key,parameter.Value);
129
            }
130

    
131
            this.Proxy = other.Proxy;
132
        }
133

    
134

    
135
        protected override WebRequest GetWebRequest(Uri address)
136
        {
137
            TimedOut = false;
138
            var webRequest = base.GetWebRequest(address);            
139
            var request = (HttpWebRequest)webRequest;
140
            request.CookieContainer=new CookieContainer();
141
            request.ServicePoint.ConnectionLimit = 50;
142
            if (IfModifiedSince.HasValue)
143
                request.IfModifiedSince = IfModifiedSince.Value;
144
            request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
145
            if(Timeout>0)
146
                request.Timeout = Timeout;
147

    
148
            if (RangeFrom.HasValue)
149
            {
150
                if (RangeTo.HasValue)
151
                    request.AddRange(RangeFrom.Value, RangeTo.Value);
152
                else
153
                    request.AddRange(RangeFrom.Value);
154
            }
155
            return request; 
156
        }
157

    
158
        public DateTime? IfModifiedSince { get; set; }
159

    
160
        //Asynchronous version
161
        protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
162
        {            
163
            Log.InfoFormat("[{0}] {1}", request.Method, request.RequestUri); 
164
            HttpWebResponse response = null;
165

    
166
            try
167
            {
168
                response = (HttpWebResponse)base.GetWebResponse(request, result);
169
            }
170
            catch (WebException exc)
171
            {
172
                if (!TryGetResponse(exc, request,out response))
173
                    throw;
174
            }
175

    
176
            StatusCode = response.StatusCode;
177
            LastModified = response.LastModified;
178
            StatusDescription = response.StatusDescription;
179
            return response;
180

    
181
        }
182
      
183

    
184
        //Synchronous version
185
        protected override WebResponse GetWebResponse(WebRequest request)
186
        {
187
            HttpWebResponse response = null;
188
            try
189
            {           
190
                Log.InfoFormat("[{0}] {1}",request.Method,request.RequestUri);     
191
                response = (HttpWebResponse)base.GetWebResponse(request);
192
            }
193
            catch (WebException exc)
194
            {
195
                if (!TryGetResponse(exc, request,out response))
196
                    throw;
197
            }
198

    
199
            StatusCode = response.StatusCode;
200
            LastModified = response.LastModified;
201
            StatusDescription = response.StatusDescription;
202
            return response;
203
        }
204

    
205
        private bool TryGetResponse(WebException exc, WebRequest request,out HttpWebResponse response)
206
        {
207
            response = null;
208
            //Fail on empty response
209
            if (exc.Response == null)
210
            {
211
                Log.WarnFormat("[{0}] {1} {2}", request.Method, exc.Status, request.RequestUri);     
212
                return false;
213
            }
214

    
215
            response = (exc.Response as HttpWebResponse);
216
            var statusCode = (int)response.StatusCode;
217
            //Succeed on allowed status codes
218
            if (AllowedStatusCodes.Contains(response.StatusCode))
219
            {
220
                if (Log.IsDebugEnabled)
221
                    Log.DebugFormat("[{0}] {1} {2}", request.Method, statusCode, request.RequestUri);     
222
                return true;
223
            }
224
            
225
            Log.WarnFormat("[{0}] {1} {2}", request.Method, statusCode, request.RequestUri);
226

    
227
            //Does the response have any content to log?
228
            if (exc.Response.ContentLength > 0)
229
            {
230
                var content = LogContent(exc.Response);
231
                Log.ErrorFormat(content);
232
            }
233
            return false;
234
        }
235

    
236
        private readonly List<HttpStatusCode> _allowedStatusCodes=new List<HttpStatusCode>{HttpStatusCode.NotModified};        
237

    
238
        public List<HttpStatusCode> AllowedStatusCodes
239
        {
240
            get
241
            {
242
                return _allowedStatusCodes;
243
            }            
244
        }
245

    
246
        public DateTime LastModified { get; private set; }
247

    
248
        private static string LogContent(WebResponse webResponse)
249
        {
250
            if (webResponse == null)
251
                throw new ArgumentNullException("webResponse");
252
            Contract.EndContractBlock();
253

    
254
            //The response stream must be copied to avoid affecting other code by disposing of the 
255
            //original response stream.
256
            var stream = webResponse.GetResponseStream();            
257
            using(var memStream=new MemoryStream())
258
            using (var reader = new StreamReader(memStream))
259
            {
260
                stream.CopyTo(memStream);                
261
                string content = reader.ReadToEnd();
262

    
263
                stream.Seek(0,SeekOrigin.Begin);
264
                return content;
265
            }
266
        }
267

    
268
        public string DownloadStringWithRetry(string address,int retries=0)
269
        {
270
            
271
            if (address == null)
272
                throw new ArgumentNullException("address");
273

    
274
            var actualAddress = GetActualAddress(address);
275

    
276
            TraceStart("GET",actualAddress);            
277
            
278
            var actualRetries = (retries == 0) ? Retries : retries;
279

    
280
            var uriString = String.Join("/", BaseAddress.TrimEnd('/'), actualAddress);
281

    
282
            var task = Retry(() =>
283
            {                                
284
                var content = DownloadString(uriString);
285

    
286
                if (StatusCode == HttpStatusCode.NoContent)
287
                    return String.Empty;
288
                return content;
289

    
290
            }, actualRetries);
291

    
292
            try
293
            {
294
                    var result = task.Result;
295
                return result;
296

    
297
            }
298
            catch (AggregateException exc)
299
            {
300
                //If the task fails, propagate the original exception
301
                if (exc.InnerException!=null)
302
                    throw exc.InnerException;
303
                throw;
304
            }
305
        }
306

    
307
        public void Head(string address,int retries=0)
308
        {
309
            AllowedStatusCodes.Add(HttpStatusCode.NotFound);
310
            RetryWithoutContent(address, retries, "HEAD");
311
        }
312

    
313
        public void PutWithRetry(string address, int retries = 0, string contentType=null)
314
        {
315
            RetryWithoutContent(address, retries, "PUT",contentType);
316
        }
317

    
318
        public void PostWithRetry(string address,string contentType)
319
        {            
320
            RetryWithoutContent(address, 0, "POST",contentType);
321
        }
322

    
323
        public void DeleteWithRetry(string address,int retries=0)
324
        {
325
            RetryWithoutContent(address, retries, "DELETE");
326
        }
327

    
328
        public string GetHeaderValue(string headerName,bool optional=false)
329
        {
330
            if (this.ResponseHeaders==null)
331
                throw new InvalidOperationException("ResponseHeaders are null");
332
            Contract.EndContractBlock();
333

    
334
            var values=this.ResponseHeaders.GetValues(headerName);
335
            if (values != null)
336
                return values[0];
337

    
338
            if (optional)            
339
                return null;            
340
            //A required header was not found
341
            throw new WebException(String.Format("The {0}  header is missing", headerName));
342
        }
343

    
344
        public void SetNonEmptyHeaderValue(string headerName, string value)
345
        {
346
            if (String.IsNullOrWhiteSpace(value))
347
                return;
348
            Headers.Add(headerName,value);
349
        }
350

    
351
        private void RetryWithoutContent(string address, int retries, string method,string contentType=null)
352
        {
353
            if (address == null)
354
                throw new ArgumentNullException("address");
355

    
356
            var actualAddress = GetActualAddress(address);            
357
            var actualRetries = (retries == 0) ? Retries : retries;
358

    
359
            var task = Retry(() =>
360
            {
361
                var uriString = String.Join("/",BaseAddress ,actualAddress);
362
                var uri = new Uri(uriString);
363
                var request =  GetWebRequest(uri);
364
                if (contentType!=null)
365
                {
366
                    request.ContentType = contentType;
367
                    request.ContentLength = 0;
368
                }
369
                request.Method = method;
370
                if (ResponseHeaders!=null)
371
                    ResponseHeaders.Clear();
372

    
373
                TraceStart(method, uriString);
374
                if (method == "PUT")
375
                    request.ContentLength = 0;
376

    
377
                //Have to use try/finally instead of using here, because WebClient needs a valid WebResponse object
378
                //in order to return response headers
379
                var response = (HttpWebResponse)GetWebResponse(request);
380
                try
381
                {
382
                    LastModified = response.LastModified;
383
                    StatusCode = response.StatusCode;
384
                    StatusDescription = response.StatusDescription;
385
                }
386
                finally
387
                {
388
                    response.Close();
389
                }
390
                
391

    
392
                return 0;
393
            }, actualRetries);
394

    
395
            try
396
            {
397
                task.Wait();
398
            }
399
            catch (AggregateException ex)
400
            {
401
                var exc = ex.InnerException;
402
                if (exc is RetryException)
403
                {
404
                    Log.ErrorFormat("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);
405
                }
406
                else
407
                {
408
                    Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, exc);
409
                }
410
                throw exc;
411

    
412
            }
413
            catch(Exception ex)
414
            {
415
                Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, ex);
416
                throw;
417
            }
418
        }
419
        
420
        private static void TraceStart(string method, string actualAddress)
421
        {
422
            Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
423
        }
424

    
425
        private string GetActualAddress(string address)
426
        {
427
            if (Parameters.Count == 0)
428
                return address;
429
            var addressBuilder=new StringBuilder(address);            
430

    
431
            bool isFirst = true;
432
            foreach (var parameter in Parameters)
433
            {
434
                if(isFirst)
435
                    addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
436
                else
437
                    addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
438
                isFirst = false;
439
            }
440
            return addressBuilder.ToString();
441
        }
442

    
443
        public string DownloadStringWithRetry(Uri address,int retries=0)
444
        {
445
            if (address == null)
446
                throw new ArgumentNullException("address");
447

    
448
            var actualRetries = (retries == 0) ? Retries : retries;            
449
            var task = Retry(() =>
450
            {
451
                var content = base.DownloadString(address);
452

    
453
                if (StatusCode == HttpStatusCode.NoContent)
454
                    return String.Empty;
455
                return content;
456

    
457
            }, actualRetries);
458

    
459
            var result = task.Result;
460
            return result;
461
        }
462

    
463
      
464
        /// <summary>
465
        /// Copies headers from another RestClient
466
        /// </summary>
467
        /// <param name="source">The RestClient from which the headers are copied</param>
468
        public void CopyHeaders(RestClient source)
469
        {
470
            if (source == null)
471
                throw new ArgumentNullException("source", "source can't be null");
472
            Contract.EndContractBlock();
473
            //The Headers getter initializes the property, it is never null
474
            Contract.Assume(Headers!=null);
475
                
476
            CopyHeaders(source.Headers,Headers);
477
        }
478
        
479
        /// <summary>
480
        /// Copies headers from one header collection to another
481
        /// </summary>
482
        /// <param name="source">The source collection from which the headers are copied</param>
483
        /// <param name="target">The target collection to which the headers are copied</param>
484
        public static void CopyHeaders(WebHeaderCollection source,WebHeaderCollection target)
485
        {
486
            if (source == null)
487
                throw new ArgumentNullException("source", "source can't be null");
488
            if (target == null)
489
                throw new ArgumentNullException("target", "target can't be null");
490
            Contract.EndContractBlock();
491

    
492
            for (int i = 0; i < source.Count; i++)
493
            {
494
                target.Add(source.GetKey(i), source[i]);
495
            }            
496
        }
497

    
498
        public void AssertStatusOK(string message)
499
        {
500
            if (StatusCode >= HttpStatusCode.BadRequest)
501
                throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
502
        }
503

    
504

    
505
        private Task<T> Retry<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)
506
        {
507
            if (original==null)
508
                throw new ArgumentNullException("original");
509
            Contract.EndContractBlock();
510

    
511
            if (tcs == null)
512
                tcs = new TaskCompletionSource<T>();
513
            Task.Factory.StartNew(original).ContinueWith(_original =>
514
                {
515
                    if (!_original.IsFaulted)
516
                        tcs.SetFromTask(_original);
517
                    else 
518
                    {
519
                        var e = _original.Exception.InnerException;
520
                        var we = (e as WebException);
521
                        if (we==null)
522
                            tcs.SetException(e);
523
                        else
524
                        {
525
                            var statusCode = GetStatusCode(we);
526

    
527
                            //Return null for 404
528
                            if (statusCode == HttpStatusCode.NotFound)
529
                                tcs.SetResult(default(T));
530
                            //Retry for timeouts and service unavailable
531
                            else if (we.Status == WebExceptionStatus.Timeout ||
532
                                (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))
533
                            {
534
                                TimedOut = true;
535
                                if (retryCount == 0)
536
                                {                                    
537
                                    Log.ErrorFormat("[ERROR] Timed out too many times. \n{0}\n",e);
538
                                    tcs.SetException(new RetryException("Timed out too many times.", e));                                    
539
                                }
540
                                else
541
                                {
542
                                    Log.ErrorFormat(
543
                                        "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
544
                                        retryCount, e);
545
                                    Retry(original, retryCount - 1, tcs);
546
                                }
547
                            }
548
                            else
549
                                tcs.SetException(e);
550
                        }
551
                    };
552
                });
553
            return tcs.Task;
554
        }
555

    
556
        private HttpStatusCode GetStatusCode(WebException we)
557
        {
558
            if (we==null)
559
                throw new ArgumentNullException("we");
560
            var statusCode = HttpStatusCode.RequestTimeout;
561
            if (we.Response != null)
562
            {
563
                statusCode = ((HttpWebResponse) we.Response).StatusCode;
564
                this.StatusCode = statusCode;
565
            }
566
            return statusCode;
567
        }
568

    
569
        public UriBuilder GetAddressBuilder(string container, string objectName)
570
        {
571
            var builder = new UriBuilder(String.Join("/", BaseAddress, container, objectName));
572
            return builder;
573
        }
574

    
575
        public Dictionary<string, string> GetMeta(string metaPrefix)
576
        {
577
            if (String.IsNullOrWhiteSpace(metaPrefix))
578
                throw new ArgumentNullException("metaPrefix");
579
            Contract.EndContractBlock();
580

    
581
            var keys = ResponseHeaders.AllKeys.AsQueryable();
582
            var dict = (from key in keys
583
                        where key.StartsWith(metaPrefix)
584
                        let name = key.Substring(metaPrefix.Length)
585
                        select new { Name = name, Value = ResponseHeaders[key] })
586
                        .ToDictionary(t => t.Name, t => t.Value);
587
            return dict;
588
        }
589

    
590

    
591
        internal Task DownloadFileTaskAsync(Uri uri, string fileName, CancellationToken cancellationToken, IProgress<DownloadProgressChangedEventArgs> progress)
592
        {
593
            cancellationToken.Register(CancelAsync);
594
            DownloadProgressChangedEventHandler onDownloadProgressChanged = (o, e) => progress.Report(e);
595
            this.DownloadProgressChanged += onDownloadProgressChanged;
596
            return this.DownloadFileTaskAsync(uri, fileName).ContinueWith(t=>
597
                {
598
                    this.DownloadProgressChanged -= onDownloadProgressChanged;
599
                });
600
        }
601

    
602
        internal Task<byte[]> DownloadDataTaskAsync(Uri uri, CancellationToken cancellationToken, IProgress<DownloadProgressChangedEventArgs> progress)
603
        {
604
            cancellationToken.Register(CancelAsync);
605
            DownloadProgressChangedEventHandler onDownloadProgressChanged = (o, e) => progress.Report(e);
606
            this.DownloadProgressChanged += onDownloadProgressChanged;
607
            return this.DownloadDataTaskAsync(uri).ContinueWith(t =>
608
            {
609
                this.DownloadProgressChanged -= onDownloadProgressChanged;
610
                return t.Result;
611
            });
612
        }
613
    }
614

    
615
    public class RetryException:Exception
616
    {
617
        public RetryException()
618
            :base()
619
        {
620
            
621
        }
622

    
623
        public RetryException(string message)
624
            :base(message)
625
        {
626
            
627
        }
628

    
629
        public RetryException(string message,Exception innerException)
630
            :base(message,innerException)
631
        {
632
            
633
        }
634

    
635
        public RetryException(SerializationInfo info,StreamingContext context)
636
            :base(info,context)
637
        {
638
            
639
        }
640
    }
641
}