tbf/TBF/Rig/Input/DataStorage/UniDataStorageReader/Readers/CsvReader .cs

345 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using TBF.Rig.Input.DataStorage.UniDataStorageReader.Searching;
namespace TBF.Rig.Input.DataStorage.UniDataStorageReader.Readers
{
/// <summary>
/// Reader implementation for CSV-based data source.
/// </summary>
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));
}
/// <summary>
/// Reads data from CSV and returns matched row data.
/// Executes the same query template for each value in DataQuery.QueryParams.
/// </summary>
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;
}
/// <summary>
/// Tests whether the CSV source can be resolved, opened and read.
/// </summary>
public ReaderDiagnosticResult TestSource(bool enableDiagnostics)
{
return ConnectToSource(enableDiagnostics);
}
/// <summary>
/// Tests whether the configured query columns exist in the CSV source.
/// </summary>
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;
}
/// <summary>
/// Opens the CSV source, validates its existence and loads its content.
/// </summary>
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;
}
/// <summary>
/// Executes the configured query for all provided parameter values.
/// </summary>
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<string> matchedValues = new List<string>();
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;
}
/// <summary>
/// Finds zero-based index of the requested column in CSV header.
/// </summary>
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;
}
/// <summary>
/// Splits CSV line by common separators.
/// Simple implementation without quoted-separator support.
/// </summary>
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 };
}
/// <summary>
/// Appends a diagnostic line if diagnostics are enabled.
/// </summary>
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.");
}
}
}