/* * Copyright (c) Contributors, http://opensimulator.org/ * See CONTRIBUTORS.TXT for a full list of copyright holders. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 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. * * Neither the name of the OpenSimulator Project nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``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 THE 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. */ using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Reflection; using System.Text; using System.Threading; using System.Web; using log4net; namespace OpenSim.Framework.Communications { /// /// Implementation of a generic REST client /// /// /// This class is a generic implementation of a REST (Representational State Transfer) web service. This /// class is designed to execute both synchronously and asynchronously. /// /// Internally the implementation works as a two stage asynchronous web-client. /// When the request is initiated, RestClient will query asynchronously for for a web-response, /// sleeping until the initial response is returned by the server. Once the initial response is retrieved /// the second stage of asynchronous requests will be triggered, in an attempt to read of the response /// object into a memorystream as a sequence of asynchronous reads. /// /// The asynchronisity of RestClient is designed to move as much processing into the back-ground, allowing /// other threads to execute, while it waits for a response from the web-service. RestClient itself can be /// invoked by the caller in either synchronous mode or asynchronous modes. /// public class RestClient { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); // private string realuri; #region member variables /// /// The base Uri of the web-service e.g. http://www.google.com /// private string _url; /// /// Path elements of the query /// private List _pathElements = new List(); /// /// Parameter elements of the query, e.g. min=34 /// private Dictionary _parameterElements = new Dictionary(); /// /// Request method. E.g. GET, POST, PUT or DELETE /// private string _method; /// /// Temporary buffer used to store bytes temporarily as they come in from the server /// private byte[] _readbuf; /// /// MemoryStream representing the resultiong resource /// private Stream _resource; /// /// WebRequest object, held as a member variable /// private HttpWebRequest _request; /// /// WebResponse object, held as a member variable, so we can close it /// private HttpWebResponse _response; /// /// This flag will help block the main synchroneous method, in case we run in synchroneous mode /// //public static ManualResetEvent _allDone = new ManualResetEvent(false); /// /// Default time out period /// //private const int DefaultTimeout = 10*1000; // 10 seconds timeout /// /// Default Buffer size of a block requested from the web-server /// private const int BufferSize = 4096; // Read blocks of 4 KB. /// /// if an exception occours during async processing, we need to save it, so it can be /// rethrown on the primary thread; /// private Exception _asyncException; #endregion member variables #region constructors /// /// Instantiate a new RestClient /// /// Web-service to query, e.g. http://osgrid.org:8003 public RestClient(string url) { _url = url; _readbuf = new byte[BufferSize]; _resource = new MemoryStream(); _request = null; _response = null; _lock = new object(); } private object _lock; #endregion constructors /// /// Add a path element to the query, e.g. assets /// /// path entry public void AddResourcePath(string element) { if (isSlashed(element)) _pathElements.Add(element.Substring(0, element.Length - 1)); else _pathElements.Add(element); } /// /// Add a query parameter to the Url /// /// Name of the parameter, e.g. min /// Value of the parameter, e.g. 42 public void AddQueryParameter(string name, string value) { try { _parameterElements.Add(HttpUtility.UrlEncode(name), HttpUtility.UrlEncode(value)); } catch (ArgumentException) { m_log.Error("[REST]: Query parameter " + name + " is already added."); } catch (Exception e) { m_log.Error("[REST]: An exception was raised adding query parameter to dictionary. Exception: {0}",e); } } /// /// Add a query parameter to the Url /// /// Name of the parameter, e.g. min public void AddQueryParameter(string name) { try { _parameterElements.Add(HttpUtility.UrlEncode(name), null); } catch (ArgumentException) { m_log.Error("[REST]: Query parameter " + name + " is already added."); } catch (Exception e) { m_log.Error("[REST]: An exception was raised adding query parameter to dictionary. Exception: {0}",e); } } /// /// Web-Request method, e.g. GET, PUT, POST, DELETE /// public string RequestMethod { get { return _method; } set { _method = value; } } /// /// True if string contains a trailing slash '/' /// /// string to be examined /// true if slash is present private static bool isSlashed(string s) { return s.Substring(s.Length - 1, 1) == "/"; } /// /// Build a Uri based on the initial Url, path elements and parameters /// /// fully constructed Uri private Uri buildUri() { StringBuilder sb = new StringBuilder(); sb.Append(_url); foreach (string e in _pathElements) { sb.Append("/"); sb.Append(e); } bool firstElement = true; foreach (KeyValuePair kv in _parameterElements) { if (firstElement) { sb.Append("?"); firstElement = false; } else sb.Append("&"); sb.Append(kv.Key); if (!string.IsNullOrEmpty(kv.Value)) { sb.Append("="); sb.Append(kv.Value); } } // realuri = sb.ToString(); //m_log.InfoFormat("[REST CLIENT]: RestURL: {0}", realuri); return new Uri(sb.ToString()); } #region Async communications with server /// /// Async method, invoked when a block of data has been received from the service /// /// private void StreamIsReadyDelegate(IAsyncResult ar) { try { Stream s = (Stream) ar.AsyncState; int read = s.EndRead(ar); if (read > 0) { _resource.Write(_readbuf, 0, read); // IAsyncResult asynchronousResult = // s.BeginRead(_readbuf, 0, BufferSize, new AsyncCallback(StreamIsReadyDelegate), s); s.BeginRead(_readbuf, 0, BufferSize, new AsyncCallback(StreamIsReadyDelegate), s); // TODO! Implement timeout, without killing the server //ThreadPool.RegisterWaitForSingleObject(asynchronousResult.AsyncWaitHandle, new WaitOrTimerCallback(TimeoutCallback), _request, DefaultTimeout, true); } else { s.Close(); //_allDone.Set(); } } catch (Exception e) { //_allDone.Set(); _asyncException = e; } } #endregion Async communications with server /// /// Perform a synchronous request /// public Stream Request() { lock (_lock) { _request = (HttpWebRequest) WebRequest.Create(buildUri()); _request.KeepAlive = false; _request.ContentType = "application/xml"; _request.Timeout = 200000; _request.Method = RequestMethod; _asyncException = null; // IAsyncResult responseAsyncResult = _request.BeginGetResponse(new AsyncCallback(ResponseIsReadyDelegate), _request); try { _response = (HttpWebResponse) _request.GetResponse(); } catch (WebException e) { HttpWebResponse errorResponse = e.Response as HttpWebResponse; if (null != errorResponse && HttpStatusCode.NotFound == errorResponse.StatusCode) { m_log.Warn("[REST CLIENT] Resource not found (404)"); } else { m_log.Error("[REST CLIENT] Error fetching resource from server " + _request.Address.ToString()); m_log.Debug(e.ToString()); } return null; } using (Stream src = _response.GetResponseStream()) { int length = src.Read(_readbuf, 0, BufferSize); while (length > 0) { _resource.Write(_readbuf, 0, length); length = src.Read(_readbuf, 0, BufferSize); } } // TODO! Implement timeout, without killing the server // this line implements the timeout, if there is a timeout, the callback fires and the request becomes aborted //ThreadPool.RegisterWaitForSingleObject(responseAsyncResult.AsyncWaitHandle, new WaitOrTimerCallback(TimeoutCallback), _request, DefaultTimeout, true); // _allDone.WaitOne(); if (_response != null) _response.Close(); if (_asyncException != null) throw _asyncException; if (_resource != null) { _resource.Flush(); _resource.Seek(0, SeekOrigin.Begin); } return _resource; } } public Stream Request(Stream src) { _request = (HttpWebRequest) WebRequest.Create(buildUri()); _request.KeepAlive = false; _request.ContentType = "application/xml"; _request.Timeout = 30000; _request.Method = RequestMethod; _asyncException = null; _request.ContentLength = src.Length; m_log.InfoFormat("[REST]: Request Length {0}", _request.ContentLength); m_log.InfoFormat("[REST]: Sending Web Request {0}", buildUri()); src.Seek(0, SeekOrigin.Begin); m_log.Info("[REST]: Seek is ok"); using (Stream dst = _request.GetRequestStream()) { m_log.Info("[REST]: GetRequestStream is ok"); byte[] buf = new byte[1024]; int length = src.Read(buf, 0, 1024); m_log.Info("[REST]: First Read is ok"); while (length > 0) { dst.Write(buf, 0, length); length = src.Read(buf, 0, 1024); } } _response = (HttpWebResponse) _request.GetResponse(); // IAsyncResult responseAsyncResult = _request.BeginGetResponse(new AsyncCallback(ResponseIsReadyDelegate), _request); // TODO! Implement timeout, without killing the server // this line implements the timeout, if there is a timeout, the callback fires and the request becomes aborted //ThreadPool.RegisterWaitForSingleObject(responseAsyncResult.AsyncWaitHandle, new WaitOrTimerCallback(TimeoutCallback), _request, DefaultTimeout, true); return null; } #region Async Invocation public IAsyncResult BeginRequest(AsyncCallback callback, object state) { /// /// In case, we are invoked asynchroneously this object will keep track of the state /// AsyncResult ar = new AsyncResult(callback, state); Util.FireAndForget(RequestHelper, ar); return ar; } public Stream EndRequest(IAsyncResult asyncResult) { AsyncResult ar = (AsyncResult) asyncResult; // Wait for operation to complete, then return result or // throw exception return ar.EndInvoke(); } private void RequestHelper(Object asyncResult) { // We know that it's really an AsyncResult object AsyncResult ar = (AsyncResult) asyncResult; try { // Perform the operation; if sucessful set the result Stream s = Request(); ar.SetAsCompleted(s, false); } catch (Exception e) { // If operation fails, set the exception ar.HandleException(e, false); } } #endregion Async Invocation } }