using System; using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; 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(); source.Add(new KeyValuePair("page", paging.Page.ToString())); source.Add(new KeyValuePair("pageSize", paging.PageSize.ToString())); return source; } protected IDictionary GetApiPasswordHeaderAttributes(string apiKeyPassword, IDictionary source = null) { if (source == null) source = new Dictionary(); source.Add(new KeyValuePair("apiKeyPassword", apiKeyPassword)); return source; } protected IDictionary GetOverwriteHeaderAttributes(bool overwrite, IDictionary source = null) { if (source == null) source = new Dictionary(); source.Add(new KeyValuePair("overwrite", overwrite.ToString())); return source; } protected IDictionary GetForceHeaderAttributes(bool force, IDictionary source = null) { if (source == null) source = new Dictionary(); source.Add(new KeyValuePair("force", force.ToString())); return source; } protected IDictionary GetUserContextHeaderAttributes(string userNameContext, IDictionary source = null) { if (source == null) source = new Dictionary(); source.Add(new KeyValuePair("context", userNameContext)); return source; } 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, string fileName,Stream stream,string streamMimeType, IDictionary headerAttributes = null, bool silent = false) where TResult : ResultDto { Log(LogSeverityEnum.Trace, $"Calling REST API POST-MULTI-STREAM {_uriApiBase.AbsoluteUri + relativeUrlApiCall}"); var boundary = Guid.NewGuid().ToString(); var mpc = new MultipartFormDataContent(boundary); mpc.Headers.Remove("Content-Type"); mpc.Headers.TryAddWithoutValidation("Content-Type", "multipart/form-data; boundary=" + boundary); var stc = new StreamContent(stream); stc.Headers.ContentType = new MediaTypeHeaderValue(streamMimeType); mpc.Add(stc, "content", fileName); using (var rq = new HttpRequestMessage()) { rq.Content = mpc; rq.Method = new System.Net.Http.HttpMethod("POST"); rq.RequestUri = new Uri(_uriApiBase.AbsoluteUri + relativeUrlApiCall); rq.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/octet-stream")); if (headerAttributes != null) foreach (var key in headerAttributes.Keys) rq.Headers.Add(key, headerAttributes[key]); var start = DateTime.Now; var resp = await _client.SendAsync(rq); //var resp = await cli.PostAsync(relativeUrlApiCall, rqdMulti); 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; SetDefaultClientHeaders(_client); HttpContentHeaders reqHeaders = 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)); reqHeaders = req.Headers; if (headerAttributes!=null) foreach (var key in headerAttributes.Keys) { //if (req.Headers.Contains(key)) req.Headers.Remove(key); 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)); reqHeaders = req.Headers; if (headerAttributes!=null) foreach (var key in headerAttributes.Keys) { // if (req.Headers.Contains(key)) req.Headers.Remove(key); req.Headers.Add(key, headerAttributes[key]); } req.Headers.ContentType = new MediaTypeHeaderValue("application/json"); resp = await _client.PutAsync(relativeUrlApiCall, req); break; case RestCallTypeEnum.Delete: using (var delRq = new HttpRequestMessage(HttpMethod.Delete, relativeUrlApiCall)) { if (headerAttributes != null) foreach (var key in headerAttributes.Keys) { delRq.Headers.Remove(key); delRq.Headers.Add(key, headerAttributes[key]); } resp = await _client.SendAsync(delRq); } 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, DumpHttpClient(method,relativeUrlApiCall, _client, reqHeaders)); Log(LogSeverityEnum.Trace, content); } var result = (TResult)_binder.LoadFromString(content, typeof(TResult)); if (result.IsSuccess) return result; // No data exception pass result if (!result.IsSuccess && result.Errors.Any(x => x.Code == 1099)) 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; SetDefaultClientHeaders(client); return client; } private void SetDefaultClientHeaders(HttpClient client) { client.DefaultRequestHeaders.Clear(); client.DefaultRequestHeaders.Add("User-Agent", "BO.Connector." + ApiName); // Add an Accept header for JSON format. client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("multipart/mixed")); //client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/octet-stream")); } private string DumpHttpClient(RestCallTypeEnum method, string relativeUrl, HttpClient client, HttpContentHeaders headers) { var sb = new StringBuilder(); sb.Append($"RQ {method.ToString().ToUpper()} to url '{_client.BaseAddress + relativeUrl}':").AppendLine(); sb.Append($"rq version: {_client.DefaultRequestVersion}").AppendLine(); sb.Append($"rq timeout: {_client.Timeout.ToReadableString()}").AppendLine(); sb.Append("Headers:").AppendLine(); foreach (var header in _client.DefaultRequestHeaders) sb.Append(header.Key).Append(" : ").Append(string.Join(",", header.Value)).AppendLine(); sb.Append(DumpHttpHeaders(headers)); return sb.ToString(); } private string DumpHttpHeaders(HttpContentHeaders headers) { var sb = new StringBuilder(); sb.Append("Headers:").AppendLine(); if (headers != null) { foreach (var header in headers) sb.Append(header.Key).Append(" : ").Append(string.Join(",", header.Value)).AppendLine(); } return sb.ToString(); } #endregion } }