From 78835c1af61b0d7cfbba13714471557f860441a6 Mon Sep 17 00:00:00 2001 From: Marek Frniak Date: Thu, 19 Mar 2026 16:30:32 +0100 Subject: [PATCH 1/3] Add UniDataStorageReader initial commit --- .../Diagnostic/ReaderDiagnosticResult.cs | 71 ++++ .../DataStorage/UniDataStorageReader/Enums.cs | 51 +++ .../UniDataStorageReader/Factory.cs | 38 ++ .../Interfaces/DataQuery.cs | 16 + .../Interfaces/IDataStorageReader.cs | 18 + .../UniDataStorageReader/Reader.cs | 94 +++++ .../UniDataStorageReader/ReaderCfg.cs | 169 ++++++++ .../Readers/CsvReader .cs | 325 ++++++++++++++++ .../Readers/DatabaseReader .cs | 38 ++ .../Readers/JsonReader .cs | 44 +++ .../Readers/RestApiReader .cs | 38 ++ .../Searching/SearchOrderDefinition.cs | 65 ++++ .../Searching/SearchOrderParser.cs | 87 +++++ .../UI/ExamplesFrm.Designer.cs | 58 +++ .../UniDataStorageReader/UI/ExamplesFrm.cs | 20 + .../UniDataStorageReader/UI/ExamplesFrm.resx | 120 ++++++ .../UniDataStorageReader/UI/ReaderCfgCtrl.cs | 358 +++++++++++++++++ .../UI/ReaderCfgCtrl.designer.cs | 364 ++++++++++++++++++ .../UI/ReaderCfgCtrl.resx | 132 +++++++ .../DatabaseWriter/WriteDBCfgCtrl.designer.cs | 2 +- .../FileWriters/IperlLogger/WriterCfg.cs | 3 + TBF/Rig/TbfComponents.cs | 1 + TBF/TBF.csproj | 59 ++- TBF/UI/MainWnd.resx | 12 +- .../Settings/DatabaseSettingsDlg.Designer.cs | 6 +- 25 files changed, 2172 insertions(+), 17 deletions(-) create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Diagnostic/ReaderDiagnosticResult.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Enums.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Factory.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Interfaces/DataQuery.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Interfaces/IDataStorageReader.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Reader.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/ReaderCfg.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/CsvReader .cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/DatabaseReader .cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/JsonReader .cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/RestApiReader .cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/SearchOrderDefinition.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/SearchOrderParser.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/UI/ExamplesFrm.Designer.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/UI/ExamplesFrm.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/UI/ExamplesFrm.resx create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/UI/ReaderCfgCtrl.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/UI/ReaderCfgCtrl.designer.cs create mode 100644 TBF/Rig/Input/DataStorage/UniDataStorageReader/UI/ReaderCfgCtrl.resx diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Diagnostic/ReaderDiagnosticResult.cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Diagnostic/ReaderDiagnosticResult.cs new file mode 100644 index 000000000..c28b7a04f --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Diagnostic/ReaderDiagnosticResult.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Text; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader +{ + /// + /// Represents result of source/order diagnostic. + /// + public class ReaderDiagnosticResult + { + /// + /// Indicates whether the diagnostic operation was successful. + /// + public bool Success { get; set; } + + /// + /// Short summary message. + /// + public string Message { get; set; } + + /// + /// Optional detailed diagnostic log lines. + /// + public List Diagnostics { get; set; } + + /// + /// Optional payload returned by the diagnostic. + /// Example: header columns, file lines count, found index, etc. + /// + public object Data { get; set; } + + public ReaderDiagnosticResult() + { + Diagnostics = new List(); + } + + public string ToDisplayText() + { + 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(); + } + } + public class CsvSearchResult + { + public bool Found { get; set; } + public string FilePath { get; set; } + public string HeaderLine { get; set; } + public string MatchedLine { get; set; } + public Dictionary Values { get; set; } + + public CsvSearchResult() + { + Values = new Dictionary(); + } + } +} \ No newline at end of file diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Enums.cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Enums.cs new file mode 100644 index 000000000..e3e94c9b7 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Enums.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader +{ + /// + /// Helper class to assign descriptions to enum values + /// + public class Description : Attribute + { + public string Text; + public Description(string t) + { + Text = t; + } + } + + public static class GetDescription + { + /// Extension method for enum-s + public static string ToDescription(this Enum en) + { + Type type = en.GetType(); + MemberInfo[] memInfo = type.GetMember(en.ToString()); + + if (memInfo != null && memInfo.Length > 0) + { + object[] attrs = memInfo[0].GetCustomAttributes(typeof(Description), false); + if (attrs != null && attrs.Length > 0) + return ((Description)attrs[0]).Text; + } + + return en.ToString(); /// Return ToString() value in case there is no description + } + } + + /// + /// Identifies the type of data storage + /// + public enum DataStorageType + { + RestApi, + Database, + Json, + Csv + } +} diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Factory.cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Factory.cs new file mode 100644 index 000000000..53d63f6d5 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Factory.cs @@ -0,0 +1,38 @@ +/// +/// Copyright (c) 2018 Sensus Slovensko a.s. +/// + +using System; +using System.Collections.Generic; +using TBF.Rig.Generic; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader +{ + /// + /// Factory component 'UniDataStorageReader' implements more storing modules + /// Modules: + /// + public class Factory : IComponentFactory + { + public string ClassName { get { return GetType().Namespace.Substring(8); } } /// For backward compatibility + public override string ToString() { return ClassName; } + + public IComponent DummyComponent() { return new Reader(new ReaderCfg("UniDataStorageReader", this)); } + + public IComponent GetComponent(IComponentCfg cfg, IList components) + { + ReaderCfg readerCfg = cfg as ReaderCfg; + if (readerCfg == null) + throw new ArgumentException("Invalid config for UniDataStorageReader"); + + return new Reader(readerCfg); + } + + public IComponentCfg DefaultConfig() { return new ReaderCfg("UniDataStorageReader", this); } + + public IComponentCfg CmpntCfgFromCmpntEntity(Config.Entities.Component component) + { + return ComponentCfgBase.CreateFromDbEntity(ReaderCfg.Serializer, component, this); + } + } +} diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Interfaces/DataQuery.cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Interfaces/DataQuery.cs new file mode 100644 index 000000000..48ec7072b --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Interfaces/DataQuery.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader.Interfaces +{ + public class DataQuery + { + /// + /// + /// + public string QueryValue { get; set; } + } +} diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Interfaces/IDataStorageReader.cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Interfaces/IDataStorageReader.cs new file mode 100644 index 000000000..c979f2a4f --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Interfaces/IDataStorageReader.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TBF.Rig.Input.DataStorage.UniDataStorageReader.Interfaces; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader +{ + public interface IDataStorageReader + { + object GetData(DataQuery query); + + ReaderDiagnosticResult TestSource(bool enableDiagnostics); + + ReaderDiagnosticResult TestOrder(bool enableDiagnostics); + } +} diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Reader.cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Reader.cs new file mode 100644 index 000000000..75e079991 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Reader.cs @@ -0,0 +1,94 @@ +using Common; +using Config.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TBF.Rig.Generic; +using TBF.Rig.Input.DataStorage.UniDataStorageReader.Interfaces; +using TBF.Rig.Input.DataStorage.UniDataStorageReader.Readers; +using TBF.Rig.Scales.MettlerToledo; +using static TBF.Rig.Input.DataStorage.UniDataStorageReader.ReaderCfg; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader +{ + /// + /// Main component that selects appropriate data reader based on configuration. + /// Acts as a dispatcher between different storage implementations. + /// + public class Reader : IComponent + { + private readonly ReaderCfg cfg; + + public Reader(ReaderCfg cfg) + { + this.cfg = cfg ?? throw new ArgumentNullException(nameof(cfg)); + } + + public string Name { get { return cfg.Name; } } + + public string ClassName { get { return cfg.ClassName; } } + + public string ParentName { get { return cfg.ParentName; } } + + public DebugMode DebugLevel { get { return cfg.DebugLevel; } } + + public LogLevel LogLevel { get { return cfg.LogLevel; } } + + public IComponentCfg Cfg { get { return cfg; } } + + public IList Corrections { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public IList Uncertainties { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public object GetDataFromStorageByParameter(DataQuery query) + { + string storageType = (cfg.DataStorageType ?? string.Empty).Trim(); + + IDataStorageReader reader; + + switch (storageType) + { + case StorageTypes.RestApi: + reader = new RestApiReader(cfg); + break; + + case StorageTypes.RemoteDatabase: + case StorageTypes.LocalDatabase: + reader = new DatabaseReader(cfg); + break; + + case StorageTypes.RemoteJson: + case StorageTypes.LocalJson: + reader = new JsonReader(cfg); + break; + + case StorageTypes.RemoteCsv: + case StorageTypes.LocalCsv: + reader = new CsvReader(cfg); + break; + + default: + throw new NotSupportedException( + $"Unsupported DataStorageType: '{cfg.DataStorageType}'"); + } + + return reader.GetData(query); + } + + public void Initialize() + { + throw new NotImplementedException(); + } + + public void StartChangeHandler() + { + throw new NotImplementedException(); + } + + public void StopChangeHandler() + { + throw new NotImplementedException(); + } + } +} diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/ReaderCfg.cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/ReaderCfg.cs new file mode 100644 index 000000000..bc8f3c2e5 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/ReaderCfg.cs @@ -0,0 +1,169 @@ +/// +/// Copyright (c) 2018 Sensus Slovensko a.s. +/// +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using Common; +using Config.Entities; +using TBF.Rig.Generic; +using TBF.Resources; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader +{ + /// + /// Class and file name is preserved for backward compatibility + /// + public class ReaderCfg : ComponentCfgBase, Generic.IComponentCfg, IParamsProvider + { + public static XmlSerializer Serializer = XmlSerializer.FromTypes(new[] { typeof(ReaderCfg) })[0]; + public override XmlSerializer GetSerializer() { return Serializer; } + + public IComponentCfgCtrl GetControl(IList cmpntEntities) { return new ReaderCfgCtrl(); } + + + public string DataStorageType; + public string DataSource; + public string SearchOrder; + + + /// Private parameterless constructor invoked by all other (public) constructors + ReaderCfg() + { + InitializeAll(); + } + + public ReaderCfg(string name, IComponentFactory factory) + : this() + { + this.Name = name; + this.Factory = factory; + } + + public string ComponentName { get { return Name; } } + + public void InitializeAll() + { + DataStorageType = string.Empty; + DataSource = string.Empty; + SearchOrder = string.Empty; + } + + string[] paramNames = new string[] + { + "Data Storage type", + "Data source", + "Search order", + }; + public string ParamName(int i) { return paramNames[i]; } + public int ParamsCount() { return paramNames.Length; } + + public ICollection ParamValues(int i) + { + switch (i) + { + case 0: + return new string[] + { + StorageTypes.RestApi, + StorageTypes.RemoteDatabase, + StorageTypes.RemoteJson, + StorageTypes.RemoteCsv, + StorageTypes.LocalDatabase, + StorageTypes.LocalJson, + StorageTypes.LocalCsv, + }; + case 1: + case 2: + default: + return null; + } + } + + public string ToString(int i) + { + return string.Format("Name={0}, DataStorageType={1}", + Name, + DataStorageType); + } + + public CfgUpdateFlags UpdateParam(int i, string strValue) + { + switch (i) + { + case 0: DataStorageType = strValue; return CfgUpdateFlags.RestartRqrd; + case 1: DataSource = strValue; return CfgUpdateFlags.RestartRqrd; + case 2: SearchOrder = strValue; return CfgUpdateFlags.RestartRqrd; + + default: return CfgUpdateFlags.None; + } + } + + public bool ValidateParam(int i, string strValue, out string message) + { + message = string.Empty; + strValue = strValue ?? string.Empty; + + switch (i) + { + case 0: + if (string.IsNullOrWhiteSpace(strValue)) + { + message = "Data storage type must be selected."; + return false; + } + return true; + + case 1: + if (string.IsNullOrWhiteSpace(strValue)) + { + message = "Data source must not be empty."; + return false; + } + return true; + + case 2: + if (string.IsNullOrWhiteSpace(strValue)) + { + message = "Search order must not be empty."; + return false; + } + return true; + + default: + message = "Invalid parameter index."; + return false; + } + } + + void CopyContentTo(ReaderCfg prms) + { + prms.DataStorageType = this.DataStorageType; + prms.DataSource = this.DataSource; + prms.SearchOrder = this.SearchOrder; + } + + public IParamsProvider Clone() + { + ReaderCfg pars = new ReaderCfg(); + CopyContentTo(pars); + return pars; + } + + public bool UpdateEmbeddedDbEntity() + { + return true; /// =OK, do nothing + } + + public static class StorageTypes + { + public const string RestApi = "REST_API"; + public const string RemoteDatabase = "REMOTE_DATABASE"; + public const string RemoteJson = "REMOTE_JSON"; + public const string RemoteCsv = "REMOTE_CSV"; + public const string LocalDatabase = "LOCAL_DATABASE"; + public const string LocalJson = "LOCAL_JSON"; + public const string LocalCsv = "LOCAL_CSV"; + } + } +} diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/CsvReader .cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/CsvReader .cs new file mode 100644 index 000000000..5c2a7c252 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/CsvReader .cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using System.IO; +using TBF.Rig.Input.DataStorage.UniDataStorageReader.Interfaces; +using TBF.Rig.Input.DataStorage.UniDataStorageReader.Searching; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader.Readers +{ + /// + /// Reader implementation for CSV-based data source. + /// + public class CsvReader : IDataStorageReader + { + //private string currentDataSource; + //private string currentSearchOrder; + private string currentParameterName; + private string currentParameterValue; + + private string resolvedPath; + private string[] loadedLines; + private string[] loadedHeaders; + + private readonly ReaderCfg cfg; + + public CsvReader(ReaderCfg cfg) + { + this.cfg = cfg ?? throw new ArgumentNullException(nameof(cfg)); + } + + /// + /// Reads data from CSV and returns matched row data. + /// + public object GetData(DataQuery query) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + + ReaderDiagnosticResult connectResult = ConnectToSource(false); + if (!connectResult.Success) + throw new InvalidOperationException(connectResult.Message); + + ReaderDiagnosticResult searchResult = ExecuteSearchOrder(query, false); + if (!searchResult.Success) + throw new InvalidOperationException(searchResult.Message); + + return searchResult.Data; + } + + /// + /// Tests whether the CSV source can be resolved, opened and read. + /// + public ReaderDiagnosticResult TestSource(bool enableDiagnostics) + { + currentParameterName = null; + currentParameterValue = null; + + return ConnectToSource(enableDiagnostics); + } + + /// + /// Tests whether the configured search-order column exists in the CSV source. + /// + public ReaderDiagnosticResult TestOrder(bool enableDiagnostics) + { + ReaderDiagnosticResult connectResult = ConnectToSource(enableDiagnostics); + if (!connectResult.Success) + return connectResult; + + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + Log(result, enableDiagnostics, "Starting SearchOrder validation."); + + SearchOrderDefinition definition = SearchOrderParser.Parse(cfg.SearchOrder); + + Log(result, enableDiagnostics, "SearchOrder parsed successfully."); + Log(result, enableDiagnostics, "Select column: " + definition.SelectColumn); + Log(result, enableDiagnostics, "Where column: " + definition.WhereColumn); + + int selectIndex = ResolveColumnIndex(loadedHeaders, definition.SelectColumn); + int whereIndex = ResolveColumnIndex(loadedHeaders, definition.WhereColumn); + + Log(result, enableDiagnostics, "Resolved select column index: " + selectIndex); + Log(result, enableDiagnostics, "Resolved where column index: " + whereIndex); + + result.Success = true; + result.Message = "SearchOrder validation finished successfully."; + result.Data = null; + } + catch (Exception ex) + { + result.Success = false; + result.Message = ex.Message; + result.Data = null; + + Log(result, enableDiagnostics, "ERROR: " + ex.Message); + } + + return result; + } + + /// + /// Opens the CSV source, validates its existence and loads its content. + /// + public ReaderDiagnosticResult ConnectToSource(bool enableDiagnostics) + { + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + Log(result, enableDiagnostics, "Starting CSV source connection test."); + + if (string.IsNullOrWhiteSpace(cfg.DataSource)) + throw new InvalidOperationException("CSV data source is empty."); + + Log(result, enableDiagnostics, "Input data source: " + cfg.DataSource); + + resolvedPath = Path.GetFullPath(cfg.DataSource); + Log(result, enableDiagnostics, "Resolved full path: " + resolvedPath); + + Log(result, enableDiagnostics, "Checking whether the file exists."); + if (!File.Exists(resolvedPath)) + throw new FileNotFoundException("CSV file was not found.", resolvedPath); + + Log(result, enableDiagnostics, "CSV file exists."); + + Log(result, enableDiagnostics, "Reading CSV file."); + loadedLines = File.ReadAllLines(resolvedPath); + + if (loadedLines == null || loadedLines.Length == 0) + throw new InvalidOperationException("CSV file is empty."); + + Log(result, enableDiagnostics, "CSV line count: " + loadedLines.Length); + + loadedHeaders = SplitCsvLine(loadedLines[0]); + if (loadedHeaders == null || loadedHeaders.Length == 0) + throw new InvalidOperationException("CSV header is empty."); + + Log(result, enableDiagnostics, "CSV header loaded successfully."); + Log(result, enableDiagnostics, "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; + } + + private ReaderDiagnosticResult ExecuteSearchOrder(DataQuery query, bool enableDiagnostics) + { + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + Log(result, enableDiagnostics, "Starting SearchOrder execution."); + + if (query == null) + throw new InvalidOperationException("DataQuery is null."); + + if (string.IsNullOrWhiteSpace(query.QueryValue)) + throw new InvalidOperationException("DataQuery.QueryValue is empty."); + + if (loadedLines == null || loadedLines.Length == 0) + throw new InvalidOperationException("CSV source is not connected."); + + if (loadedHeaders == null || loadedHeaders.Length == 0) + throw new InvalidOperationException("CSV header is not loaded."); + + SearchOrderDefinition definition = SearchOrderParser.Parse(cfg.SearchOrder); + + Log(result, enableDiagnostics, "SearchOrder parsed successfully."); + Log(result, enableDiagnostics, "Select column: " + definition.SelectColumn); + Log(result, enableDiagnostics, "Where column: " + definition.WhereColumn); + Log(result, enableDiagnostics, "QueryValue: " + query.QueryValue); + + int selectIndex = ResolveColumnIndex(loadedHeaders, definition.SelectColumn); + int whereIndex = ResolveColumnIndex(loadedHeaders, definition.WhereColumn); + + Log(result, enableDiagnostics, "Resolved select column index: " + selectIndex); + Log(result, enableDiagnostics, "Resolved where column index: " + whereIndex); + + for (int i = 1; i < loadedLines.Length; i++) + { + if (string.IsNullOrWhiteSpace(loadedLines[i])) + continue; + + string[] values = SplitCsvLine(loadedLines[i]); + + if (whereIndex >= values.Length) + continue; + + string currentValue = (values[whereIndex] ?? string.Empty).Trim(); + + if (string.Equals( + currentValue, + query.QueryValue.Trim(), + StringComparison.OrdinalIgnoreCase)) + { + string returnValue = selectIndex < values.Length + ? values[selectIndex] + : string.Empty; + + Log(result, enableDiagnostics, "Match found at line index: " + i); + Log(result, enableDiagnostics, "Returned value: " + returnValue); + + result.Success = true; + result.Message = "Value found."; + result.Data = returnValue; + return result; + } + } + + Log(result, enableDiagnostics, "No matching row found."); + + result.Success = true; + result.Message = "No match found."; + result.Data = null; + } + catch (Exception ex) + { + result.Success = false; + result.Message = ex.Message; + result.Data = null; + + Log(result, enableDiagnostics, "ERROR: " + ex.Message); + } + + return result; + } + + /// + /// Finds zero-based index of the requested column in CSV header. + /// + private int FindColumnIndex(string[] headers, string columnName) + { + for (int i = 0; i < headers.Length; i++) + { + if (string.Equals( + (headers[i] ?? string.Empty).Trim(), + (columnName ?? string.Empty).Trim(), + StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + + return -1; + } + + /// + /// Splits CSV line by semicolon. + /// Simple implementation without quoted-separator support. + /// + 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 string[] { line }; + } + + /// + /// Appends a diagnostic line if diagnostics are enabled. + /// + private void Log(ReaderDiagnosticResult result, bool enableDiagnostics, string message) + { + if (!enableDiagnostics || result == null) + return; + + result.Diagnostics.Add(message); + } + + private int ResolveColumnIndex(string[] headers, ColumnReference columnRef) + { + if (columnRef == null) + throw new InvalidOperationException("Column reference is null."); + + if (columnRef.HasIndex) + { + int index = columnRef.Index.Value; + + if (index < 0 || index >= headers.Length) + { + throw new InvalidOperationException( + string.Format("Column index {0} is out of range. Header column count: {1}.", index, headers.Length)); + } + + return index; + } + + if (columnRef.HasName) + { + int index = FindColumnIndex(headers, columnRef.Name); + if (index < 0) + { + throw new InvalidOperationException( + string.Format("Column '{0}' was not found in CSV header.", columnRef.Name)); + } + + return index; + } + + throw new InvalidOperationException("Column reference is not defined."); + } + } +} \ No newline at end of file diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/DatabaseReader .cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/DatabaseReader .cs new file mode 100644 index 000000000..d599c3223 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/DatabaseReader .cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TBF.Rig.Input.DataStorage.UniDataStorageReader.Interfaces; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader.Readers +{ + public class DatabaseReader : IDataStorageReader + { + private readonly ReaderCfg cfg; + + public DatabaseReader(ReaderCfg cfg) + { + this.cfg = cfg ?? throw new ArgumentNullException(nameof(cfg)); + } + + public object GetData(DataQuery query) + { + if (string.IsNullOrWhiteSpace(cfg.DataSource)) + throw new InvalidOperationException("Database data source is empty."); + + // TODO: DB connection + SQL query by searchOrder + return null; + } + + public ReaderDiagnosticResult TestOrder(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + + public ReaderDiagnosticResult TestSource(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + } +} diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/JsonReader .cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/JsonReader .cs new file mode 100644 index 000000000..c46ea848c --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/JsonReader .cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TBF.Rig.Input.DataStorage.UniDataStorageReader.Interfaces; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader.Readers +{ + public class JsonReader : IDataStorageReader + { + private readonly ReaderCfg cfg; + + public JsonReader(ReaderCfg cfg) + { + this.cfg = cfg ?? throw new ArgumentNullException(nameof(cfg)); + } + + public object GetData(DataQuery query) + { + if (string.IsNullOrWhiteSpace(cfg.DataSource)) + throw new InvalidOperationException("JSON data source is empty."); + + if (!File.Exists(cfg.DataSource)) + throw new FileNotFoundException("JSON file not found.", cfg.DataSource); + + string json = File.ReadAllText(cfg.DataSource); + + // TODO: deserialize + filter + return json; + } + + public ReaderDiagnosticResult TestOrder(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + + public ReaderDiagnosticResult TestSource(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + } +} diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/RestApiReader .cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/RestApiReader .cs new file mode 100644 index 000000000..3941df1a7 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/RestApiReader .cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TBF.Rig.Input.DataStorage.UniDataStorageReader.Interfaces; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader.Readers +{ + public class RestApiReader : IDataStorageReader + { + private readonly ReaderCfg cfg; + + public RestApiReader(ReaderCfg cfg) + { + this.cfg = cfg ?? throw new ArgumentNullException(nameof(cfg)); + } + + public object GetData(DataQuery query) + { + if (string.IsNullOrWhiteSpace(cfg.DataSource)) + throw new InvalidOperationException("REST API data source is empty."); + + // TODO: HTTP request + return null; + } + + public ReaderDiagnosticResult TestOrder(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + + public ReaderDiagnosticResult TestSource(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + } +} diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/SearchOrderDefinition.cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/SearchOrderDefinition.cs new file mode 100644 index 000000000..41d664e1a --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/SearchOrderDefinition.cs @@ -0,0 +1,65 @@ +using System; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader.Searching +{ + /// + /// Parsed representation of SearchOrder expression. + /// Example: + /// SELECT [Password] WHERE [PcbId] = QUERYVALUE + /// + public class SearchOrderDefinition + { + /// + /// Column to be returned. + /// + public ColumnReference SelectColumn { get; set; } + + /// + /// Column used for lookup. + /// + public ColumnReference WhereColumn { get; set; } + } + + /// + /// Represents a column reference either by name or by zero-based index. + /// + public class ColumnReference + { + /// + /// Column name if referenced by [ColumnName]. + /// + public string Name { get; set; } + + /// + /// Zero-based column index if referenced by COLUMN(n). + /// + public int? Index { get; set; } + + /// + /// Returns true if reference is by column name. + /// + public bool HasName + { + get { return !string.IsNullOrWhiteSpace(Name); } + } + + /// + /// Returns true if reference is by column index. + /// + public bool HasIndex + { + get { return Index.HasValue; } + } + + public override string ToString() + { + if (HasName) + return "[" + Name + "]"; + + if (HasIndex) + return "COLUMN(" + Index.Value + ")"; + + return ""; + } + } +} \ No newline at end of file diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/SearchOrderParser.cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/SearchOrderParser.cs new file mode 100644 index 000000000..a133d458e --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/SearchOrderParser.cs @@ -0,0 +1,87 @@ +using System; +using System.Text.RegularExpressions; +using TBF.Rig.Input.DataStorage.UniDataStorageReader.Searching; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader +{ + /// + /// Parses SQL-like SearchOrder expressions. + /// Supported syntax: + /// SELECT [ReturnColumn] WHERE [MatchColumn] = QUERYVALUE + /// SELECT COLUMN(1) WHERE COLUMN(0) = QUERYVALUE + /// Mixed forms are also supported. + /// + public static class SearchOrderParser + { + private static readonly Regex FullPattern = new Regex( + @"^\s*SELECT\s+(?\[[^\]]+\]|COLUMN\(\d+\))\s+WHERE\s+(?\[[^\]]+\]|COLUMN\(\d+\))\s*=\s*QUERYVALUE\s*$", + @"^\s*SELECT\s+(?