using DataConnection.REST.Configuration; using DataConnection.REST.Interfaces; using DataConnection.REST.Models; // Added for metadata models using Microsoft.EntityFrameworkCore.Storage.Json; 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.Text.Json.Serialization; 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 v2, adjust if needed // --- Logging Aggiunto --- var absoluteUri = _sapHttpClient.BaseAddress != null ? new Uri(_sapHttpClient.BaseAddress, loginUri).ToString() : loginUri; Console.WriteLine($"--- SAP B1 Login Attempt ---"); Console.WriteLine($"Target URL: {absoluteUri}"); Console.WriteLine($"Payload: CompanyDB='{companyDB}', UserName='{userName}', Password='{(string.IsNullOrEmpty(password) ? "" : password.Substring(0, Math.Min(1, password.Length)) + "...")}'"); // Non loggare la password completa in produzione! // Verifica se IgnoreSslErrors è attivo (preso dalle opzioni passate al costruttore) Console.WriteLine($"Ignore SSL Errors: {_options.IgnoreSslErrors}"); // --- Fine Logging --- try { // Use the internal HttpClient configured with CookieContainer // Pass the loginPayload object directly, PostAsJsonAsync handles serialization 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}"); Console.WriteLine($"--- End SAP B1 Login Attempt (Failed) ---"); // Log fine tentativo 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()}"); Console.WriteLine($"--- End SAP B1 Login Attempt (Success) ---"); // Log fine tentativo 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}"); // Logga dettagli sull'eccezione interna se presente (spesso utile per problemi SSL) if (ex.InnerException != null) { Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); } Console.WriteLine($"--- End SAP B1 Login Attempt (Exception) ---"); // Log fine tentativo return false; } catch (JsonException ex) { Console.WriteLine($"JSON Parsing Error during SAP B1 Login: {ex.Message}"); Console.WriteLine($"--- End SAP B1 Login Attempt (JsonException) ---"); // Log fine tentativo return false; } catch (Exception ex) { Console.WriteLine($"Error during SAP B1 Login: {ex.Message}"); Console.WriteLine($"--- End SAP B1 Login Attempt (Exception) ---"); // Log fine tentativo return false; } // finally // Aggiunto per sicurezza - Rimosso perchè non necessario qui // { // // Potrebbe essere utile loggare qui se necessario // } } /// /// 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; } /// /// Creates a new entity in SAP Business One using the Service Layer. /// /// The name of the entity to create (e.g., "Items", "BusinessPartners"). /// The data for the new entity as key-value pairs. /// Cancellation token. /// The created entity data or null if creation failed. public override async Task?> CreateEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default) { // Ensure session is active before attempting creation if (!IsSessionActive()) { Console.WriteLine("Error: Not logged into SAP B1 Service Layer. Cannot create entity."); return null; } // SAP B1 Service Layer typically uses plural entity names for endpoints var createUri = $"/b1s/v2/{entityName}"; try { Console.WriteLine($"--- SAP B1 Entity Creation Attempt ---"); Console.WriteLine($"Entity: {entityName}"); Console.WriteLine($"Target URL: {(_sapHttpClient.BaseAddress != null ? new Uri(_sapHttpClient.BaseAddress, createUri) : createUri)}"); Console.WriteLine($"Data: {System.Text.Json.JsonSerializer.Serialize(entityData)}"); var response = await _sapHttpClient.PostAsJsonAsync(createUri, entityData, cancellationToken); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); Console.WriteLine($"SAP B1 Entity Creation failed: {response.StatusCode}"); Console.WriteLine($"Error details: {errorContent}"); Console.WriteLine($"--- End SAP B1 Entity Creation Attempt (Failed) ---"); return null; } var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); Console.WriteLine($"SAP B1 Entity Creation successful"); Console.WriteLine($"Response: {responseContent}"); Console.WriteLine($"--- End SAP B1 Entity Creation Attempt (Success) ---"); if (string.IsNullOrEmpty(responseContent)) return entityData; // Return original data if no response content return System.Text.Json.JsonSerializer.Deserialize>(responseContent); } catch (HttpRequestException ex) { Console.WriteLine($"HTTP Request Error during SAP B1 entity creation: {ex.Message}"); if (ex.InnerException != null) { Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); } Console.WriteLine($"--- End SAP B1 Entity Creation Attempt (Exception) ---"); return null; } catch (JsonException ex) { Console.WriteLine($"JSON Parsing Error during SAP B1 entity creation: {ex.Message}"); Console.WriteLine($"--- End SAP B1 Entity Creation Attempt (JsonException) ---"); return null; } catch (Exception ex) { Console.WriteLine($"Error during SAP B1 entity creation: {ex.Message}"); Console.WriteLine($"--- End SAP B1 Entity Creation Attempt (Exception) ---"); return null; } } /// /// Discovers a list of available entities from SAP B1 Service Layer without detailed field information. /// /// Cancellation token /// A list of discovered entity summaries public async Task> DiscoverEntitySummariesAsync(CancellationToken cancellationToken = default) { if (!IsSessionActive()) { Console.WriteLine("Error: Not logged into SAP B1 Service Layer. Cannot discover metadata."); return new List(); } var entities = new List(); var metadataUri = "/b1s/v2/$metadata"; try { var response = await _sapHttpClient.GetAsync(metadataUri, cancellationToken); response.EnsureSuccessStatusCode(); var xmlContent = await response.Content.ReadAsStringAsync(cancellationToken); var edmx = XDocument.Parse(xmlContent); // Define XML namespaces XNamespace edm = "http://schemas.microsoft.com/ado/2009/11/edm"; XNamespace edmxNs = "http://schemas.microsoft.com/ado/2007/06/edmx"; XNamespace defaultEdmNs = edmx.Root?.GetDefaultNamespace() ?? edm; XNamespace dataServicesNs = edmx.Root?.Element(edmxNs + "DataServices")?.GetDefaultNamespace() ?? defaultEdmNs; 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 entityName = entityTypeElement.Attribute("Name")?.Value; if (string.IsNullOrEmpty(entityName)) continue; var entitySummary = new RestEntitySummary { Name = entityName, Label = entityName, // SAP B1 typically doesn't have separate labels IsCustom = entityName.StartsWith("U_", StringComparison.OrdinalIgnoreCase), EntityType = "EntityType" }; entities.Add(entitySummary); } } catch (HttpRequestException ex) { Console.WriteLine($"HTTP Request Error during SAP B1 metadata discovery: {ex.Message}"); } catch (XmlException ex) { Console.WriteLine($"XML Parsing Error during SAP B1 metadata discovery: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"Error during SAP B1 metadata discovery: {ex.Message}"); } return entities.OrderBy(e => e.Name).ToList(); } /// /// Discovers detailed information for a specific entity including all its properties. /// /// The name of the entity to get details for /// Cancellation token /// Detailed entity information or null if not found public async Task DiscoverEntityDetailsAsync(string entityName, CancellationToken cancellationToken = default) { if (!IsSessionActive()) { Console.WriteLine("Error: Not logged into SAP B1 Service Layer. Cannot discover entity details."); return null; } var metadataUri = "/b1s/v2/$metadata"; try { var response = await _sapHttpClient.GetAsync(metadataUri, cancellationToken); response.EnsureSuccessStatusCode(); var xmlContent = await response.Content.ReadAsStringAsync(cancellationToken); var edmx = XDocument.Parse(xmlContent); // Define XML namespaces XNamespace edm = "http://schemas.microsoft.com/ado/2009/11/edm"; XNamespace edmxNs = "http://schemas.microsoft.com/ado/2007/06/edmx"; XNamespace defaultEdmNs = edmx.Root?.GetDefaultNamespace() ?? edm; XNamespace dataServicesNs = edmx.Root?.Element(edmxNs + "DataServices")?.GetDefaultNamespace() ?? defaultEdmNs; 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 null; } // Find the specific EntityType element var entityTypeElement = schemaElement.Elements(defaultEdmNs + "EntityType") .FirstOrDefault(e => e.Attribute("Name")?.Value == entityName); if (entityTypeElement == null) { Console.WriteLine($"Error: Could not find EntityType '{entityName}' in $metadata response."); return null; } var entityInfo = new RestEntityInfo { Name = entityName }; // 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), IsRequired = propertyElement.Attribute("Nullable")?.Value?.ToLower() == "false", MaxLength = int.TryParse(propertyElement.Attribute("MaxLength")?.Value, out var maxLen) ? maxLen : null }; entityInfo.Properties.Add(propInfo); } return entityInfo; } catch (HttpRequestException ex) { Console.WriteLine($"HTTP Request Error during SAP B1 entity details discovery: {ex.Message}"); } catch (XmlException ex) { Console.WriteLine($"XML Parsing Error during SAP B1 entity details discovery: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"Error during SAP B1 entity details discovery: {ex.Message}"); } return null; } /// /// Authenticates with SAP B1 Service Layer using the credentials from options. /// /// Cancellation token /// True if authentication is successful public override async Task AuthenticateAsync(CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(_options.Username) || string.IsNullOrEmpty(_options.Password)) { Console.WriteLine("SAP B1 authentication requires username and password in options"); return false; } // For SAP B1, we also need the company database name // We'll check multiple fields for the company DB name var companyDB = !string.IsNullOrEmpty(_options.ApiKey) ? _options.ApiKey : !string.IsNullOrEmpty(_options.AuthToken) ? _options.AuthToken : "SBODEMOUS"; // Default fallback Console.WriteLine($"Using SAP B1 credentials - CompanyDB: {companyDB}, Username: {_options.Username}"); return await LoginAsync(companyDB, _options.Username, _options.Password, cancellationToken); } // 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 } } }