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..d2b57162d --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Diagnostic/ReaderDiagnosticResult.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Text; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader +{ + /// + /// Represents result of source/query 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 static ReaderDiagnosticResult SuccessResult(string message = "OK") + { + return new ReaderDiagnosticResult + { + Success = true, + Message = message + }; + } + + public static ReaderDiagnosticResult Failure(string message) + { + return new ReaderDiagnosticResult + { + Success = false, + Message = message + }; + } + + public ReaderDiagnosticResult() + { + Diagnostics = new List(); + } + + 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/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..76d984c74 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Interfaces/DataQuery.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader +{ + /// + /// Represents query input for data storage readers. + /// Each value is used to execute the same query template once. + /// A single-item list represents a single query. + /// + public class DataQuery + { + /// + /// Parameter values used for repeated execution of the same query template. + /// + public List QueryParams { get; } = new List(); + } +} \ No newline at end of file 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..373d086c4 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Interfaces/IDataStorageReader.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader +{ + public interface IDataStorageReader + { + object GetData(DataQuery query); + + ReaderDiagnosticResult TestSource(bool enableDiagnostics); + + ReaderDiagnosticResult TestQuery(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..657780198 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Reader.cs @@ -0,0 +1,93 @@ +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.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..0f63d36a8 --- /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 QueryTemplate; + + + /// 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; + QueryTemplate = string.Empty; + } + + string[] paramNames = new string[] + { + "Data Storage type", + "Data source", + "Query template", + }; + 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: QueryTemplate = 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 = "Query template 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.QueryTemplate = this.QueryTemplate; + } + + 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..1c100837a --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/CsvReader .cs @@ -0,0 +1,345 @@ +using System; +using System.Collections.Generic; +using System.IO; +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 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. + /// Executes the same query template for each value in DataQuery.QueryParams. + /// + public object GetData(DataQuery query) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + + ReaderDiagnosticResult connectResult = ConnectToSource(true); + if (!connectResult.Success) + throw new InvalidOperationException(connectResult.Message); + + ReaderDiagnosticResult searchResult = ExecuteQuery(query, true); + if (!searchResult.Success) + throw new InvalidOperationException(searchResult.Message); + + return searchResult; + } + + /// + /// Tests whether the CSV source can be resolved, opened and read. + /// + public ReaderDiagnosticResult TestSource(bool enableDiagnostics) + { + return ConnectToSource(enableDiagnostics); + } + + /// + /// Tests whether the configured query columns exist in the CSV source. + /// + public ReaderDiagnosticResult TestQuery(bool enableDiagnostics) + { + ReaderDiagnosticResult connectResult = ConnectToSource(enableDiagnostics); + if (!connectResult.Success) + return connectResult; + + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + Log(result, enableDiagnostics, "Starting QueryTemplate validation."); + + SearchOrderDefinition definition = SearchOrderParser.Parse(cfg.QueryTemplate); + + Log(result, enableDiagnostics, "QueryTemplate 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 = "QueryTemplate 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; + } + + /// + /// Executes the configured query for all provided parameter values. + /// + private ReaderDiagnosticResult ExecuteQuery(DataQuery query, bool enableDiagnostics) + { + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + Log(result, enableDiagnostics, "Starting QueryTemplate execution."); + + if (query == null) + throw new InvalidOperationException("DataQuery is null."); + + if (query.QueryParams == null || query.QueryParams.Count == 0) + throw new InvalidOperationException("DataQuery.QueryParams 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.QueryTemplate); + + Log(result, enableDiagnostics, "QueryTemplate 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); + Log(result, enableDiagnostics, "Query parameter count: " + query.QueryParams.Count); + + List matchedValues = new List(); + int matchCount = 0; + + for (int paramIndex = 0; paramIndex < query.QueryParams.Count; paramIndex++) + { + string queryValue = (query.QueryParams[paramIndex] ?? string.Empty).Trim(); + + Log(result, enableDiagnostics, string.Format("=== Query item {0} ===", paramIndex + 1)); + Log(result, enableDiagnostics, "QueryParam: " + queryValue); + + 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)) + { + string returnValue = selectIndex < values.Length + ? values[selectIndex] + : string.Empty; + + Log(result, enableDiagnostics, "Match found at line index: " + lineIndex); + Log(result, enableDiagnostics, "Returned value: " + returnValue); + + matchedValues.Add(returnValue); + matchCount++; + found = true; + break; + } + } + + if (!found) + { + Log(result, enableDiagnostics, "No matching row found."); + } + } + + if (query.QueryParams.Count == 1) + { + result.Data = matchedValues.Count > 0 ? (object)matchedValues[0] : null; + result.Message = matchedValues.Count > 0 ? "Value found." : "No match found."; + } + else + { + result.Data = matchedValues; + result.Message = string.Format( + "Batch query finished. Matches found: {0} of {1}.", + matchCount, + query.QueryParams.Count); + } + + result.Success = true; + } + 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 common separators. + /// 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[] { 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..57fdaf946 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/DatabaseReader .cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Reflection; +using TBF.Rig.Input.DataStorage.UniDataStorageReader.Searching; +using TBF.Rig.Input.DataStorage.UniDataStorageReader.Searching.Database; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader.Readers +{ + /// + /// Reader implementation for SQL Server based data source. + /// DataSource = SQL Server connection string + /// QueryTemplate = SQL query containing QUERYPARAM placeholder + /// + public class DatabaseReader : IDataStorageReader + { + private readonly ReaderCfg cfg; + + public ReaderDiagnosticResult TestSource(bool enableDiagnostics) + { + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + if (string.IsNullOrWhiteSpace(cfg.DataSource)) + { + result.Success = false; + result.Message = "Data source is empty."; + return result; + } + + if (enableDiagnostics) + result.Diagnostics.Add("Opening SQL connection..."); + + using (SqlConnection connection = new SqlConnection(cfg.DataSource)) + { + connection.Open(); + + if (enableDiagnostics) + result.Diagnostics.Add("Connection opened successfully."); + + using (SqlCommand command = new SqlCommand("SELECT 1", connection)) + { + object val = command.ExecuteScalar(); + + if (enableDiagnostics) + result.Diagnostics.Add("Test query executed. Result=" + val); + } + } + + result.Success = true; + result.Message = "Connection to SQL Server OK."; + return result; + } + catch (Exception ex) + { + result.Success = false; + result.Message = "Failed to connect to SQL Server."; + + if (enableDiagnostics) + result.Diagnostics.Add(ex.ToString()); + + return result; + } + } + + public ReaderDiagnosticResult TestQuery(bool enableDiagnostics) + { + ReaderDiagnosticResult result = new ReaderDiagnosticResult(); + + try + { + if (string.IsNullOrWhiteSpace(cfg.QueryTemplate)) + { + result.Success = false; + result.Message = "Query template is empty."; + return result; + } + + string sql = PrepareSqlText(cfg.QueryTemplate); + + if (enableDiagnostics) + { + result.Diagnostics.Add("Original template:"); + result.Diagnostics.Add(cfg.QueryTemplate); + + result.Diagnostics.Add("Prepared SQL:"); + result.Diagnostics.Add(sql); + } + + using (SqlConnection connection = new SqlConnection(cfg.DataSource)) + using (SqlCommand command = new SqlCommand(sql, connection)) + { + // dummy parameter + command.Parameters.AddWithValue("@value", "TEST"); + + if (enableDiagnostics) + result.Diagnostics.Add("Parameter @value = TEST"); + + connection.Open(); + + object val = command.ExecuteScalar(); + + if (enableDiagnostics) + result.Diagnostics.Add("Query executed successfully."); + + result.Data = val; // môže byť null → OK + } + + result.Success = true; + result.Message = "Query executed successfully."; + return result; + } + catch (Exception ex) + { + result.Success = false; + result.Message = "Query execution failed."; + + if (enableDiagnostics) + result.Diagnostics.Add(ex.ToString()); + + return result; + } + } + + public DatabaseReader(ReaderCfg cfg) + { + this.cfg = cfg ?? throw new ArgumentNullException(nameof(cfg)); + } + + /// + /// Reads data from SQL Server and returns matched data. + /// If query returns 1 column, scalar value is returned. + /// If query returns multiple columns, Dictionary<string, object> is returned. + /// + public object GetData(DataQuery query) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + + ReaderDiagnosticResult connectResult = ConnectToSource(true); + if (!connectResult.Success) + throw new InvalidOperationException(connectResult.Message); + + string sqlText = PrepareSqlText(cfg.QueryTemplate); + object queryValue = ExtractQueryValue(query); + + using (SqlConnection connection = new SqlConnection(cfg.DataSource)) + using (SqlCommand command = new SqlCommand(sqlText, connection)) + { + AddQueryParameters(command, queryValue); + + connection.Open(); + + using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.SingleRow)) + { + DatabaseSearchResult result = new DatabaseSearchResult(); + result.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; + } + } + } + + /// + /// Validates database connectivity and basic query readiness. + /// + public ReaderDiagnosticResult ConnectToSource(bool validateExistence) + { + if (string.IsNullOrWhiteSpace(cfg.DataSource)) + return ReaderDiagnosticResult.Failure("Data source must not be empty."); + + if (string.IsNullOrWhiteSpace(cfg.QueryTemplate)) + return ReaderDiagnosticResult.Failure("Query template must not be empty."); + + try + { + using (SqlConnection connection = new SqlConnection(cfg.DataSource)) + { + connection.Open(); + + if (validateExistence) + { + using (SqlCommand command = new SqlCommand("SELECT 1", connection)) + { + command.ExecuteScalar(); + } + } + } + + return ReaderDiagnosticResult.SuccessResult(); + } + catch (Exception ex) + { + return ReaderDiagnosticResult.Failure( + string.Format("Failed to connect to SQL Server data source. {0}", ex.Message)); + } + } + + 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"); + } + + private static void AddQueryParameters(SqlCommand command, object queryValue) + { + command.Parameters.Clear(); + + SqlParameter parameter = command.Parameters.Add("@value", SqlDbType.Variant); + parameter.Value = queryValue ?? DBNull.Value; + } + + private static object ExtractQueryValue(DataQuery query) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + + if (query.QueryParams == null || query.QueryParams.Count == 0) + throw new InvalidOperationException("DataQuery does not contain any query parameter."); + + return query.QueryParams[0]; + } + } +} \ No newline at end of file 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..10d327395 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/JsonReader .cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +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 TestQuery(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..70bba9b38 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/RestApiReader .cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +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 TestQuery(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + + public ReaderDiagnosticResult TestSource(bool enableDiagnostics) + { + throw new NotImplementedException(); + } + } +} diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/Csv/CsvSearchResult.cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/Csv/CsvSearchResult.cs new file mode 100644 index 000000000..534a0560b --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/Csv/CsvSearchResult.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader.Searching.Csv +{ + 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(); + } + } +} diff --git a/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/Database/DatabaseSearchResult.cs b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/Database/DatabaseSearchResult.cs new file mode 100644 index 000000000..69bc967e8 --- /dev/null +++ b/TBF/Rig/Input/DataStorage/UniDataStorageReader/Searching/Database/DatabaseSearchResult.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TBF.Rig.Input.DataStorage.UniDataStorageReader.Searching.Database +{ + public class DatabaseSearchResult + { + public bool Found { get; set; } + public string Query { get; set; } + public Dictionary Values { get; set; } + + public DatabaseSearchResult() + { + Values = new Dictionary(); + } + } +} 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..94d44918c --- /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 QueryTemplate expression. + /// Example: + /// SELECT [Password] WHERE [PcbId] = QUERYPARAM + /// + 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..36ae15de5 --- /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 QueryTemplate expressions. + /// Supported syntax: + /// SELECT [ReturnColumn] WHERE [MatchColumn] = QUERYPARAM + /// SELECT COLUMN(1) WHERE COLUMN(0) = QUERYPARAM + /// Mixed forms are also supported. + /// + public static class SearchOrderParser + { + private static readonly Regex FullPattern = new Regex( + @"^\s*SELECT\s+(?