tbf/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writers/XmlFileWriter.cs
Marek Frniak 054075a341 <Feat>: Add XML result generation with file and MSSQL output support, increase revision to 3.9.3145.100
Cause:

- ResultsWriter required a generic way to generate customer-specific XML result data from TBF measurement results.
- The customer reference XML contains example runtime values and repeated result structures, so it cannot be used directly as the generated output.
- TBF result variables must be explicitly mapped to destinations in the customer XML structure.
- The generated XML result data must support two output targets:
  - direct creation of an XML file,
  - delivery of the XML payload to a Microsoft SQL stored procedure.
- Preview generation must allow the XML structure and configured mappings to be verified without executing the production database write.
- Increase revision to 3.9.3145.100.

Solution:

1. Added XML reference analysis
   - Creates a clean base XML structure.
   - Extracts the repeating result prototype.
   - Prevents sample runtime values from the customer reference XML from leaking into generated results.
2. Added configurable TBF-to-XML result mapping
   - Allows explicit mapping of TBF result variables to customer XML destinations.
   - Supports one-time and repeating XML destinations.
   - Keeps the mapping independent of the semantic meaning of customer XML attribute names.
3. Added XML destination viewer and configurator
   - Shows the current mapping.
   - Highlights repeating destinations.
   - Identifies already used one-time destinations.
4. Added runtime XML result generation
   - Uses the configured TBF-to-XML mappings.
   - Uses measurement procedure result data.
   - Builds the output from the clean base XML and repeating XML prototype.
   - Generates repeated result records according to the executed measurement procedure.
5. Added simulation-based Preview request
   - Generates a complete XML payload using simulation values.
   - Allows XML structure and mapping verification before production execution.
   - Does not execute the production stored procedure.
6. Added direct XML file output support to UniDataStorageWriter
   - Supports File / .xml as a physical output target.
   - Creates the generated XML result file in the configured output directory.
7. Added Microsoft SQL stored-procedure XML output
   - Supports Microsoft SQL / StoredProcedure as a physical output target.
   - Passes the generated XML payload through the configured stored procedure parameter.
8. Added optional XML payload archiving
   - Allows generated XML payloads to be stored in the configured Payload archive.
   - Can be used together with the Microsoft SQL stored-procedure output.
9. Kept ResultsWriter independent of the physical output target
   - ResultsWriter generates the result payload.
   - UniDataStorageWriter decides how and where the payload is physically written.
   - The same ResultsWriter XML generation mechanism is therefore used for both XML file and MSSQL outputs.
10. Increased revision
   - Updated revision to 3.9.3145.100.
2026-09-02 12:05:10 +02:00

313 lines
8.7 KiB
C#

///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.IO;
using System.Text;
using System.Xml.Linq;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Diagnostic;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
using static TBF.Rig.Output.DataStorage.UniDataStorageWriter.Types;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers
{
/// <summary>
/// Writes a complete generated XML payload to a file.
/// </summary>
/// <remarks>
/// <para>
/// The configured <see cref="WriterCfg.DataSource"/> is interpreted as the
/// output directory. One file is created for every write request.
/// </para>
///
/// <para>
/// The XML payload itself is expected in
/// <see cref="DataWriteRequest.Payload"/>. This allows higher-level
/// components such as ResultsWriter to generate the complete document from
/// an XML reference structure and then use UniDataStorageWriter only as the
/// physical output target.
/// </para>
/// </remarks>
public class XmlFileWriter : IDataStorageWriter
{
private readonly WriterCfg cfg;
/// <summary>
/// Initializes a new XML file writer.
/// </summary>
public XmlFileWriter(
WriterCfg cfg)
{
this.cfg =
cfg ??
throw new ArgumentNullException(
nameof(cfg));
}
/// <summary>
/// Gets capabilities supported by this writer.
/// </summary>
public WriterCapabilities Capabilities
{
get
{
WriterCapabilities caps =
new WriterCapabilities();
caps.SupportedStorageTypes.Add(
StorageTypes.LocalFile);
caps.SupportedStorageTypes.Add(
StorageTypes.RemoteFile);
caps.SupportedTechnologyTypes.Add(
TechnologyTypes.Xml);
caps.SupportedWriteModes.Add(
WriteMode.Insert);
return caps;
}
}
/// <summary>
/// Validates the configured output directory.
/// </summary>
public WriterDiagnosticResult TestSource(
bool validateOnly)
{
try
{
string directory =
GetOutputDirectory();
if (!Directory.Exists(directory))
{
if (validateOnly)
{
return new WriterDiagnosticResult
{
Success = true,
Message =
"XML output directory does not exist yet. " +
"It will be created on the first write."
};
}
Directory.CreateDirectory(
directory);
}
return new WriterDiagnosticResult
{
Success = true,
Message =
"XML output directory is ready."
};
}
catch (Exception ex)
{
return new WriterDiagnosticResult
{
Success = false,
Message =
"XML output directory test failed: " +
ex.Message
};
}
}
/// <summary>
/// Writes one complete XML payload to disk.
/// </summary>
public WriterDiagnosticResult WriteData(
DataWriteRequest request)
{
if (request == null)
throw new ArgumentNullException(
nameof(request));
if (request.Mode !=
WriteMode.Insert)
{
return Fail(
"XML file writer supports Insert mode only.");
}
if (string.IsNullOrWhiteSpace(
request.Payload))
{
return Fail(
"XML payload is empty.");
}
try
{
//
// Validate the complete payload before any file is created.
//
XDocument.Parse(
request.Payload,
LoadOptions.PreserveWhitespace);
string directory =
GetOutputDirectory();
Directory.CreateDirectory(
directory);
string fileName =
CreateSafeFileName(
request.OutputFileName);
string fullPath =
CreateUniquePath(
directory,
fileName);
File.WriteAllText(
fullPath,
request.Payload,
new UTF8Encoding(false));
return new WriterDiagnosticResult
{
Success = true,
Message =
"XML payload written successfully: " +
fullPath,
ExecutedTemplate =
fullPath
};
}
catch (Exception ex)
{
return Fail(
"XML payload write failed: " +
ex.Message);
}
}
/// <summary>
/// Resolves and validates the configured output directory.
/// </summary>
private string GetOutputDirectory()
{
string directory =
(cfg.DataSource ??
string.Empty)
.Trim();
if (string.IsNullOrWhiteSpace(
directory))
{
throw new InvalidOperationException(
"XML output directory is not configured.");
}
if (string.Equals(
Path.GetExtension(directory),
".xml",
StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
"For XML payload output, Data source must be a directory, not an .xml file path.");
}
return Path.GetFullPath(
directory);
}
/// <summary>
/// Creates a safe XML file name.
/// </summary>
private string CreateSafeFileName(
string requestedFileName)
{
string fileName =
string.IsNullOrWhiteSpace(
requestedFileName)
? "Payload_" +
DateTime.Now.ToString(
"yyyyMMdd_HHmmss_fff") +
".xml"
: Path.GetFileName(
requestedFileName.Trim());
foreach (char invalidCharacter
in Path.GetInvalidFileNameChars())
{
fileName =
fileName.Replace(
invalidCharacter,
'_');
}
if (!fileName.EndsWith(
".xml",
StringComparison.OrdinalIgnoreCase))
{
fileName +=
".xml";
}
return fileName;
}
/// <summary>
/// Prevents accidental overwrite of an existing payload file.
/// </summary>
private string CreateUniquePath(
string directory,
string fileName)
{
string path =
Path.Combine(
directory,
fileName);
if (!File.Exists(path))
return path;
string name =
Path.GetFileNameWithoutExtension(
fileName);
string extension =
Path.GetExtension(
fileName);
int index =
1;
do
{
path =
Path.Combine(
directory,
string.Format(
"{0}_{1}{2}",
name,
index,
extension));
index++;
}
while (File.Exists(path));
return path;
}
private WriterDiagnosticResult Fail(
string message)
{
return new WriterDiagnosticResult
{
Success = false,
Message = message
};
}
}
}