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+(?