diff --git a/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs b/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs index af3614b73..a1a4510b5 100644 --- a/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs +++ b/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs @@ -12,21 +12,39 @@ using static GenesisCordonelInterface.API.PublicModels; namespace GenesisCordonelInterface.API { /// - /// Outside-facing facade for Genesis Cordonel Interface. - /// Exposes only selected operations intended for external callers. + /// Public-facing facade for external applications integrating with + /// Genesis Cordonel Interface. /// + /// + /// This class exposes a simplified and controlled API for external callers. + /// It validates public input, maps public request models to internal models, + /// forwards operations to the internal GCI implementation and exposes status + /// notifications for meter batch changes. + /// + /// This layer should stay thin. Business logic and meter communication are + /// handled by . + /// public class InterfaceOutsideToGCI { + /// + /// Internal GCI implementation used by this public facade. + /// public readonly InterfaceGCIToLaatzen _innerMeterAPI; + /// + /// Occurs when meter batch status information changes. + /// public event Action> MeterBatchStatusChanged; + /// + /// Initializes a new instance of the public GCI facade. + /// public InterfaceOutsideToGCI() { _innerMeterAPI = new InterfaceGCIToLaatzen(); } - // + // Laatzen ToolBox actions #region ================================== PORT DETECTION ================================== @@ -66,6 +84,20 @@ namespace GenesisCordonelInterface.API #region ================================== INIT/UPDATE/GET slot ================================== + /// + /// Initializes a meter slot using the provided slot configuration. + /// + /// + /// Slot initialization request containing slot id, configuration source, + /// password source, request port and streaming port. + /// + /// Cancellation token used to cancel the asynchronous operation. + /// + /// Result describing whether the slot was created, updated, already existed or failed. + /// + /// + /// Thrown when is null. + /// public async Task InitSlotAsync( GciInitSlotRequest request, CancellationToken token = default) @@ -86,6 +118,20 @@ namespace GenesisCordonelInterface.API return result; } + /// + /// Updates configuration of an existing meter slot. + /// + /// + /// Slot configuration request containing updated configuration source, + /// password source and port settings. + /// + /// Cancellation token used to cancel the asynchronous operation. + /// + /// Result describing whether the slot update succeeded or failed. + /// + /// + /// Thrown when is null. + /// public async Task UpdateSlotAsync( GciInitSlotRequest request, CancellationToken token = default) @@ -106,6 +152,18 @@ namespace GenesisCordonelInterface.API return result; } + /// + /// Gets information about one meter slot. + /// + /// Slot id to query. Must be greater than zero. + /// Cancellation token used to cancel the asynchronous operation. + /// + /// Slot information including existence, connection state, login state, + /// PCB id and configured communication ports. + /// + /// + /// Thrown when is invalid. + /// public async Task GetSlotAsync( int slotId, CancellationToken token = default) @@ -118,6 +176,13 @@ namespace GenesisCordonelInterface.API return result; } + /// + /// Gets information about all currently initialized meter slots. + /// + /// Cancellation token used to cancel the asynchronous operation. + /// + /// Collection of slot information records for all known meters. + /// public async Task GetAllSlotsAsync( CancellationToken token = default) { @@ -126,6 +191,17 @@ namespace GenesisCordonelInterface.API return result; } + /// + /// Cleans one meter slot and releases its runtime resources. + /// + /// Slot id to clean. Must be greater than zero. + /// Cancellation token used to cancel the asynchronous operation. + /// + /// Result describing whether the slot cleanup succeeded or failed. + /// + /// + /// Thrown when is invalid. + /// public async Task CleanSlotAsync( int slot, CancellationToken token = default) @@ -140,6 +216,13 @@ namespace GenesisCordonelInterface.API return result; } + /// + /// Cleans all initialized meter slots and releases related runtime resources. + /// + /// Cancellation token used to cancel the asynchronous operation. + /// + /// Result describing whether cleanup of all slots succeeded or failed. + /// public async Task CleanAllSlotsAsync( CancellationToken token = default) { @@ -152,6 +235,17 @@ namespace GenesisCordonelInterface.API #endregion #region ================================== PASSWORD ================================== + + /// + /// Sets runtime password for the meter assigned to the specified slot. + /// + /// Slot id. Must be greater than zero. + /// Password to assign to the meter. + /// Cancellation token used to cancel the asynchronous operation. + /// Result containing password update status. + /// + /// Thrown when slot id is invalid or password is empty. + /// public async Task SetPasswordAsync( int slot, string password, @@ -173,6 +267,16 @@ namespace GenesisCordonelInterface.API #endregion #region ================================== LOGIN ================================== + + /// + /// Logs in to the meter assigned to the specified slot. + /// + /// Slot id. Must be greater than zero. + /// Cancellation token used to cancel the asynchronous operation. + /// Login result containing login state and status message. + /// + /// Thrown when is invalid. + /// public async Task LoginOneSlotAsync( int slot, CancellationToken token = default) @@ -189,6 +293,15 @@ namespace GenesisCordonelInterface.API #region ================================== CONNECTION ================================== + /// + /// Connects the meter assigned to the specified slot. + /// + /// Slot id. Must be greater than zero. + /// Cancellation token used to cancel the asynchronous operation. + /// Connection result containing connection state and status message. + /// + /// Thrown when is invalid. + /// public async Task ConnectOneSlotAsync( int slot, CancellationToken token = default) @@ -203,6 +316,15 @@ namespace GenesisCordonelInterface.API return result; } + /// + /// Disconnects the meter assigned to the specified slot. + /// + /// Slot id. Must be greater than zero. + /// Cancellation token used to cancel the asynchronous operation. + /// Disconnect result containing final connection state and status message. + /// + /// Thrown when is invalid. + /// public async Task DisconnectAsync( int slot, CancellationToken token = default) @@ -219,6 +341,16 @@ namespace GenesisCordonelInterface.API #endregion #region ================================== PCB ================================== + + /// + /// Reads PCB identifier from the meter assigned to the specified slot. + /// + /// Slot id. Must be greater than zero. + /// Cancellation token used to cancel the asynchronous operation. + /// Result containing PCB id and read status. + /// + /// Thrown when is invalid. + /// public async Task GetPcbIdAsync( int slot, CancellationToken token = default) @@ -234,6 +366,18 @@ namespace GenesisCordonelInterface.API #region ================================== READ ================================== + /// + /// Reads a firmware register value from the meter assigned to the specified slot. + /// + /// Slot id. Must be greater than zero. + /// Register identifier to read. + /// Cancellation token used to cancel the asynchronous operation. + /// + /// Register read result containing raw register value and operation status. + /// + /// + /// Thrown when slot id or register name is invalid. + /// public async Task ReadRegisterAsync( int slot, string registerName, @@ -258,6 +402,25 @@ namespace GenesisCordonelInterface.API #region ================================== WRITE ================================== + /// + /// Writes a value to a firmware register. + /// + /// Slot id. Must be greater than zero. + /// Register identifier to write. + /// Value to write. + /// + /// Indicates whether configuration should be permanently stored. + /// + /// + /// Indicates whether firmware system state should be refreshed after write. + /// + /// Cancellation token used to cancel the asynchronous operation. + /// + /// Register write result describing write status. + /// + /// + /// Thrown when slot id or register name is invalid. + /// public async Task WriteRegisterAsync( int slot, string registerName, @@ -290,6 +453,18 @@ namespace GenesisCordonelInterface.API #region ================================== Password ================================== + /// + /// Updates meter password for the specified slot. + /// + /// Slot id. Must be greater than zero. + /// New password. + /// Cancellation token used to cancel the asynchronous operation. + /// + /// Password update result. + /// + /// + /// Thrown when slot id or password is invalid. + /// public async Task SetMeterPasswordAsync( int slot, string password, @@ -313,16 +488,36 @@ namespace GenesisCordonelInterface.API #region ================================== DEBUG STATUS ================================== + /// + /// Gets runtime diagnostic information for all active workers. + /// + /// + /// Collection containing worker state, queue information, + /// current operation and activity timestamps. + /// public List GetWorkerDebugStatuses() { return _innerMeterAPI.GetWorkerDebugStatuses(); } + /// + /// Gets runtime diagnostic information for all meter slots. + /// + /// + /// Collection containing slot state, connection state, + /// selected state and communication configuration. + /// public List GetMeterBatchDebugStatuses() { return _innerMeterAPI.GetMeterBatchDebugStatuses(); } + /// + /// Raises meter batch status change notification. + /// + /// + /// Intended to notify external consumers after changes in meter state. + /// public void RaiseMeterBatchStatusChanged() { var statuses = GetMeterBatchDebugStatuses(); @@ -332,121 +527,54 @@ namespace GenesisCordonelInterface.API handler(statuses); } - // ---------------------------------------------------- #endregion #region ================================== SLOT SELECTION ================================== - // ---------------------------------------------------- + /// + /// Sets selection state for a slot. + /// + /// Slot id. + /// Selection state. public void SetSlotSelected(int slot, bool selected) { _innerMeterAPI.SetSlotSelected(slot, selected); RaiseMeterBatchStatusChanged(); } + /// + /// Determines whether the specified slot is selected. + /// + /// Slot id. + /// + /// True if slot is selected; otherwise false. + /// public bool IsSlotSelected(int slot) { return _innerMeterAPI.IsSlotSelected(slot); } + /// + /// Gets all selected slot identifiers. + /// + /// + /// Ordered collection of selected slot ids. + /// public List GetSelectedSlots() { return _innerMeterAPI.GetSelectedSlots(); } - // ---------------------------------------------------- - #endregion - - #region ================================== SLOT PORT CONFIG ================================== - // ---------------------------------------------------- - - /*public void SetSlotRequestPort(int slot, string portName) - { - lock (_portLock) - { - if (string.IsNullOrWhiteSpace(portName)) - { - _requestPorts.Remove(slot); - } - else - { - _requestPorts[slot] = new GciPortConfig - { - PortName = portName, - Type = "Serial" - }; - } - } - - RaiseMeterBatchStatusChanged(); - } - - public void SetSlotStreamingPort(int slot, string portName) - { - lock (_portLock) - { - if (string.IsNullOrWhiteSpace(portName)) - { - _streamingPorts.Remove(slot); - } - else - { - _streamingPorts[slot] = new GciPortConfig - { - PortName = portName, - Type = "Serial" - }; - } - } - - RaiseMeterBatchStatusChanged(); - } - - public GciPortConfig? GetSlotRequestPort(int slot) - { - lock (_portLock) - { - GciPortConfig port; - if (_requestPorts.TryGetValue(slot, out port)) - return port; - - return null; - } - } - - public GciPortConfig? GetSlotStreamingPort(int slot) - { - lock (_portLock) - { - GciPortConfig port; - if (_streamingPorts.TryGetValue(slot, out port)) - return port; - - return null; - } - }*/ - - // ---------------------------------------------------- - #endregion - - #region ================================== METER BATCH SETUP ================================== - - public void ReloadSlotSetup() - { - _innerMeterAPI.ReloadSlotSetup(); - RaiseMeterBatchStatusChanged(); - } - - public void SaveSlotSetup(List data) - { - _innerMeterAPI.SaveSlotSetup(data); - RaiseMeterBatchStatusChanged(); - } - #endregion #region ================================== Register names ================================== + /// + /// Gets all available firmware register identifiers. + /// + /// + /// Ordered collection of register names. + /// public List GetAllRegisterNames() { return _innerMeterAPI.GetAllRegisterNames(); @@ -454,7 +582,7 @@ namespace GenesisCordonelInterface.API #endregion - // Preadjustment + // Laatzen Preadjustment processes #region ================================== PreAdjustment ================================== public Task PreAdjustment_DetectAsync( diff --git a/GenesisCordonelInterface/Config/gci_config.json b/GenesisCordonelInterface/Config/gci_config.json new file mode 100644 index 000000000..617517733 --- /dev/null +++ b/GenesisCordonelInterface/Config/gci_config.json @@ -0,0 +1,35 @@ +{ + "_Comment": "GCI DataStorage configuration", + + "DataStorage": { + + "MeterLoginPasswords": { + + "___Documentation___": { + "Description": "Reads login passwords for meters by PCB ID", + "Type": "Supported: LocalDatabase, RemoteDatabase, LocalCsv, LocalJson, RestApi", + "DataSource": "Database connection string", + "QueryTemplate": "QUERYPARAM is placeholder for runtime value. Example: WHERE [PcbId]=QUERYPARAM -> PCB ID provided during GetPasswordAsync()." + }, + + "Name": "MeterLoginPasswords", + "Type": "LocalDatabase", + "DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;", + "QueryTemplate": "SELECT [Password] FROM [dbo].[SkeletonKeys] WHERE [PcbId] = QUERYPARAM" + }, + + "PreAdjustmentCalibrationParams": { + + "___Documentation___": { + "Description": "Reads pre-adjustment values from CSV", + "DataSource": "Relative path resolved from application directory", + "QueryTemplate": "CSV/JSON syntax: SELECT [ReturnColumn] WHERE [MatchColumn]=QUERYPARAM or SELECT COLUMN(1) WHERE COLUMN(0)=QUERYPARAM" + }, + + "Name": "PreAdjustmentCalibrationParams", + "Type": "LocalCsv", + "DataSource": "Data\\preadjustment_params.csv", + "QueryTemplate": "SELECT [Offset] WHERE [PcbId] = QUERYPARAM" + } + } +} diff --git a/GenesisCordonelInterface/Core/DataStorage/Config/GciConfig.cs b/GenesisCordonelInterface/Core/DataStorage/Config/GciConfig.cs new file mode 100644 index 000000000..c4cf686c0 --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Config/GciConfig.cs @@ -0,0 +1,32 @@ +namespace GenesisCordonelInterface.Core.DataStorage.Config +{ + /// + /// Root GCI configuration object loaded from gci_config.json. + /// + /// This class represents the top-level configuration structure + /// and serves as an entry point for all configurable GCI features. + /// + /// Example: + /// + /// { + /// "DataStorage": + /// { + /// ... + /// } + /// } + /// + public class GciConfig + { + /// + /// Data storage configuration section. + /// + /// Contains definitions for all configured readers and writers, + /// such as: + /// + /// - MeterLoginPasswords + /// - PreAdjustmentCalibrationParams + /// - future storage providers + /// + public GciDataStorageConfig DataStorage { get; set; } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Config/GciConfigLoader.cs b/GenesisCordonelInterface/Core/DataStorage/Config/GciConfigLoader.cs new file mode 100644 index 000000000..b88db0d3b --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Config/GciConfigLoader.cs @@ -0,0 +1,177 @@ +using System; +using System.IO; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models; +using Newtonsoft.Json; + +namespace GenesisCordonelInterface.Core.DataStorage.Config +{ + /// + /// Loads GCI configuration from gci_config.json. + /// + /// Responsibilities: + /// - Locate configuration file + /// - Deserialize JSON into strongly typed objects + /// - Normalize relative file paths + /// - Preserve database connection strings and REST URLs + /// + /// Example: + /// + /// Config/ + /// gci_config.json + /// + /// Data/ + /// meter_passwords.csv + /// + /// Relative paths: + /// Data\file.csv + /// + /// become: + /// + /// C:\App\bin\Debug\Data\file.csv + /// + /// Database sources remain unchanged: + /// + /// Server=(localdb)\MojaDB;Database=... + /// + public static class GciConfigLoader + { + /// + /// Loads default GCI configuration from: + /// + /// Config\gci_config.json + /// + /// + /// Loaded GCI configuration. + /// + public static GciConfig LoadDefault() + { + string baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + + string configPath = Path.Combine( + baseDirectory, + "Config", + "gci_config.json"); + + return Load(configPath, baseDirectory); + } + + /// + /// Loads GCI configuration from specified file. + /// + /// + /// Path to configuration json file. + /// + /// + /// Base directory used for resolving relative paths. + /// + /// + /// Loaded configuration object. + /// + public static GciConfig Load( + string configPath, + string baseDirectory = null) + { + if (string.IsNullOrWhiteSpace(configPath)) + throw new ArgumentException( + "Config path must not be empty.", + nameof(configPath)); + + if (!File.Exists(configPath)) + throw new FileNotFoundException( + "GCI config file was not found.", + configPath); + + string json = File.ReadAllText(configPath); + + GciConfig config = + JsonConvert.DeserializeObject(json); + + if (config == null) + throw new InvalidOperationException( + "GCI config could not be loaded."); + + NormalizeDataStoragePaths( + config, + baseDirectory ?? Path.GetDirectoryName(configPath)); + + return config; + } + + /// + /// Normalizes paths for all configured data storage entries. + /// + /// Converts relative file paths into absolute paths. + /// Database connection strings remain untouched. + /// + /// + /// Loaded configuration object. + /// + /// + /// Base path used for relative resolution. + /// + private static void NormalizeDataStoragePaths( + GciConfig config, + string baseDirectory) + { + if (config.DataStorage == null) + return; + + NormalizePath( + config.DataStorage.MeterLoginPasswords, + baseDirectory); + + NormalizePath( + config.DataStorage.PreAdjustmentCalibrationParams, + baseDirectory); + } + + /// + /// Converts relative file paths into absolute paths. + /// + /// Applies only to: + /// - LocalCsv + /// - RemoteCsv + /// - LocalJson + /// - RemoteJson + /// + /// Does not modify: + /// - Database connection strings + /// - REST URLs + /// + /// + /// Storage configuration. + /// + /// + /// Base path for resolution. + /// + private static void NormalizePath( + DataStorageConfig storageConfig, + string baseDirectory) + { + if (storageConfig == null) + return; + + if (string.IsNullOrWhiteSpace(storageConfig.DataSource)) + return; + + // Database connection strings and URLs + // must never be treated as file paths. + if (storageConfig.Type == DataStorageType.LocalDatabase || + storageConfig.Type == DataStorageType.RemoteDatabase || + storageConfig.Type == DataStorageType.RestApi) + { + return; + } + + if (Path.IsPathRooted(storageConfig.DataSource)) + return; + + storageConfig.DataSource = + Path.GetFullPath( + Path.Combine( + baseDirectory, + storageConfig.DataSource)); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Config/GciDataStorageConfig.cs b/GenesisCordonelInterface/Core/DataStorage/Config/GciDataStorageConfig.cs new file mode 100644 index 000000000..526fa2e24 --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Config/GciDataStorageConfig.cs @@ -0,0 +1,30 @@ +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models; + +namespace GenesisCordonelInterface.Core.DataStorage.Config +{ + /// + /// Contains all configured GCI data storage sources. + /// + /// Each property represents one logical use-case + /// and points to its storage configuration. + /// + /// Example: + /// MeterLoginPasswords + /// -> SQL database + /// + /// PreAdjustmentCalibrationParams + /// -> CSV file + /// + public class GciDataStorageConfig + { + /// + /// Configuration for meter login password lookup. + /// + public DataStorageConfig MeterLoginPasswords { get; set; } + + /// + /// Configuration for pre-adjustment calibration data lookup. + /// + public DataStorageConfig PreAdjustmentCalibrationParams { get; set; } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Contracts/IDataStorageReader..cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Contracts/IDataStorageReader..cs new file mode 100644 index 000000000..342f37170 --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Contracts/IDataStorageReader..cs @@ -0,0 +1,49 @@ +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts +{ + /// + /// Defines common contract for all GCI data storage readers. + /// + /// Implementations can read data from different storage types: + /// - SQL database + /// - CSV file + /// - JSON file + /// - REST API + /// + /// Consumers should use this interface instead of concrete readers. + /// + public interface IDataStorageReader + { + /// + /// Reads data from configured storage using provided query. + /// + /// + /// Query object containing lookup parameters. + /// + /// + /// Storage-specific result object. + /// + object GetData(Models.DataQuery query); + + /// + /// Tests whether configured data source is accessible. + /// + /// + /// Enables detailed diagnostic output. + /// + /// + /// Diagnostic result of source test. + /// + Models.ReaderDiagnosticResult TestSource(bool enableDiagnostics); + + /// + /// Tests whether configured query is valid for the data source. + /// + /// + /// Enables detailed diagnostic output. + /// + /// + /// Diagnostic result of query test. + /// + Models.ReaderDiagnosticResult TestQuery(bool enableDiagnostics); + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/DataStorageReaderFactory.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/DataStorageReaderFactory.cs new file mode 100644 index 000000000..0613a1153 --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/DataStorageReaderFactory.cs @@ -0,0 +1,94 @@ +using System; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts; + +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common +{ + /// + /// Factory responsible for creating appropriate + /// data storage reader implementations. + /// + /// The reader type is selected according to: + /// + /// DataStorageConfig.Type + /// + /// Example: + /// + /// LocalDatabase + /// -> DatabaseDataStorageReader + /// + /// LocalCsv + /// -> CsvDataStorageReader + /// + /// LocalJson + /// -> JsonDataStorageReader + /// + /// RestApi + /// -> RestApiDataStorageReader + /// + /// This factory hides implementation details from + /// higher layers and keeps consumers independent + /// of storage technology. + /// + /// Trace: + /// + /// MeterLoginPasswordReader + /// -> DataStorageReaderFactory.Create() + /// -> DatabaseDataStorageReader + /// -> CsvDataStorageReader + /// -> JsonDataStorageReader + /// -> RestApiDataStorageReader + /// + public static class DataStorageReaderFactory + { + /// + /// Creates appropriate reader implementation + /// according to storage configuration. + /// + /// + /// Data storage configuration loaded from gci_config.json. + /// + /// + /// Configured storage reader implementation. + /// + /// + /// Configuration is null. + /// + /// + /// Unsupported storage type. + /// + public static IDataStorageReader Create( + DataStorageConfig config) + { + if (config == null) + throw new ArgumentNullException(nameof(config)); + + switch (config.Type) + { + case DataStorageType.RemoteDatabase: + case DataStorageType.LocalDatabase: + + return new Providers.DatabaseDataStorageReader(config); + + case DataStorageType.RemoteCsv: + case DataStorageType.LocalCsv: + + return new Providers.CsvDataStorageReader(config); + + case DataStorageType.RemoteJson: + case DataStorageType.LocalJson: + + return new Providers.JsonDataStorageReader(config); + + case DataStorageType.RestApi: + + return new Providers.RestApiDataStorageReader(config); + + default: + + throw new NotSupportedException( + $"Unsupported DataStorageType: '{config.Type}'"); + } + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataQuery.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataQuery.cs new file mode 100644 index 000000000..47762bb88 --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataQuery.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; + +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models +{ + /// + /// Represents query input for data storage readers. + /// + /// Query parameters are used as lookup values + /// during storage search operations. + /// + /// A single parameter: + /// + /// 231630279 + /// + /// executes one lookup. + /// + /// Multiple parameters: + /// + /// 231630279 + /// 231630243 + /// 231630148 + /// + /// can execute batch operations. + /// + /// Example: + /// + /// QueryTemplate: + /// + /// SELECT [Password] + /// WHERE [PcbId]=QUERYPARAM + /// + /// Query: + /// + /// QueryParams: + /// 231630279 + /// + /// Result: + /// + /// Password belonging to 231630279. + /// + public class DataQuery + { + /// + /// Collection of query values. + /// + /// Single item: + /// + /// QueryParams[0] + /// + /// represents one lookup. + /// + /// Multiple items may be processed + /// as batch requests. + /// + public List QueryParams { get; } + = new List(); + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageConfig.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageConfig.cs new file mode 100644 index 000000000..2bc89bc64 --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageConfig.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models +{ + /// + /// Configuration for one storage source. + /// Loaded from gci_config.json + /// + public class DataStorageConfig + { + /// + /// Logical storage name. + /// Example: + /// MeterLoginPasswords + /// CalibrationParameters + /// + public string Name { get; set; } + + /// + /// Storage implementation type. + /// + public DataStorageType Type { get; set; } + + /// + /// Connection string, + /// file path, + /// URL etc. + /// + public string DataSource { get; set; } + + /// + /// Query template. + /// Example: + /// + /// SQL: + /// SELECT Password + /// FROM Passwords + /// WHERE PcbId=QUERYPARAM + /// + /// CSV: + /// SELECT [Password] + /// WHERE [PcbId]=QUERYPARAM + /// + public string QueryTemplate { get; set; } + } +} diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageType.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageType.cs new file mode 100644 index 000000000..a28659ae9 --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageType.cs @@ -0,0 +1,79 @@ +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models +{ + /// + /// Identifies supported data storage implementations. + /// + /// The type determines which reader implementation + /// will be created by DataStorageReaderFactory. + /// + /// Example: + /// + /// LocalDatabase + /// -> DatabaseDataStorageReader + /// + /// LocalCsv + /// -> CsvDataStorageReader + /// + /// RestApi + /// -> RestApiDataStorageReader + /// + /// Source: + /// + /// gci_config.json + /// + /// Example: + /// + /// "Type":"LocalDatabase" + /// + public enum DataStorageType + { + /// + /// REST API endpoint. + /// + /// Example: + /// + /// https://api.company.com/passwords + /// + RestApi, + + /// + /// Database located on remote server. + /// + /// Example: + /// + /// SQL server on network machine. + /// + RemoteDatabase, + + /// + /// Database located on local machine. + /// + /// Example: + /// + /// SQL LocalDB + /// SQLite + /// local SQL Server instance + /// + LocalDatabase, + + /// + /// JSON source stored remotely. + /// + RemoteJson, + + /// + /// JSON file stored locally. + /// + LocalJson, + + /// + /// CSV source stored remotely. + /// + RemoteCsv, + + /// + /// CSV file stored locally. + /// + LocalCsv + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DatabaseSearchResult.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DatabaseSearchResult.cs new file mode 100644 index 000000000..2f69501ee --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DatabaseSearchResult.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models +{ + public class DatabaseSearchResult + { + public bool Found { get; set; } + + public string Query { get; set; } + + public Dictionary Values { get; set; } + + public DatabaseSearchResult() + { + Values = new Dictionary(); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/ReaderDiagnosticResult.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/ReaderDiagnosticResult.cs new file mode 100644 index 000000000..b456631c2 --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/ReaderDiagnosticResult.cs @@ -0,0 +1,141 @@ +using System.Collections.Generic; +using System.Text; + +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models +{ + /// + /// Represents diagnostic result of data storage operations. + /// + /// Used for: + /// - source validation + /// - query validation + /// - connection tests + /// - detailed diagnostic logging + /// + /// Example: + /// + /// TestSource() + /// -> connection successful + /// + /// TestQuery() + /// -> column validation + /// + /// Can also carry additional payload data. + /// + public class ReaderDiagnosticResult + { + /// + /// Indicates whether operation completed successfully. + /// + public bool Success { get; set; } + + /// + /// Human readable summary message. + /// + /// Example: + /// + /// "Connection successful." + /// + /// or: + /// + /// "Column not found." + /// + public string Message { get; set; } + + /// + /// Detailed diagnostic output lines. + /// + /// Used for debugging and troubleshooting. + /// + public List Diagnostics { get; set; } + + /// + /// Optional operation payload. + /// + /// Example: + /// + /// loaded CSV lines + /// SQL test value + /// parsed content + /// + public object Data { get; set; } + + /// + /// Creates empty diagnostic result. + /// + public ReaderDiagnosticResult() + { + Diagnostics = new List(); + } + + /// + /// Creates successful diagnostic result. + /// + /// + /// Success message. + /// + /// + /// Successful result object. + /// + public static ReaderDiagnosticResult SuccessResult( + string message = "OK") + { + return new ReaderDiagnosticResult + { + Success = true, + Message = message + }; + } + + /// + /// Creates failed diagnostic result. + /// + /// + /// Failure message. + /// + /// + /// Failed result object. + /// + public static ReaderDiagnosticResult Failure( + string message) + { + return new ReaderDiagnosticResult + { + Success = false, + Message = message + }; + } + + /// + /// Converts diagnostic information into display text. + /// + /// Output contains: + /// + /// diagnostic lines + /// + + /// summary message + /// + /// + /// Formatted text representation. + /// + public string ToDisplayDiag() + { + StringBuilder sb = new StringBuilder(); + + if (Diagnostics != null && + Diagnostics.Count > 0) + { + foreach (string line in Diagnostics) + sb.AppendLine(line); + + if (!string.IsNullOrWhiteSpace(Message)) + sb.AppendLine(); + } + + if (!string.IsNullOrWhiteSpace(Message)) + sb.AppendLine(Message); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/CsvDataStorageReader.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/CsvDataStorageReader.cs new file mode 100644 index 000000000..004d6414a --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/CsvDataStorageReader.cs @@ -0,0 +1,394 @@ +using System; +using System.Collections.Generic; +using System.IO; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Searching; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts; + +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers +{ + /// + /// Reads and searches CSV based data sources. + /// + /// Supported query syntax: + /// SELECT [ReturnColumn] WHERE [MatchColumn] = QUERYPARAM + /// + /// Example: + /// SELECT [Password] WHERE [PcbId] = QUERYPARAM + /// + /// Supports: + /// - column names: [ColumnName] + /// - column indexes: COLUMN(n) + /// + /// Trace: + /// MeterLoginPasswordReader + /// -> CsvDataStorageReader.GetData() + /// -> ConnectToSource() + /// -> ExecuteQuery() + /// + public class CsvDataStorageReader : IDataStorageReader + { + private readonly DataStorageConfig config; + + private string resolvedPath; + private string[] loadedLines; + private string[] loadedHeaders; + + /// + /// Creates CSV data storage reader using provided configuration. + /// + /// + /// Data storage configuration loaded from gci_config.json. + /// + public CsvDataStorageReader(DataStorageConfig config) + { + this.config = config ?? throw new ArgumentNullException(nameof(config)); + } + + /// + /// Reads values from CSV using configured QueryTemplate. + /// + /// + /// Query parameters used for lookup. + /// + /// + /// ReaderDiagnosticResult containing lookup result. + /// + public object GetData(DataQuery query) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + + ReaderDiagnosticResult sourceResult = ConnectToSource(true); + if (!sourceResult.Success) + throw new InvalidOperationException(sourceResult.Message); + + ReaderDiagnosticResult queryResult = ExecuteQuery(query, true); + if (!queryResult.Success) + throw new InvalidOperationException(queryResult.Message); + + return queryResult; + } + + /// + /// Validates CSV source accessibility and content. + /// + /// + /// Enables detailed diagnostic output. + /// + /// + /// Diagnostic result. + /// + public ReaderDiagnosticResult TestSource(bool enableDiagnostics) + { + return ConnectToSource(enableDiagnostics); + } + + /// + /// Validates configured QueryTemplate against CSV header. + /// + /// + /// Enables detailed diagnostic output. + /// + /// + /// Diagnostic result. + /// + public ReaderDiagnosticResult TestQuery(bool enableDiagnostics) + { + ReaderDiagnosticResult sourceResult = ConnectToSource(enableDiagnostics); + if (!sourceResult.Success) + return sourceResult; + + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + Log(result, enableDiagnostics, "Starting QueryTemplate validation."); + + SearchOrderDefinition definition = + SearchOrderParser.Parse(config.QueryTemplate); + + int selectIndex = ResolveColumnIndex( + loadedHeaders, + definition.SelectColumn); + + int whereIndex = ResolveColumnIndex( + loadedHeaders, + definition.WhereColumn); + + Log(result, enableDiagnostics, "QueryTemplate parsed successfully."); + Log(result, enableDiagnostics, "Select column index: " + selectIndex); + Log(result, enableDiagnostics, "Where column index: " + whereIndex); + + result.Success = true; + result.Message = "QueryTemplate validation finished successfully."; + } + catch (Exception ex) + { + result.Success = false; + result.Message = ex.Message; + Log(result, enableDiagnostics, "ERROR: " + ex.Message); + } + + return result; + } + + /// + /// Opens CSV file and loads its content. + /// Validates file existence and header. + /// + /// + /// Enables detailed diagnostic output. + /// + /// + /// Source validation result. + /// + private ReaderDiagnosticResult ConnectToSource(bool enableDiagnostics) + { + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + Log(result, enableDiagnostics, "Starting CSV source connection test."); + + if (string.IsNullOrWhiteSpace(config.DataSource)) + throw new InvalidOperationException("CSV data source is empty."); + + resolvedPath = Path.GetFullPath(config.DataSource); + + Log(result, enableDiagnostics, "Resolved full path: " + resolvedPath); + + if (!File.Exists(resolvedPath)) + throw new FileNotFoundException("CSV file was not found.", resolvedPath); + + loadedLines = File.ReadAllLines(resolvedPath); + + if (loadedLines == null || loadedLines.Length == 0) + throw new InvalidOperationException("CSV file is empty."); + + loadedHeaders = SplitCsvLine(loadedLines[0]); + + if (loadedHeaders == null || loadedHeaders.Length == 0) + throw new InvalidOperationException("CSV header is empty."); + + Log(result, enableDiagnostics, "CSV line count: " + loadedLines.Length); + Log(result, enableDiagnostics, "CSV header column count: " + loadedHeaders.Length); + + result.Success = true; + result.Message = "CSV source connection finished successfully."; + result.Data = loadedLines; + } + catch (Exception ex) + { + result.Success = false; + result.Message = ex.Message; + result.Data = null; + + Log(result, enableDiagnostics, "ERROR: " + ex.Message); + } + + return result; + } + + + /// + /// Executes configured QueryTemplate against loaded CSV data. + /// + /// + /// Query values used for matching. + /// + /// + /// Enables detailed diagnostic output. + /// + /// + /// Query result. + /// + private ReaderDiagnosticResult ExecuteQuery( + DataQuery query, + bool enableDiagnostics) + { + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + if (query.QueryParams == null || query.QueryParams.Count == 0) + throw new InvalidOperationException("DataQuery.QueryParams is empty."); + + SearchOrderDefinition definition = + SearchOrderParser.Parse(config.QueryTemplate); + + int selectIndex = ResolveColumnIndex( + loadedHeaders, + definition.SelectColumn); + + int whereIndex = ResolveColumnIndex( + loadedHeaders, + definition.WhereColumn); + + List matchedValues = new List(); + + foreach (string param in query.QueryParams) + { + string queryValue = (param ?? string.Empty).Trim(); + bool found = false; + + for (int lineIndex = 1; lineIndex < loadedLines.Length; lineIndex++) + { + if (string.IsNullOrWhiteSpace(loadedLines[lineIndex])) + continue; + + string[] values = SplitCsvLine(loadedLines[lineIndex]); + + if (whereIndex >= values.Length) + continue; + + string currentValue = (values[whereIndex] ?? string.Empty).Trim(); + + if (!string.Equals( + currentValue, + queryValue, + StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string returnValue = + selectIndex < values.Length + ? values[selectIndex] + : string.Empty; + + matchedValues.Add(returnValue); + found = true; + break; + } + + if (!found) + Log(result, enableDiagnostics, "No match found for: " + queryValue); + } + + result.Success = true; + + if (query.QueryParams.Count == 1) + { + result.Data = matchedValues.Count > 0 ? matchedValues[0] : null; + result.Message = matchedValues.Count > 0 + ? "Value found." + : "No match found."; + } + else + { + result.Data = matchedValues; + result.Message = + "Batch query finished. Matches found: " + + matchedValues.Count + + " of " + + query.QueryParams.Count + + "."; + } + } + catch (Exception ex) + { + result.Success = false; + result.Message = ex.Message; + result.Data = null; + + Log(result, enableDiagnostics, "ERROR: " + ex.Message); + } + + return result; + } + + /// + /// Resolves column index either by name or by explicit index. + /// + /// + /// CSV header columns. + /// + /// + /// Parsed QueryTemplate column definition. + /// + /// + /// Zero-based column index. + /// + private int ResolveColumnIndex( + string[] headers, + ColumnReference columnReference) + { + if (columnReference == null) + throw new InvalidOperationException("Column reference is null."); + + if (columnReference.HasIndex) + { + int index = columnReference.Index.Value; + + if (index < 0 || index >= headers.Length) + throw new InvalidOperationException("Column index is out of range."); + + return index; + } + + if (columnReference.HasName) + { + for (int i = 0; i < headers.Length; i++) + { + if (string.Equals( + headers[i]?.Trim(), + columnReference.Name?.Trim(), + StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + + throw new InvalidOperationException( + "Column '" + columnReference.Name + "' was not found in CSV header."); + } + + throw new InvalidOperationException("Column reference is not defined."); + } + + /// + /// Splits CSV line using common separators. + /// Supports: + /// ';' + /// ',' + /// '\t' + /// + /// + /// Input CSV line. + /// + /// + /// Parsed columns. + /// + private string[] SplitCsvLine(string line) + { + if (string.IsNullOrEmpty(line)) + return new string[0]; + + if (line.Contains(";")) + return line.Split(';'); + + if (line.Contains(",")) + return line.Split(','); + + if (line.Contains("\t")) + return line.Split('\t'); + + return new[] { line }; + } + + /// + /// Adds diagnostic line when diagnostics are enabled. + /// + private void Log( + ReaderDiagnosticResult result, + bool enableDiagnostics, + string message) + { + if (!enableDiagnostics || result == null) + return; + + result.Diagnostics.Add(message); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/DatabaseDataStorageReader.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/DatabaseDataStorageReader.cs new file mode 100644 index 000000000..acf2ccee3 --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/DatabaseDataStorageReader.cs @@ -0,0 +1,273 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts; + +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers +{ + /// + /// Reads data from SQL Server based data storage. + /// + /// The reader uses configuration loaded from gci_config.json. + /// + /// QueryTemplate must contain QUERYPARAM placeholder. + /// + /// Example: + /// SELECT [Password] FROM [dbo].[SkeletonKeys] WHERE [PcbId] = QUERYPARAM + /// + /// The placeholder is internally converted to SQL parameter @value. + /// + public class DatabaseDataStorageReader : IDataStorageReader + { + /// + /// Data storage configuration containing connection string and query template. + /// + private readonly DataStorageConfig config; + + /// + /// Creates SQL Server data storage reader using provided configuration. + /// + /// + /// Data storage configuration loaded from gci_config.json. + /// + public DatabaseDataStorageReader(DataStorageConfig config) + { + this.config = config ?? throw new ArgumentNullException(nameof(config)); + } + + /// + /// Executes configured SQL query and returns first matching row. + /// + /// + /// Query object containing lookup parameter. + /// + /// + /// DatabaseSearchResult containing returned SQL columns and values. + /// + public object GetData(DataQuery query) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + + ReaderDiagnosticResult sourceResult = TestSource(true); + if (!sourceResult.Success) + throw new InvalidOperationException(sourceResult.Message); + + string sqlText = PrepareSqlText(config.QueryTemplate); + object queryValue = ExtractQueryValue(query); + + using (SqlConnection connection = new SqlConnection(config.DataSource)) + using (SqlCommand command = new SqlCommand(sqlText, connection)) + { + AddQueryParameter(command, queryValue); + + connection.Open(); + + using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.SingleRow)) + { + DatabaseSearchResult result = new DatabaseSearchResult + { + Query = sqlText + }; + + if (!reader.Read()) + { + result.Found = false; + return result; + } + + result.Found = true; + + for (int i = 0; i < reader.FieldCount; i++) + { + object value = reader.GetValue(i); + result.Values[reader.GetName(i)] = + value == DBNull.Value ? null : value; + } + + return result; + } + } + } + + /// + /// Tests whether SQL Server connection can be opened. + /// + /// + /// Enables detailed diagnostic output. + /// + /// + /// Diagnostic result of SQL connection test. + /// + public ReaderDiagnosticResult TestSource(bool enableDiagnostics) + { + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + if (string.IsNullOrWhiteSpace(config.DataSource)) + throw new InvalidOperationException("Data source is empty."); + + Log(result, enableDiagnostics, "Opening SQL connection."); + + using (SqlConnection connection = new SqlConnection(config.DataSource)) + { + connection.Open(); + + Log(result, enableDiagnostics, "Connection opened successfully."); + + using (SqlCommand command = new SqlCommand("SELECT 1", connection)) + { + object value = command.ExecuteScalar(); + Log(result, enableDiagnostics, "Test query result: " + value); + } + } + + result.Success = true; + result.Message = "Connection to SQL Server OK."; + } + catch (Exception ex) + { + result.Success = false; + result.Message = "Failed to connect to SQL Server. " + ex.Message; + Log(result, enableDiagnostics, ex.ToString()); + } + + return result; + } + + /// + /// Tests whether configured SQL query can be prepared and executed. + /// + /// + /// Enables detailed diagnostic output. + /// + /// + /// Diagnostic result of query execution test. + /// + public ReaderDiagnosticResult TestQuery(bool enableDiagnostics) + { + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + if (string.IsNullOrWhiteSpace(config.QueryTemplate)) + throw new InvalidOperationException("Query template is empty."); + + string sqlText = PrepareSqlText(config.QueryTemplate); + + Log(result, enableDiagnostics, "Original template:"); + Log(result, enableDiagnostics, config.QueryTemplate); + + Log(result, enableDiagnostics, "Prepared SQL:"); + Log(result, enableDiagnostics, sqlText); + + using (SqlConnection connection = new SqlConnection(config.DataSource)) + using (SqlCommand command = new SqlCommand(sqlText, connection)) + { + AddQueryParameter(command, "TEST"); + + connection.Open(); + object value = command.ExecuteScalar(); + + result.Data = value; + + Log(result, enableDiagnostics, "Query executed successfully."); + } + + result.Success = true; + result.Message = "Query executed successfully."; + } + catch (Exception ex) + { + result.Success = false; + result.Message = "Query execution failed. " + ex.Message; + Log(result, enableDiagnostics, ex.ToString()); + } + + return result; + } + + /// + /// Converts configured QueryTemplate to executable SQL text. + /// + /// + /// SQL query template containing QUERYPARAM placeholder. + /// + /// + /// SQL text with QUERYPARAM replaced by @value parameter. + /// + private static string PrepareSqlText(string queryTemplate) + { + if (string.IsNullOrWhiteSpace(queryTemplate)) + throw new ArgumentException( + "Query template must not be empty.", + nameof(queryTemplate)); + + if (!queryTemplate.Contains("QUERYPARAM")) + throw new InvalidOperationException( + "Query template must contain QUERYPARAM placeholder."); + + return queryTemplate.Replace("QUERYPARAM", "@value"); + } + + /// + /// Extracts first query parameter value. + /// + /// + /// Data query containing query parameters. + /// + /// + /// First query parameter value. + /// + private static object ExtractQueryValue(DataQuery query) + { + if (query.QueryParams == null || query.QueryParams.Count == 0) + throw new InvalidOperationException( + "DataQuery does not contain any query parameter."); + + return query.QueryParams[0]; + } + + /// + /// Adds lookup parameter to SQL command. + /// + /// + /// SQL command. + /// + /// + /// Query parameter value. + /// + private static void AddQueryParameter(SqlCommand command, object value) + { + command.Parameters.Clear(); + + SqlParameter parameter = command.Parameters.Add("@value", SqlDbType.Variant); + parameter.Value = value ?? DBNull.Value; + } + + /// + /// Adds diagnostic line when diagnostics are enabled. + /// + /// + /// Diagnostic result object. + /// + /// + /// Indicates whether diagnostics are enabled. + /// + /// + /// Diagnostic message. + /// + private static void Log( + ReaderDiagnosticResult result, + bool enableDiagnostics, + string message) + { + if (!enableDiagnostics || result == null) + return; + + result.Diagnostics.Add(message); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/JsonDataStorageReader.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/JsonDataStorageReader.cs new file mode 100644 index 000000000..206dc4774 --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/JsonDataStorageReader.cs @@ -0,0 +1,31 @@ +using System; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts; + +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers +{ + public class JsonDataStorageReader : IDataStorageReader + { + private readonly DataStorageConfig config; + + public JsonDataStorageReader(DataStorageConfig config) + { + this.config = config ?? throw new ArgumentNullException(nameof(config)); + } + + public object GetData(DataQuery query) + { + throw new NotImplementedException(); + } + + public ReaderDiagnosticResult TestSource(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + + public ReaderDiagnosticResult TestQuery(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/RestApiDataStorageReader.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/RestApiDataStorageReader.cs new file mode 100644 index 000000000..0c1f012af --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/RestApiDataStorageReader.cs @@ -0,0 +1,31 @@ +using System; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models; +using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts; + +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers +{ + public class RestApiDataStorageReader : IDataStorageReader + { + private readonly DataStorageConfig config; + + public RestApiDataStorageReader(DataStorageConfig config) + { + this.config = config ?? throw new ArgumentNullException(nameof(config)); + } + + public object GetData(DataQuery query) + { + throw new NotImplementedException(); + } + + public ReaderDiagnosticResult TestSource(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + + public ReaderDiagnosticResult TestQuery(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderDefinition.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderDefinition.cs new file mode 100644 index 000000000..36a1ceebe --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderDefinition.cs @@ -0,0 +1,125 @@ +using System; + +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Searching +{ + /// + /// Parsed representation of QueryTemplate expression. + /// + /// Example: + /// + /// SELECT [Password] WHERE [PcbId] = QUERYPARAM + /// + /// becomes: + /// + /// SelectColumn: + /// [Password] + /// + /// WhereColumn: + /// [PcbId] + /// + public class SearchOrderDefinition + { + /// + /// Column to be returned from lookup result. + /// + /// Example: + /// + /// SELECT [Password] + /// + /// returns: + /// + /// [Password] + /// + public ColumnReference SelectColumn { get; set; } + + /// + /// Column used for row matching. + /// + /// Example: + /// + /// WHERE [PcbId] = QUERYPARAM + /// + /// uses: + /// + /// [PcbId] + /// + public ColumnReference WhereColumn { get; set; } + } + + /// + /// Represents one column definition inside QueryTemplate. + /// + /// Supports two forms: + /// + /// [ColumnName] + /// + /// or: + /// + /// COLUMN(number) + /// + /// Examples: + /// + /// [Password] + /// + /// COLUMN(2) + /// + public class ColumnReference + { + /// + /// Column name used in named lookup mode. + /// + /// Example: + /// + /// [Password] + /// + public string Name { get; set; } + + /// + /// Zero-based column index used in indexed lookup mode. + /// + /// Example: + /// + /// COLUMN(2) + /// + public int? Index { get; set; } + + /// + /// Indicates whether column uses name-based lookup. + /// + public bool HasName + { + get + { + return !string.IsNullOrWhiteSpace(Name); + } + } + + /// + /// Indicates whether column uses index-based lookup. + /// + public bool HasIndex + { + get + { + return Index.HasValue; + } + } + + /// + /// Returns human readable representation. + /// + /// + /// Column expression text. + /// + public override string ToString() + { + if (HasName) + return "[" + Name + "]"; + + if (HasIndex) + return "COLUMN(" + Index.Value + ")"; + + return ""; + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderParser.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderParser.cs new file mode 100644 index 000000000..89612e828 --- /dev/null +++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderParser.cs @@ -0,0 +1,167 @@ +using System; +using System.Text.RegularExpressions; + +namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Searching +{ + /// + /// Parses SQL-like QueryTemplate expressions used by GCI data storage readers. + /// + /// Supported syntax: + /// + /// SELECT [ReturnColumn] WHERE [MatchColumn] = QUERYPARAM + /// + /// Examples: + /// + /// SELECT [Password] WHERE [PcbId] = QUERYPARAM + /// + /// SELECT COLUMN(1) WHERE COLUMN(0) = QUERYPARAM + /// + /// Mixed forms are also supported: + /// + /// SELECT [Password] WHERE COLUMN(0)=QUERYPARAM + /// + /// The parser converts text expressions into structured + /// SearchOrderDefinition objects which are later consumed + /// by CSV and other readers. + /// + public static class SearchOrderParser + { + /// + /// Full QueryTemplate validation pattern. + /// + /// Expected syntax: + /// + /// SELECT [Column] WHERE [Column] = QUERYPARAM + /// + private static readonly Regex FullPattern = new Regex( + @"^\s*SELECT\s+(?