-#region
-/* -----------------------------------------------------------------------
- * <copyright file="RestClient.cs" company="GRNet">
- *
- * Copyright 2011-2012 GRNET S.A. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * 1. Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the following
- * disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following
- * disclaimer in the documentation and/or other materials
- * provided with the distribution.
- *
- *
- * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
- * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
- * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
- * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
- * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- * The views and conclusions contained in the software and
- * documentation are those of the authors and should not be
- * interpreted as representing official policies, either expressed
- * or implied, of GRNET S.A.
- * </copyright>
- * -----------------------------------------------------------------------
- */
-#endregion
-using System.Collections.Specialized;
-using System.Diagnostics;
-using System.Diagnostics.Contracts;
-using System.IO;
-using System.Net;
-using System.Reflection;
-using System.Runtime.Serialization;
-using System.Threading.Tasks;
-using log4net;
-
-
-namespace Pithos.Network
-{
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
-
- /// <summary>
- /// TODO: Update summary.
- /// </summary>
- public class RestClient:WebClient
- {
- private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
-
- public int Timeout { get; set; }
-
- public bool TimedOut { get; set; }
-
- public HttpStatusCode StatusCode { get; private set; }
-
- public string StatusDescription { get; set; }
-
- public long? RangeFrom { get; set; }
- public long? RangeTo { get; set; }
-
- public int Retries { get; set; }
-
- private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();
- public Dictionary<string, string> Parameters
- {
- get
- {
- Contract.Ensures(_parameters!=null);
- return _parameters;
- }
- }
-
-
- [ContractInvariantMethod]
- private void Invariants()
- {
- Contract.Invariant(Headers!=null);
- }
-
- public RestClient():base()
- {
- //The maximum error response must be large because missing server hashes are return as a Conflivt (409) error response
- //Any value above 2^21-1 will result in an empty response.
- //-1 essentially ignores the maximum length
- HttpWebRequest.DefaultMaximumErrorResponseLength = -1;
- }
-
-
- public RestClient(RestClient other)
- : base()
- {
- if (other==null)
- throw new ArgumentNullException("other");
- Contract.EndContractBlock();
-
- //The maximum error response must be large because missing server hashes are return as a Conflivt (409) error response
- //Any value above 2^21-1 will result in an empty response.
- //-1 essentially ignores the maximum length
- HttpWebRequest.DefaultMaximumErrorResponseLength = -1;
-
- CopyHeaders(other);
- Timeout = other.Timeout;
- Retries = other.Retries;
- BaseAddress = other.BaseAddress;
-
- foreach (var parameter in other.Parameters)
- {
- Parameters.Add(parameter.Key,parameter.Value);
- }
-
- this.Proxy = other.Proxy;
- }
-
-
- protected override WebRequest GetWebRequest(Uri address)
- {
- TimedOut = false;
- var webRequest = base.GetWebRequest(address);
- var request = (HttpWebRequest)webRequest;
- request.ServicePoint.ConnectionLimit = 50;
- if (IfModifiedSince.HasValue)
- request.IfModifiedSince = IfModifiedSince.Value;
- request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
- if(Timeout>0)
- request.Timeout = Timeout;
-
- if (RangeFrom.HasValue)
- {
- if (RangeTo.HasValue)
- request.AddRange(RangeFrom.Value, RangeTo.Value);
- else
- request.AddRange(RangeFrom.Value);
- }
- return request;
- }
-
- public DateTime? IfModifiedSince { get; set; }
-
- //Asynchronous version
- protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
- {
- Log.InfoFormat("[{0}] {1}", request.Method, request.RequestUri);
- HttpWebResponse response = null;
-
- try
- {
- response = (HttpWebResponse)base.GetWebResponse(request, result);
- }
- catch (WebException exc)
- {
- if (!TryGetResponse(exc, request,out response))
- throw;
- }
-
- StatusCode = response.StatusCode;
- LastModified = response.LastModified;
- StatusDescription = response.StatusDescription;
- return response;
-
- }
-
-
- //Synchronous version
- protected override WebResponse GetWebResponse(WebRequest request)
- {
- HttpWebResponse response = null;
- try
- {
- Log.InfoFormat("[{0}] {1}",request.Method,request.RequestUri);
- response = (HttpWebResponse)base.GetWebResponse(request);
- }
- catch (WebException exc)
- {
- if (!TryGetResponse(exc, request,out response))
- throw;
- }
-
- StatusCode = response.StatusCode;
- LastModified = response.LastModified;
- StatusDescription = response.StatusDescription;
- return response;
- }
-
- private bool TryGetResponse(WebException exc, WebRequest request,out HttpWebResponse response)
- {
- response = null;
- //Fail on empty response
- if (exc.Response == null)
- {
- Log.WarnFormat("[{0}] {1} {2}", request.Method, exc.Status, request.RequestUri);
- return false;
- }
-
- response = (exc.Response as HttpWebResponse);
- var statusCode = (int)response.StatusCode;
- //Succeed on allowed status codes
- if (AllowedStatusCodes.Contains(response.StatusCode))
- {
- if (Log.IsDebugEnabled)
- Log.DebugFormat("[{0}] {1} {2}", request.Method, statusCode, request.RequestUri);
- return true;
- }
-
- Log.WarnFormat("[{0}] {1} {2}", request.Method, statusCode, request.RequestUri);
-
- //Does the response have any content to log?
- if (exc.Response.ContentLength > 0)
- {
- var content = LogContent(exc.Response);
- Log.ErrorFormat(content);
- }
- return false;
- }
-
- private readonly List<HttpStatusCode> _allowedStatusCodes=new List<HttpStatusCode>{HttpStatusCode.NotModified};
-
- public List<HttpStatusCode> AllowedStatusCodes
- {
- get
- {
- return _allowedStatusCodes;
- }
- }
-
- public DateTime LastModified { get; private set; }
-
- private static string LogContent(WebResponse webResponse)
- {
- if (webResponse == null)
- throw new ArgumentNullException("webResponse");
- Contract.EndContractBlock();
-
- //The response stream must be copied to avoid affecting other code by disposing of the
- //original response stream.
- var stream = webResponse.GetResponseStream();
- using(var memStream=new MemoryStream())
- using (var reader = new StreamReader(memStream))
- {
- stream.CopyTo(memStream);
- string content = reader.ReadToEnd();
-
- stream.Seek(0,SeekOrigin.Begin);
- return content;
- }
- }
-
- public string DownloadStringWithRetry(string address,int retries=0)
- {
-
- if (address == null)
- throw new ArgumentNullException("address");
-
- var actualAddress = GetActualAddress(address);
-
- TraceStart("GET",actualAddress);
-
- var actualRetries = (retries == 0) ? Retries : retries;
-
- var uriString = String.Join("/", BaseAddress.TrimEnd('/'), actualAddress);
-
- var task = Retry(() =>
- {
- var content = base.DownloadString(uriString);
-
- if (StatusCode == HttpStatusCode.NoContent)
- return String.Empty;
- return content;
-
- }, actualRetries);
-
- try
- {
- var result = task.Result;
- return result;
-
- }
- catch (AggregateException exc)
- {
- //If the task fails, propagate the original exception
- if (exc.InnerException!=null)
- throw exc.InnerException;
- throw;
- }
- }
-
- public void Head(string address,int retries=0)
- {
- AllowedStatusCodes.Add(HttpStatusCode.NotFound);
- RetryWithoutContent(address, retries, "HEAD");
- }
-
- public void PutWithRetry(string address, int retries = 0)
- {
- RetryWithoutContent(address, retries, "PUT");
- }
-
- public void DeleteWithRetry(string address,int retries=0)
- {
- RetryWithoutContent(address, retries, "DELETE");
- }
-
- public string GetHeaderValue(string headerName,bool optional=false)
- {
- if (this.ResponseHeaders==null)
- throw new InvalidOperationException("ResponseHeaders are null");
- Contract.EndContractBlock();
-
- var values=this.ResponseHeaders.GetValues(headerName);
- if (values != null)
- return values[0];
-
- if (optional)
- return null;
- //A required header was not found
- throw new WebException(String.Format("The {0} header is missing", headerName));
- }
-
- public void SetNonEmptyHeaderValue(string headerName, string value)
- {
- if (String.IsNullOrWhiteSpace(value))
- return;
- Headers.Add(headerName,value);
- }
-
- private void RetryWithoutContent(string address, int retries, string method)
- {
- if (address == null)
- throw new ArgumentNullException("address");
-
- var actualAddress = GetActualAddress(address);
- var actualRetries = (retries == 0) ? Retries : retries;
-
- var task = Retry(() =>
- {
- var uriString = String.Join("/",BaseAddress ,actualAddress);
- var uri = new Uri(uriString);
- var request = GetWebRequest(uri);
- request.Method = method;
- if (ResponseHeaders!=null)
- ResponseHeaders.Clear();
-
- TraceStart(method, uriString);
- if (method == "PUT")
- request.ContentLength = 0;
-
- //Have to use try/finally instead of using here, because WebClient needs a valid WebResponse object
- //in order to return response headers
- var response = (HttpWebResponse)GetWebResponse(request);
- try
- {
- LastModified = response.LastModified;
- StatusCode = response.StatusCode;
- StatusDescription = response.StatusDescription;
- }
- finally
- {
- response.Close();
- }
-
-
- return 0;
- }, actualRetries);
-
- try
- {
- task.Wait();
- }
- catch (AggregateException ex)
- {
- var exc = ex.InnerException;
- if (exc is RetryException)
- {
- Log.ErrorFormat("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);
- }
- else
- {
- Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, exc);
- }
- throw exc;
-
- }
- catch(Exception ex)
- {
- Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, ex);
- throw;
- }
- }
-
- private static void TraceStart(string method, string actualAddress)
- {
- Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
- }
-
- private string GetActualAddress(string address)
- {
- if (Parameters.Count == 0)
- return address;
- var addressBuilder=new StringBuilder(address);
-
- bool isFirst = true;
- foreach (var parameter in Parameters)
- {
- if(isFirst)
- addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
- else
- addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
- isFirst = false;
- }
- return addressBuilder.ToString();
- }
-
- public string DownloadStringWithRetry(Uri address,int retries=0)
- {
- if (address == null)
- throw new ArgumentNullException("address");
-
- var actualRetries = (retries == 0) ? Retries : retries;
- var task = Retry(() =>
- {
- var content = base.DownloadString(address);
-
- if (StatusCode == HttpStatusCode.NoContent)
- return String.Empty;
- return content;
-
- }, actualRetries);
-
- var result = task.Result;
- return result;
- }
-
-
- /// <summary>
- /// Copies headers from another RestClient
- /// </summary>
- /// <param name="source">The RestClient from which the headers are copied</param>
- public void CopyHeaders(RestClient source)
- {
- if (source == null)
- throw new ArgumentNullException("source", "source can't be null");
- Contract.EndContractBlock();
- //The Headers getter initializes the property, it is never null
- Contract.Assume(Headers!=null);
-
- CopyHeaders(source.Headers,Headers);
- }
-
- /// <summary>
- /// Copies headers from one header collection to another
- /// </summary>
- /// <param name="source">The source collection from which the headers are copied</param>
- /// <param name="target">The target collection to which the headers are copied</param>
- public static void CopyHeaders(WebHeaderCollection source,WebHeaderCollection target)
- {
- if (source == null)
- throw new ArgumentNullException("source", "source can't be null");
- if (target == null)
- throw new ArgumentNullException("target", "target can't be null");
- Contract.EndContractBlock();
-
- for (int i = 0; i < source.Count; i++)
- {
- target.Add(source.GetKey(i), source[i]);
- }
- }
-
- public void AssertStatusOK(string message)
- {
- if (StatusCode >= HttpStatusCode.BadRequest)
- throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
- }
-
-
- private Task<T> Retry<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)
- {
- if (original==null)
- throw new ArgumentNullException("original");
- Contract.EndContractBlock();
-
- if (tcs == null)
- tcs = new TaskCompletionSource<T>();
- Task.Factory.StartNew(original).ContinueWith(_original =>
- {
- if (!_original.IsFaulted)
- tcs.SetFromTask(_original);
- else
- {
- var e = _original.Exception.InnerException;
- var we = (e as WebException);
- if (we==null)
- tcs.SetException(e);
- else
- {
- var statusCode = GetStatusCode(we);
-
- //Return null for 404
- if (statusCode == HttpStatusCode.NotFound)
- tcs.SetResult(default(T));
- //Retry for timeouts and service unavailable
- else if (we.Status == WebExceptionStatus.Timeout ||
- (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))
- {
- TimedOut = true;
- if (retryCount == 0)
- {
- Log.ErrorFormat("[ERROR] Timed out too many times. \n{0}\n",e);
- tcs.SetException(new RetryException("Timed out too many times.", e));
- }
- else
- {
- Log.ErrorFormat(
- "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
- retryCount, e);
- Retry(original, retryCount - 1, tcs);
- }
- }
- else
- tcs.SetException(e);
- }
- };
- });
- return tcs.Task;
- }
-
- private HttpStatusCode GetStatusCode(WebException we)
- {
- if (we==null)
- throw new ArgumentNullException("we");
- var statusCode = HttpStatusCode.RequestTimeout;
- if (we.Response != null)
- {
- statusCode = ((HttpWebResponse) we.Response).StatusCode;
- this.StatusCode = statusCode;
- }
- return statusCode;
- }
-
- public UriBuilder GetAddressBuilder(string container, string objectName)
- {
- var builder = new UriBuilder(String.Join("/", BaseAddress, container, objectName));
- return builder;
- }
-
- public Dictionary<string, string> GetMeta(string metaPrefix)
- {
- if (String.IsNullOrWhiteSpace(metaPrefix))
- throw new ArgumentNullException("metaPrefix");
- Contract.EndContractBlock();
-
- var keys = ResponseHeaders.AllKeys.AsQueryable();
- var dict = (from key in keys
- where key.StartsWith(metaPrefix)
- let name = key.Substring(metaPrefix.Length)
- select new { Name = name, Value = ResponseHeaders[key] })
- .ToDictionary(t => t.Name, t => t.Value);
- return dict;
- }
- }
-
- public class RetryException:Exception
- {
- public RetryException()
- :base()
- {
-
- }
-
- public RetryException(string message)
- :base(message)
- {
-
- }
-
- public RetryException(string message,Exception innerException)
- :base(message,innerException)
- {
-
- }
-
- public RetryException(SerializationInfo info,StreamingContext context)
- :base(info,context)
- {
-
- }
- }
-}
+#region\r
+/* -----------------------------------------------------------------------\r
+ * <copyright file="RestClient.cs" company="GRNet">\r
+ * \r
+ * Copyright 2011-2012 GRNET S.A. All rights reserved.\r
+ *\r
+ * Redistribution and use in source and binary forms, with or\r
+ * without modification, are permitted provided that the following\r
+ * conditions are met:\r
+ *\r
+ * 1. Redistributions of source code must retain the above\r
+ * copyright notice, this list of conditions and the following\r
+ * disclaimer.\r
+ *\r
+ * 2. Redistributions in binary form must reproduce the above\r
+ * copyright notice, this list of conditions and the following\r
+ * disclaimer in the documentation and/or other materials\r
+ * provided with the distribution.\r
+ *\r
+ *\r
+ * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS\r
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\r
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR\r
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF\r
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED\r
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\r
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\r
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r
+ * POSSIBILITY OF SUCH DAMAGE.\r
+ *\r
+ * The views and conclusions contained in the software and\r
+ * documentation are those of the authors and should not be\r
+ * interpreted as representing official policies, either expressed\r
+ * or implied, of GRNET S.A.\r
+ * </copyright>\r
+ * -----------------------------------------------------------------------\r
+ */\r
+#endregion\r
+using System.Collections.Specialized;\r
+using System.Diagnostics;\r
+using System.Diagnostics.Contracts;\r
+using System.IO;\r
+using System.Net;\r
+using System.Reflection;\r
+using System.Runtime.Serialization;\r
+using System.Threading;\r
+using System.Threading.Tasks;\r
+using log4net;\r
+\r
+\r
+namespace Pithos.Network\r
+{\r
+ using System;\r
+ using System.Collections.Generic;\r
+ using System.Linq;\r
+ using System.Text;\r
+\r
+ /// <summary>\r
+ /// TODO: Update summary.\r
+ /// </summary>\r
+ public class RestClient:WebClient\r
+ {\r
+ private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);\r
+\r
+ public int Timeout { get; set; }\r
+\r
+ public bool TimedOut { get; set; }\r
+\r
+ public HttpStatusCode StatusCode { get; private set; }\r
+\r
+ public string StatusDescription { get; set; }\r
+\r
+ public long? RangeFrom { get; set; }\r
+ public long? RangeTo { get; set; }\r
+\r
+ public int Retries { get; set; }\r
+\r
+ private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();\r
+ public Dictionary<string, string> Parameters\r
+ {\r
+ get\r
+ {\r
+ Contract.Ensures(_parameters!=null);\r
+ return _parameters;\r
+ } \r
+ }\r
+\r
+\r
+ [ContractInvariantMethod]\r
+ private void Invariants()\r
+ {\r
+ Contract.Invariant(Headers!=null); \r
+ }\r
+\r
+ public RestClient():base()\r
+ {\r
+ //The maximum error response must be large because missing server hashes are return as a Conflivt (409) error response\r
+ //Any value above 2^21-1 will result in an empty response.\r
+ //-1 essentially ignores the maximum length\r
+ HttpWebRequest.DefaultMaximumErrorResponseLength = -1;\r
+ }\r
+\r
+ \r
+ public RestClient(RestClient other)\r
+ : base()\r
+ {\r
+ if (other==null)\r
+ //Log.ErrorFormat("[ERROR] No parameters provided to the rest client. \n{0}\n", other);\r
+ throw new ArgumentNullException("other");\r
+ Contract.EndContractBlock();\r
+\r
+ //The maximum error response must be large because missing server hashes are return as a Conflivt (409) error response\r
+ //Any value above 2^21-1 will result in an empty response.\r
+ //-1 essentially ignores the maximum length\r
+ HttpWebRequest.DefaultMaximumErrorResponseLength = -1;\r
+ \r
+\r
+ CopyHeaders(other);\r
+ Timeout = other.Timeout;\r
+ Retries = other.Retries;\r
+ BaseAddress = other.BaseAddress; \r
+\r
+ foreach (var parameter in other.Parameters)\r
+ {\r
+ Parameters.Add(parameter.Key,parameter.Value);\r
+ }\r
+\r
+ this.Proxy = other.Proxy;\r
+ }\r
+\r
+\r
+ protected override WebRequest GetWebRequest(Uri address)\r
+ {\r
+ TimedOut = false;\r
+ var webRequest = base.GetWebRequest(address); \r
+ var request = (HttpWebRequest)webRequest;\r
+ request.CookieContainer=new CookieContainer();\r
+ request.ServicePoint.ConnectionLimit = 50;\r
+ if (IfModifiedSince.HasValue)\r
+ request.IfModifiedSince = IfModifiedSince.Value;\r
+ request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;\r
+ if(Timeout>0)\r
+ request.Timeout = Timeout;\r
+\r
+ if (RangeFrom.HasValue)\r
+ {\r
+ if (RangeTo.HasValue)\r
+ request.AddRange(RangeFrom.Value, RangeTo.Value);\r
+ else\r
+ request.AddRange(RangeFrom.Value);\r
+ }\r
+ return request; \r
+ }\r
+\r
+ public DateTime? IfModifiedSince { get; set; }\r
+\r
+ //Asynchronous version\r
+ protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)\r
+ { \r
+ Log.InfoFormat("[{0}] {1}", request.Method, request.RequestUri); \r
+ HttpWebResponse response = null;\r
+\r
+ try\r
+ {\r
+ response = (HttpWebResponse)base.GetWebResponse(request, result);\r
+ }\r
+ catch (WebException exc)\r
+ {\r
+ if (!TryGetResponse(exc, request,out response))\r
+ throw;\r
+ }\r
+\r
+ StatusCode = response.StatusCode;\r
+ LastModified = response.LastModified;\r
+ StatusDescription = response.StatusDescription;\r
+ return response;\r
+\r
+ }\r
+ \r
+\r
+ //Synchronous version\r
+ protected override WebResponse GetWebResponse(WebRequest request)\r
+ {\r
+ HttpWebResponse response = null;\r
+ try\r
+ { \r
+ Log.InfoFormat("[{0}] {1}",request.Method,request.RequestUri); \r
+ response = (HttpWebResponse)base.GetWebResponse(request);\r
+ }\r
+ catch (WebException exc)\r
+ {\r
+ if (!TryGetResponse(exc, request,out response))\r
+ throw;\r
+ }\r
+\r
+ StatusCode = response.StatusCode;\r
+ LastModified = response.LastModified;\r
+ StatusDescription = response.StatusDescription;\r
+ return response;\r
+ }\r
+\r
+ private bool TryGetResponse(WebException exc, WebRequest request,out HttpWebResponse response)\r
+ {\r
+ response = null;\r
+ //Fail on empty response\r
+ if (exc.Response == null)\r
+ {\r
+ Log.WarnFormat("[{0}] {1} {2}", request.Method, exc.Status, request.RequestUri); \r
+ return false;\r
+ }\r
+\r
+ response = (exc.Response as HttpWebResponse);\r
+ var statusCode = (int)response.StatusCode;\r
+ //Succeed on allowed status codes\r
+ if (AllowedStatusCodes.Contains(response.StatusCode))\r
+ {\r
+ if (Log.IsDebugEnabled)\r
+ Log.DebugFormat("[{0}] {1} {2}", request.Method, statusCode, request.RequestUri); \r
+ return true;\r
+ }\r
+ \r
+ Log.WarnFormat("[{0}] {1} {2}", request.Method, statusCode, request.RequestUri);\r
+\r
+ //Does the response have any content to log?\r
+ if (exc.Response.ContentLength > 0)\r
+ {\r
+ var content = LogContent(exc.Response);\r
+ Log.ErrorFormat(content);\r
+ }\r
+ return false;\r
+ }\r
+\r
+ private readonly List<HttpStatusCode> _allowedStatusCodes=new List<HttpStatusCode>{HttpStatusCode.NotModified}; \r
+\r
+ public List<HttpStatusCode> AllowedStatusCodes\r
+ {\r
+ get\r
+ {\r
+ return _allowedStatusCodes;\r
+ } \r
+ }\r
+\r
+ public DateTime LastModified { get; private set; }\r
+\r
+ private static string LogContent(WebResponse webResponse)\r
+ {\r
+ if (webResponse == null)\r
+ throw new ArgumentNullException("webResponse");\r
+ Contract.EndContractBlock();\r
+\r
+ //The response stream must be copied to avoid affecting other code by disposing of the \r
+ //original response stream.\r
+ var stream = webResponse.GetResponseStream(); \r
+ using(var memStream=new MemoryStream())\r
+ using (var reader = new StreamReader(memStream))\r
+ {\r
+ stream.CopyTo(memStream); \r
+ string content = reader.ReadToEnd();\r
+\r
+ stream.Seek(0,SeekOrigin.Begin);\r
+ return content;\r
+ }\r
+ }\r
+\r
+ public string DownloadStringWithRetry(string address,int retries=0)\r
+ {\r
+ \r
+ if (address == null)\r
+ throw new ArgumentNullException("address");\r
+\r
+ var actualAddress = GetActualAddress(address);\r
+\r
+ TraceStart("GET",actualAddress); \r
+ \r
+ var actualRetries = (retries == 0) ? Retries : retries;\r
+\r
+ var uriString = String.Join("/", BaseAddress.TrimEnd('/'), actualAddress);\r
+\r
+ var task = Retry(() =>\r
+ { \r
+ var content = DownloadString(uriString);\r
+\r
+ if (StatusCode == HttpStatusCode.NoContent)\r
+ return String.Empty;\r
+ return content;\r
+\r
+ }, actualRetries);\r
+\r
+ try\r
+ {\r
+ var result = task.Result;\r
+ return result;\r
+\r
+ }\r
+ catch (AggregateException exc)\r
+ {\r
+ //If the task fails, propagate the original exception\r
+ if (exc.InnerException!=null)\r
+ throw exc.InnerException;\r
+ throw;\r
+ }\r
+ }\r
+\r
+ public void Head(string address,int retries=0)\r
+ {\r
+ AllowedStatusCodes.Add(HttpStatusCode.NotFound);\r
+ RetryWithoutContent(address, retries, "HEAD");\r
+ }\r
+\r
+ public void PutWithRetry(string address, int retries = 0, string contentType=null)\r
+ {\r
+ RetryWithoutContent(address, retries, "PUT",contentType);\r
+ }\r
+\r
+ public void PostWithRetry(string address,string contentType)\r
+ { \r
+ RetryWithoutContent(address, 0, "POST",contentType);\r
+ }\r
+\r
+ public void DeleteWithRetry(string address,int retries=0)\r
+ {\r
+ RetryWithoutContent(address, retries, "DELETE");\r
+ }\r
+\r
+ public string GetHeaderValue(string headerName,bool optional=false)\r
+ {\r
+ if (this.ResponseHeaders==null)\r
+ throw new InvalidOperationException("ResponseHeaders are null");\r
+ Contract.EndContractBlock();\r
+\r
+ var values=this.ResponseHeaders.GetValues(headerName);\r
+ if (values != null)\r
+ return values[0];\r
+\r
+ if (optional) \r
+ return null; \r
+ //A required header was not found\r
+ throw new WebException(String.Format("The {0} header is missing", headerName));\r
+ }\r
+\r
+ public void SetNonEmptyHeaderValue(string headerName, string value)\r
+ {\r
+ if (String.IsNullOrWhiteSpace(value))\r
+ return;\r
+ Headers.Add(headerName,value);\r
+ }\r
+\r
+ private void RetryWithoutContent(string address, int retries, string method,string contentType=null)\r
+ {\r
+ if (address == null)\r
+ throw new ArgumentNullException("address");\r
+\r
+ var actualAddress = GetActualAddress(address); \r
+ var actualRetries = (retries == 0) ? Retries : retries;\r
+\r
+ var task = Retry(() =>\r
+ {\r
+ var uriString = String.Join("/",BaseAddress ,actualAddress);\r
+ var uri = new Uri(uriString);\r
+ var request = GetWebRequest(uri);\r
+ if (contentType!=null)\r
+ {\r
+ request.ContentType = contentType;\r
+ request.ContentLength = 0;\r
+ }\r
+ request.Method = method;\r
+ if (ResponseHeaders!=null)\r
+ ResponseHeaders.Clear();\r
+\r
+ TraceStart(method, uriString);\r
+ if (method == "PUT")\r
+ request.ContentLength = 0;\r
+\r
+ //Have to use try/finally instead of using here, because WebClient needs a valid WebResponse object\r
+ //in order to return response headers\r
+ var response = (HttpWebResponse)GetWebResponse(request);\r
+ try\r
+ {\r
+ LastModified = response.LastModified;\r
+ StatusCode = response.StatusCode;\r
+ StatusDescription = response.StatusDescription;\r
+ }\r
+ finally\r
+ {\r
+ response.Close();\r
+ }\r
+ \r
+\r
+ return 0;\r
+ }, actualRetries);\r
+\r
+ try\r
+ {\r
+ task.Wait();\r
+ }\r
+ catch (AggregateException ex)\r
+ {\r
+ var exc = ex.InnerException;\r
+ if (exc is RetryException)\r
+ {\r
+ Log.ErrorFormat("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);\r
+ }\r
+ else\r
+ {\r
+ Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, exc);\r
+ }\r
+ throw exc;\r
+\r
+ }\r
+ catch(Exception ex)\r
+ {\r
+ Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, ex);\r
+ throw;\r
+ }\r
+ }\r
+ \r
+ private static void TraceStart(string method, string actualAddress)\r
+ {\r
+ Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);\r
+ }\r
+\r
+ private string GetActualAddress(string address)\r
+ {\r
+ if (Parameters.Count == 0)\r
+ return address;\r
+ var addressBuilder=new StringBuilder(address); \r
+\r
+ bool isFirst = true;\r
+ foreach (var parameter in Parameters)\r
+ {\r
+ if(isFirst)\r
+ addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);\r
+ else\r
+ addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);\r
+ isFirst = false;\r
+ }\r
+ return addressBuilder.ToString();\r
+ }\r
+\r
+ public string DownloadStringWithRetry(Uri address,int retries=0)\r
+ {\r
+ if (address == null)\r
+ throw new ArgumentNullException("address");\r
+\r
+ var actualRetries = (retries == 0) ? Retries : retries; \r
+ var task = Retry(() =>\r
+ {\r
+ var content = base.DownloadString(address);\r
+\r
+ if (StatusCode == HttpStatusCode.NoContent)\r
+ return String.Empty;\r
+ return content;\r
+\r
+ }, actualRetries);\r
+\r
+ var result = task.Result;\r
+ return result;\r
+ }\r
+\r
+ \r
+ /// <summary>\r
+ /// Copies headers from another RestClient\r
+ /// </summary>\r
+ /// <param name="source">The RestClient from which the headers are copied</param>\r
+ public void CopyHeaders(RestClient source)\r
+ {\r
+ if (source == null)\r
+ throw new ArgumentNullException("source", "source can't be null");\r
+ Contract.EndContractBlock();\r
+ //The Headers getter initializes the property, it is never null\r
+ Contract.Assume(Headers!=null);\r
+ \r
+ CopyHeaders(source.Headers,Headers);\r
+ }\r
+ \r
+ /// <summary>\r
+ /// Copies headers from one header collection to another\r
+ /// </summary>\r
+ /// <param name="source">The source collection from which the headers are copied</param>\r
+ /// <param name="target">The target collection to which the headers are copied</param>\r
+ public static void CopyHeaders(WebHeaderCollection source,WebHeaderCollection target)\r
+ {\r
+ if (source == null)\r
+ throw new ArgumentNullException("source", "source can't be null");\r
+ if (target == null)\r
+ throw new ArgumentNullException("target", "target can't be null");\r
+ Contract.EndContractBlock();\r
+\r
+ for (int i = 0; i < source.Count; i++)\r
+ {\r
+ target.Add(source.GetKey(i), source[i]);\r
+ } \r
+ }\r
+\r
+ public void AssertStatusOK(string message)\r
+ {\r
+ if (StatusCode >= HttpStatusCode.BadRequest)\r
+ throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));\r
+ }\r
+\r
+\r
+ private Task<T> Retry<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)\r
+ {\r
+ if (original==null)\r
+ throw new ArgumentNullException("original");\r
+ Contract.EndContractBlock();\r
+\r
+ if (tcs == null)\r
+ tcs = new TaskCompletionSource<T>();\r
+ Task.Factory.StartNew(original).ContinueWith(_original =>\r
+ {\r
+ if (!_original.IsFaulted)\r
+ tcs.SetFromTask(_original);\r
+ else \r
+ {\r
+ var e = _original.Exception.InnerException;\r
+ var we = (e as WebException);\r
+ if (we==null)\r
+ tcs.SetException(e);\r
+ else\r
+ {\r
+ var statusCode = GetStatusCode(we);\r
+\r
+ //Return null for 404\r
+ if (statusCode == HttpStatusCode.NotFound)\r
+ tcs.SetResult(default(T));\r
+ //Retry for timeouts and service unavailable\r
+ else if (we.Status == WebExceptionStatus.Timeout ||\r
+ (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))\r
+ {\r
+ TimedOut = true;\r
+ if (retryCount == 0)\r
+ { \r
+ Log.ErrorFormat("[ERROR] Timed out too many times. \n{0}\n",e);\r
+ tcs.SetException(new RetryException("Timed out too many times.", e)); \r
+ }\r
+ else\r
+ {\r
+ Log.ErrorFormat(\r
+ "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,\r
+ retryCount, e);\r
+ Retry(original, retryCount - 1, tcs);\r
+ }\r
+ }\r
+ else\r
+ tcs.SetException(e);\r
+ }\r
+ };\r
+ });\r
+ return tcs.Task;\r
+ }\r
+\r
+ private HttpStatusCode GetStatusCode(WebException we)\r
+ {\r
+ if (we==null)\r
+ throw new ArgumentNullException("we");\r
+ var statusCode = HttpStatusCode.RequestTimeout;\r
+ if (we.Response != null)\r
+ {\r
+ statusCode = ((HttpWebResponse) we.Response).StatusCode;\r
+ this.StatusCode = statusCode;\r
+ }\r
+ return statusCode;\r
+ }\r
+\r
+ public UriBuilder GetAddressBuilder(string container, string objectName)\r
+ {\r
+ var builder = new UriBuilder(String.Join("/", BaseAddress, container, objectName));\r
+ return builder;\r
+ }\r
+\r
+ public Dictionary<string, string> GetMeta(string metaPrefix)\r
+ {\r
+ if (String.IsNullOrWhiteSpace(metaPrefix))\r
+ throw new ArgumentNullException("metaPrefix");\r
+ Contract.EndContractBlock();\r
+\r
+ var keys = ResponseHeaders.AllKeys.AsQueryable();\r
+ var dict = (from key in keys\r
+ where key.StartsWith(metaPrefix)\r
+ let name = key.Substring(metaPrefix.Length)\r
+ select new { Name = name, Value = ResponseHeaders[key] })\r
+ .ToDictionary(t => t.Name, t => t.Value);\r
+ return dict;\r
+ }\r
+\r
+\r
+ internal Task DownloadFileTaskAsync(Uri uri, string fileName, CancellationToken cancellationToken, IProgress<DownloadProgressChangedEventArgs> progress)\r
+ {\r
+ cancellationToken.Register(CancelAsync);\r
+ DownloadProgressChangedEventHandler onDownloadProgressChanged = (o, e) => progress.Report(e);\r
+ this.DownloadProgressChanged += onDownloadProgressChanged;\r
+ return this.DownloadFileTaskAsync(uri, fileName).ContinueWith(t=>\r
+ {\r
+ this.DownloadProgressChanged -= onDownloadProgressChanged;\r
+ });\r
+ }\r
+\r
+ internal Task<byte[]> DownloadDataTaskAsync(Uri uri, CancellationToken cancellationToken, IProgress<DownloadProgressChangedEventArgs> progress)\r
+ {\r
+ cancellationToken.Register(CancelAsync);\r
+ DownloadProgressChangedEventHandler onDownloadProgressChanged = (o, e) => progress.Report(e);\r
+ this.DownloadProgressChanged += onDownloadProgressChanged;\r
+ return this.DownloadDataTaskAsync(uri).ContinueWith(t =>\r
+ {\r
+ this.DownloadProgressChanged -= onDownloadProgressChanged;\r
+ return t.Result;\r
+ });\r
+ }\r
+ }\r
+\r
+ public class RetryException:Exception\r
+ {\r
+ public RetryException()\r
+ :base()\r
+ {\r
+ \r
+ }\r
+\r
+ public RetryException(string message)\r
+ :base(message)\r
+ {\r
+ \r
+ }\r
+\r
+ public RetryException(string message,Exception innerException)\r
+ :base(message,innerException)\r
+ {\r
+ \r
+ }\r
+\r
+ public RetryException(SerializationInfo info,StreamingContext context)\r
+ :base(info,context)\r
+ {\r
+ \r
+ }\r
+ }\r
+}\r