using DataConnection.REST.Configuration; using DataConnection.REST.Interfaces; using DataConnection.REST.Models; // Added for metadata models using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; // Added for XML parsing namespace DataConnection.REST.Implementations { /// /// Client specific for SAP Business One Service Layer. /// Handles session-based authentication and metadata discovery. /// public class SapB1ServiceClient : BaseRestServiceClient, IRestMetadataDiscovery // Implement the interface { private CookieContainer _cookieContainer; private HttpClientHandler _httpClientHandler; private HttpClient _sapHttpClient; // Use a dedicated HttpClient with cookie handling // SAP B1 Specific Login Info private string? _b1SessionId; private string? _routeId; private DateTime _sessionTimeout; // Consider making options specific to SAP B1 if needed public SapB1ServiceClient(RestServiceOptions options) : base(CreateConfiguredHttpClient(options, out var handler, out var container), options) { // Store handler and container for direct cookie access if needed _httpClientHandler = handler; _cookieContainer = container; _sapHttpClient = _httpClient; // Use the base class's configured HttpClient } private static HttpClient CreateConfiguredHttpClient(RestServiceOptions options, out HttpClientHandler handler, out CookieContainer container) { container = new CookieContainer(); handler = new HttpClientHandler { CookieContainer = container, UseCookies = true, // Service Layer might use self-signed certificates in test environments // Use cautiously in production! // ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }; // Conditionally ignore SSL errors based on options if (options.IgnoreSslErrors) { handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; // Log a warning when ignoring SSL errors Console.WriteLine("Warning: Ignoring SSL certificate errors. Use only for trusted development environments."); } var client = new HttpClient(handler); // Configure base address, timeout etc. from base class logic // The base constructor will call ConfigureHttpClient which uses _options return client; } /// /// Logs into SAP Business One Service Layer. /// /// The Company Database name. /// Service Layer username. /// Service Layer password. /// Cancellation token. /// True if login is successful, false otherwise. public async Task LoginAsync(string companyDB, string userName, string password, CancellationToken cancellationToken = default) { var loginPayload = new { CompanyDB = companyDB, UserName = userName, Password = password }; var loginUri = "/b1s/v2/Login"; // Assuming v1, adjust if needed try { // Use the internal HttpClient configured with CookieContainer var response = await _sapHttpClient.PostAsJsonAsync(loginUri, loginPayload, cancellationToken); if (!response.IsSuccessStatusCode) { // Log error details Console.WriteLine($"SAP B1 Login failed: {response.StatusCode}"); var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); Console.WriteLine($"Error details: {errorContent}"); return false; } // Extract session details from response (if needed directly) and cookies var sessionInfo = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); // Cookies are automatically handled by HttpClientHandler and CookieContainer // Optionally, store session details if needed for manual checks or info if (sessionInfo != null) { _b1SessionId = GetCookieValue("B1SESSION"); _routeId = GetCookieValue("ROUTEID"); // Important for sticky sessions in load-balanced environments _sessionTimeout = DateTime.UtcNow.AddSeconds(sessionInfo.SessionTimeout); Console.WriteLine($"SAP B1 Login successful. Session expires around: {_sessionTimeout.ToLocalTime()}"); return true; } Console.WriteLine("SAP B1 Login response could not be parsed."); return false; } catch (HttpRequestException ex) { Console.WriteLine($"HTTP Request Error during SAP B1 Login: {ex.Message}"); return false; } catch (JsonException ex) { Console.WriteLine($"JSON Parsing Error during SAP B1 Login: {ex.Message}"); return false; } catch (Exception ex) { Console.WriteLine($"Error during SAP B1 Login: {ex.Message}"); return false; } } /// /// Logs out from SAP Business One Service Layer. /// /// Cancellation token. /// Task representing the asynchronous operation. public async Task LogoutAsync(CancellationToken cancellationToken = default) { var logoutUri = "/b1s/v2/Logout"; try { // Send POST request to Logout endpoint. Cookies are sent automatically. var response = await _sapHttpClient.PostAsync(logoutUri, null, cancellationToken); response.EnsureSuccessStatusCode(); // Check if logout was successful // Clear local session state _b1SessionId = null; _routeId = null; // Optionally clear cookies from container if needed, though they become invalid ClearCookies(); Console.WriteLine("SAP B1 Logout successful."); } catch (HttpRequestException ex) { // Log or handle error, maybe session already expired Console.WriteLine($"HTTP Request Error during SAP B1 Logout: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"Error during SAP B1 Logout: {ex.Message}"); } } /// /// Checks if the current session is active based on timeout. /// public bool IsSessionActive() { return !string.IsNullOrEmpty(_b1SessionId) && DateTime.UtcNow < _sessionTimeout; } /// /// Discovers entities and their properties from the SAP B1 Service Layer metadata. /// /// Cancellation token. /// A list of discovered entity information. public async Task> DiscoverEntitiesAsync(CancellationToken cancellationToken = default) { // Ensure session is active before attempting discovery if (!IsSessionActive()) { // Or attempt login? For now, throw exception or return empty list Console.WriteLine("Error: Not logged into SAP B1 Service Layer. Cannot discover metadata."); // Consider throwing a specific exception return new List(); } var metadataUri = "/b1s/v2/$metadata"; // Adjust version (v1/v2) if necessary var entities = new List(); try { // Use the internal HttpClient which handles cookies var response = await _sapHttpClient.GetAsync(metadataUri, cancellationToken); response.EnsureSuccessStatusCode(); var xmlContent = await response.Content.ReadAsStringAsync(cancellationToken); var edmx = XDocument.Parse(xmlContent); // Define XML namespaces (adjust if different in your specific $metadata) XNamespace edm = "http://schemas.microsoft.com/ado/2009/11/edm"; XNamespace edmxNs = "http://schemas.microsoft.com/ado/2007/06/edmx"; // Older namespace sometimes used XNamespace defaultEdmNs = edmx.Root?.GetDefaultNamespace() ?? edm; // Get default namespace if present XNamespace dataServicesNs = edmx.Root?.Element(edmxNs + "DataServices")?.GetDefaultNamespace() ?? defaultEdmNs; // Find the Schema element (might be nested) var schemaElement = edmx.Descendants(defaultEdmNs + "Schema").FirstOrDefault() ?? edmx.Descendants(dataServicesNs + "Schema").FirstOrDefault(); if (schemaElement == null) { Console.WriteLine("Error: Could not find Schema element in $metadata response."); return entities; } // Find all EntityType elements within the Schema foreach (var entityTypeElement in schemaElement.Elements(defaultEdmNs + "EntityType")) { var entityInfo = new RestEntityInfo { Name = entityTypeElement.Attribute("Name")?.Value ?? string.Empty }; if (string.IsNullOrEmpty(entityInfo.Name)) continue; // Skip if name is missing // Find key properties for this entity type var keyProperties = new HashSet(); var keyElement = entityTypeElement.Element(defaultEdmNs + "Key"); if (keyElement != null) { foreach (var propRef in keyElement.Elements(defaultEdmNs + "PropertyRef")) { var keyName = propRef.Attribute("Name")?.Value; if (!string.IsNullOrEmpty(keyName)) keyProperties.Add(keyName); } } // Find properties for this entity type foreach (var propertyElement in entityTypeElement.Elements(defaultEdmNs + "Property")) { var propName = propertyElement.Attribute("Name")?.Value; if (string.IsNullOrEmpty(propName)) continue; var propInfo = new RestPropertyInfo { Name = propName, Type = propertyElement.Attribute("Type")?.Value ?? string.Empty, IsKey = keyProperties.Contains(propName) // Add extraction for Nullable, MaxLength etc. if needed from attributes }; entityInfo.Properties.Add(propInfo); } entities.Add(entityInfo); } } catch (HttpRequestException ex) { Console.WriteLine($"HTTP Request Error during metadata discovery: {ex.Message}"); // Handle error appropriately } catch (XmlException ex) { Console.WriteLine($"XML Parsing Error during metadata discovery: {ex.Message}"); // Handle error appropriately } catch (Exception ex) { Console.WriteLine($"Error during metadata discovery: {ex.Message}"); // Handle error appropriately } return entities; } // Helper to get cookie value private string? GetCookieValue(string cookieName) { if (_httpClient.BaseAddress == null) return null; var cookies = _cookieContainer.GetCookies(_httpClient.BaseAddress); return cookies[cookieName]?.Value; } // Helper to clear cookies private void ClearCookies() { if (_httpClient.BaseAddress == null) return; var cookies = _cookieContainer.GetCookies(_httpClient.BaseAddress); foreach (Cookie cookie in cookies) { cookie.Expired = true; } } // Ensure logout on dispose? Optional, depends on desired lifecycle management. // public override void Dispose() { ... LogoutAsync().Wait(); ... base.Dispose(); } // --- Nested class for deserializing Login response --- private class SapB1SessionInfo { public string SessionId { get; set; } = string.Empty; public string Version { get; set; } = string.Empty; public int SessionTimeout { get; set; } // In seconds } } }