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.
313 lines
8.7 KiB
C#
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
|
|
};
|
|
}
|
|
}
|
|
}
|