using System; using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using BO.AppServer.Metadata.Dto; using Quadarax.Foundation.Core.Data.Interface; using Quadarax.Foundation.Core.Data.Interface.Entity; using Quadarax.Foundation.Core.Data.Interface.Entity.Dto; using Quadarax.Foundation.Core.Json; using Quadarax.Foundation.Core.Logging; using Quadarax.Foundation.Core.Object; using Quadarax.Foundation.Core.Value.Extensions; namespace BO.Connector { public abstract class AbstractConnection : DisposableObject { #region *** Private fields *** private HttpClient _client; private Uri _uriApiBase; private ILogHandler _log; private TimeSpan _timeout; private string _ticket; private Binder _binder; private bool _isDumpContentEnabled; protected abstract string ApiName { get; } protected bool IsOpen => !string.IsNullOrEmpty(_ticket); protected string Ticket => _ticket; #endregion #region *** Constructors *** protected AbstractConnection(Uri uriApiBase, TimeSpan timeout, ILogHandler log = null, bool isDumpContentEnabled = false) { _uriApiBase = uriApiBase ?? throw new ArgumentNullException(nameof(uriApiBase)); _uriApiBase = new Uri(_uriApiBase.AbsoluteUri + ApiName + "/"); _log = log; _timeout = timeout; _binder = new Binder(new FileSystem()); _binder.SerializerOptions.IgnoreReadOnlyProperties = true; _binder.SerializerOptions.IgnoreNullValues = true; _isDumpContentEnabled = isDumpContentEnabled; } #endregion #region *** Public Operations *** public void Open(string userName, string userPassword, string magicKey) { Log(LogSeverityEnum.Trace, $"Opening Connector '{ApiName}' open with magicKey='{magicKey}'"); _client = CreateClient(); InitTicket(magicKey); Login(userName, userPassword); OnOpening(); Log(LogSeverityEnum.Debug, $"Connector '{ApiName}' open for user '{userName}' with timeout setting {_timeout}."); } public void Close() { _client?.CancelPendingRequests(); _client?.Dispose(); _client = null; _ticket = string.Empty; Log(LogSeverityEnum.Debug, $"Connector '{ApiName}' closed."); } public bool Ping(out TimeSpan duration, out string status) { CheckIsOpen(); var start = DateTime.Now; var ping = Task.Run(() =>CallRequestAsync>($"{_ticket}/ping", RestCallTypeEnum.Get, null, null, true)); ping.Wait(); duration = DateTime.Now - start; if (ping.Result == null) { status = string.Empty; return false; } status = ping.Result.Value.Status; return ping.Result.IsSuccess; } #endregion #region *** Virtuals & Overrides *** protected override void OnDisposing() { _client?.Dispose(); } #endregion #region *** Private Operations *** protected void CheckIsOpen() { if (!IsOpen) throw new InvalidOperationException( "Connector is not open to call specific operation. Call Open() first."); } protected void Log(LogSeverityEnum type, string message, Exception e = null) { _log?.Log(type, message, e); } protected void InitTicket(string magicKey) { var result = Task.Run(() => CallRequestAsync>($"{magicKey}/auth", RestCallTypeEnum.Get));; result.Wait(); if (!result.Result.IsSuccess) throw result.Result.ToAggregateException(); _ticket = result.Result.Value.Ticket; } protected void Login(string userName, string userPassword) { var headerAtts = new Dictionary(); headerAtts.Add("userName",userName); headerAtts.Add("userPassword",userPassword); var result = Task.Run(() => CallRequestAsync>($"{_ticket}/login", RestCallTypeEnum.Post, new ResultPlain(), headerAtts)); result.Wait(); if (!result.Result.IsSuccess) throw result.Result.ToAggregateException(); Log(LogSeverityEnum.Info,$"User '{result.Result.Value.Name}' is logged in."); } protected IDictionary GetPagingHeaderAttributes(PagingDto paging, IDictionary source = null) { if (paging == null || paging.IsDisabled) return null;//paging = new PagingDto(); if (source == null) source = new Dictionary(); var result = new Dictionary { { "page", paging.Page.ToString() }, { "pageSize", paging.PageSize.ToString() } }; return result; } protected IDictionary GetApiPasswordHeaderAttributes(string apiKeyPassword, IDictionary source = null) { if (source == null) source = new Dictionary(); var result = new Dictionary { { "apiKeyPassword", apiKeyPassword }, }; return result; } protected IDictionary GetUserContextHeaderAttributes(string userNameContext, IDictionary source = null) { if (source == null) source = new Dictionary(); var result = new Dictionary { { "context", userNameContext }, }; return result; } protected async Task CallGetStreamAsync(string relativeUrlApiCall, IDictionary headerAttributes = null, bool silent = false) { Log(LogSeverityEnum.Trace, $"Calling REST API GET STREAM {_uriApiBase.AbsoluteUri + relativeUrlApiCall}"); if (headerAttributes != null) { foreach (var key in headerAttributes.Keys) { _client.DefaultRequestHeaders.Remove(key); _client.DefaultRequestHeaders.Add(key,headerAttributes[key]); } } return await _client.GetStreamAsync(relativeUrlApiCall); } protected async Task CallPostStreamMultipartAsync(string relativeUrlApiCall, IDto data,Stream stream, IDictionary headerAttributes = null, bool silent = false) where TResult : ResultDto { Log(LogSeverityEnum.Trace, $"Calling REST API POST-MULTI-STREAM {_uriApiBase.AbsoluteUri + relativeUrlApiCall}"); var rqData = new StringContent(_binder.SaveToString(data)); var rqStream = new StreamContent(stream); var req = new MultipartContent(); req.Add(rqData); req.Add(rqStream); if (headerAttributes!=null) foreach (var key in headerAttributes.Keys) req.Headers.Add(key,headerAttributes[key]); req.Headers.ContentType = new MediaTypeHeaderValue("application/json"); var start = DateTime.Now; var resp = await _client.PostAsync(relativeUrlApiCall, req); Log(LogSeverityEnum.Trace, $"REST API POST-MULTI-STREAM {_uriApiBase.AbsoluteUri + relativeUrlApiCall} done @ {(DateTime.Now - start).ToReadableString()}"); if (!resp.IsSuccessStatusCode) { var message = $"Calling REST API POST-MULTI-STREAM {_uriApiBase.AbsoluteUri + relativeUrlApiCall} ends with result code '{resp.StatusCode}'"; Log(LogSeverityEnum.Warn, message); throw new Exception(message); } var content = await resp.Content.ReadAsStringAsync(); if (_isDumpContentEnabled) Log(LogSeverityEnum.Trace, content); var result = (TResult)_binder.LoadFromString(content, typeof(TResult)); if (result.IsSuccess) return result; if (silent) return result; // handle errors var excps = new List(); foreach (var err in result.Errors) { var exc = new Exception($"[{err.Code}] {err.Message}"); exc.Data.Add("Code", err.Code); excps.Add(exc); } throw new AggregateException(excps); } protected async Task CallRequestAsync(string relativeUrlApiCall,RestCallTypeEnum method, IDto data = null,IDictionary headerAttributes = null, bool silent = false) where TResult : ResultDto { if (data == null) if(method != RestCallTypeEnum.Get && method != RestCallTypeEnum.GetStream && method != RestCallTypeEnum.Delete) throw new ArgumentNullException(nameof(data), $"For REST API method '{method}' data must be specified!"); Log(LogSeverityEnum.Trace, $"Calling REST API {method} {_uriApiBase.AbsoluteUri + relativeUrlApiCall}"); var start = DateTime.Now; HttpResponseMessage resp = null; switch (method) { case RestCallTypeEnum.Get: if (headerAttributes != null) { foreach (var key in headerAttributes.Keys) { _client.DefaultRequestHeaders.Remove(key); _client.DefaultRequestHeaders.Add(key,headerAttributes[key]); } } resp = await _client.GetAsync(relativeUrlApiCall); break; case RestCallTypeEnum.Post: var req = new StringContent(_binder.SaveToString(data)); if (headerAttributes!=null) foreach (var key in headerAttributes.Keys) req.Headers.Add(key,headerAttributes[key]); req.Headers.ContentType = new MediaTypeHeaderValue("application/json"); resp = await _client.PostAsync(relativeUrlApiCall, req); break; case RestCallTypeEnum.Put: req = new StringContent(_binder.SaveToString(data)); if (headerAttributes!=null) foreach (var key in headerAttributes.Keys) req.Headers.Add(key,headerAttributes[key]); req.Headers.ContentType = new MediaTypeHeaderValue("application/json"); resp = await _client.PutAsync(relativeUrlApiCall, req); break; case RestCallTypeEnum.Delete: resp = await _client.DeleteAsync(relativeUrlApiCall); break; default: throw new NotSupportedException($"{method} REST API operation is not implemented."); } // may remove paging headers after call /* if (headerAttributes != null) foreach (var key in headerAttributes.Keys) _client.DefaultRequestHeaders.Remove(key); */ Log(LogSeverityEnum.Trace, $"REST API {method} {_uriApiBase.AbsoluteUri + relativeUrlApiCall} done @ {(DateTime.Now - start).ToReadableString()}"); if (!resp.IsSuccessStatusCode) { var message = $"Calling REST API {method} {_uriApiBase.AbsoluteUri + relativeUrlApiCall} ends with result code '{resp.StatusCode}'"; Log(LogSeverityEnum.Warn, message); throw new Exception(message); } var content = await resp.Content.ReadAsStringAsync(); if (_isDumpContentEnabled) Log(LogSeverityEnum.Trace, content); var result = (TResult)_binder.LoadFromString(content, typeof(TResult)); if (result.IsSuccess) return result; if (silent) return result; // handle errors var excps = new List(); foreach (var err in result.Errors) { var exc = new Exception($"[{err.Code}] {err.Message}"); exc.Data.Add("Code", err.Code); excps.Add(exc); } throw new AggregateException(excps); } protected virtual void OnOpening() { } private HttpClient CreateClient() { var client = new HttpClient(); client.BaseAddress = new Uri(_uriApiBase, ApiName); client.Timeout = _timeout; client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Add("User-Agent", "BO.Connector." + ApiName); // Add an Accept header for JSON format. client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); return client; } #endregion } }