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.
587 lines
16 KiB
C#
587 lines
16 KiB
C#
///
|
|
/// Copyright (c) 2026 Sensus Slovensko a.s.
|
|
///
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using System.Windows.Forms;
|
|
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters;
|
|
using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window;
|
|
|
|
namespace TBF.Rig.Output.DB.ResultsWriter
|
|
{
|
|
/// <summary>
|
|
/// Displays the structural model of an XML payload example and allows
|
|
/// selection of one logical XML destination.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Sample XML values are intentionally not displayed. The dialog is a
|
|
/// structure viewer and mapping configurator, not an XML value editor.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Existing TBF mappings are shown directly beside their XML destinations.
|
|
/// Repeating prototype destinations can be used by multiple TBF results.
|
|
/// One-time destinations can be assigned only once.
|
|
/// </para>
|
|
/// </remarks>
|
|
public partial class XmlDestinationPickerDlg : Form
|
|
{
|
|
private readonly PayloadTemplateInspector inspector;
|
|
|
|
/// <summary>
|
|
/// Initializes a new picker dialog.
|
|
/// </summary>
|
|
public XmlDestinationPickerDlg()
|
|
{
|
|
InitializeComponent();
|
|
|
|
inspector =
|
|
new PayloadTemplateInspector();
|
|
|
|
try
|
|
{
|
|
Icon =
|
|
Properties.Resources.TBF_icon;
|
|
}
|
|
catch
|
|
{
|
|
// Optional icon only.
|
|
}
|
|
|
|
ConfiguredMappings =
|
|
new Dictionary<string, IList<string>>(
|
|
StringComparer.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the customer XML example path.
|
|
/// </summary>
|
|
public string TemplatePath
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the TBF result currently being configured.
|
|
/// </summary>
|
|
public string CurrentSourceName
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the destination currently assigned to the edited item.
|
|
/// </summary>
|
|
public string CurrentDestinationPath
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets mappings already configured by other TBF result items.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Dictionary key is the logical XML destination. The list contains
|
|
/// TBF result names currently mapped to the destination.
|
|
/// </remarks>
|
|
public IDictionary<string, IList<string>> ConfiguredMappings
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the selected logical destination.
|
|
/// </summary>
|
|
public string SelectedDestinationPath
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
private void XmlDestinationPickerDlg_Load(
|
|
object sender,
|
|
EventArgs e)
|
|
{
|
|
Text =
|
|
"Select XML destination";
|
|
|
|
templatePathTextBox.Text =
|
|
TemplatePath ?? string.Empty;
|
|
|
|
configuringResultTextBox.Text =
|
|
CurrentSourceName ?? string.Empty;
|
|
|
|
selectedPathTextBox.Text =
|
|
string.Empty;
|
|
|
|
mappedResultsTextBox.Text =
|
|
string.Empty;
|
|
|
|
mappingStatusLabel.Text =
|
|
"Select an XML attribute or value.";
|
|
|
|
chooseButton.Enabled =
|
|
false;
|
|
|
|
LoadStructure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the customer XML example and builds the structure tree.
|
|
/// </summary>
|
|
private void LoadStructure()
|
|
{
|
|
xmlTreeView.BeginUpdate();
|
|
|
|
try
|
|
{
|
|
xmlTreeView.Nodes.Clear();
|
|
|
|
PayloadTemplateInspectionResult result =
|
|
inspector.Inspect(
|
|
TemplatePath);
|
|
|
|
if (result.Root == null)
|
|
return;
|
|
|
|
TreeNode rootNode =
|
|
CreateTreeNode(
|
|
result.Root);
|
|
|
|
xmlTreeView.Nodes.Add(
|
|
rootNode);
|
|
|
|
rootNode.Expand();
|
|
|
|
TreeNode currentNode =
|
|
FindTreeNode(
|
|
xmlTreeView.Nodes,
|
|
CurrentDestinationPath);
|
|
|
|
if (currentNode != null)
|
|
{
|
|
ExpandParents(
|
|
currentNode);
|
|
|
|
xmlTreeView.SelectedNode =
|
|
currentNode;
|
|
|
|
currentNode.EnsureVisible();
|
|
}
|
|
}
|
|
catch (Exception exc)
|
|
{
|
|
MessageBox.Show(
|
|
this,
|
|
"Failed to analyze XML payload example." +
|
|
Environment.NewLine +
|
|
Environment.NewLine +
|
|
exc.Message,
|
|
"XML payload structure",
|
|
MessageBoxButtons.OK,
|
|
MessageBoxIcon.Error);
|
|
}
|
|
finally
|
|
{
|
|
xmlTreeView.EndUpdate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates one visual tree node from the structural payload model.
|
|
/// </summary>
|
|
private TreeNode CreateTreeNode(
|
|
PayloadTemplateNode payloadNode)
|
|
{
|
|
string displayText =
|
|
payloadNode.DisplayText ??
|
|
payloadNode.Name ??
|
|
string.Empty;
|
|
|
|
IList<string> mappedBy =
|
|
GetConfiguredMappings(
|
|
payloadNode.DestinationPath);
|
|
|
|
bool isCurrentMapping =
|
|
payloadNode.IsSelectable &&
|
|
string.Equals(
|
|
payloadNode.DestinationPath,
|
|
CurrentDestinationPath,
|
|
StringComparison.Ordinal);
|
|
|
|
if (payloadNode.IsSelectable)
|
|
{
|
|
List<string> visibleMappings =
|
|
new List<string>();
|
|
|
|
if (mappedBy != null)
|
|
{
|
|
visibleMappings.AddRange(
|
|
mappedBy.Where(
|
|
value =>
|
|
!string.IsNullOrWhiteSpace(
|
|
value)));
|
|
}
|
|
|
|
if (isCurrentMapping &&
|
|
!string.IsNullOrWhiteSpace(
|
|
CurrentSourceName) &&
|
|
!visibleMappings.Contains(
|
|
CurrentSourceName))
|
|
{
|
|
visibleMappings.Insert(
|
|
0,
|
|
CurrentSourceName);
|
|
}
|
|
|
|
if (visibleMappings.Count > 0)
|
|
{
|
|
displayText +=
|
|
" <- " +
|
|
string.Join(
|
|
", ",
|
|
visibleMappings);
|
|
}
|
|
}
|
|
|
|
TreeNode treeNode =
|
|
new TreeNode(
|
|
displayText);
|
|
|
|
treeNode.Tag =
|
|
payloadNode;
|
|
|
|
ApplyMappingAppearance(
|
|
treeNode,
|
|
payloadNode,
|
|
mappedBy,
|
|
isCurrentMapping);
|
|
|
|
foreach (PayloadTemplateNode child
|
|
in payloadNode.Children)
|
|
{
|
|
treeNode.Nodes.Add(
|
|
CreateTreeNode(
|
|
child));
|
|
}
|
|
|
|
return treeNode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies mapping state colors and tooltips.
|
|
/// </summary>
|
|
private void ApplyMappingAppearance(
|
|
TreeNode treeNode,
|
|
PayloadTemplateNode payloadNode,
|
|
IList<string> mappedBy,
|
|
bool isCurrentMapping)
|
|
{
|
|
if (!payloadNode.IsSelectable)
|
|
{
|
|
if (payloadNode.IsRepeatingPrototypeRoot)
|
|
{
|
|
treeNode.NodeFont =
|
|
new Font(
|
|
xmlTreeView.Font,
|
|
FontStyle.Bold);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (isCurrentMapping)
|
|
{
|
|
treeNode.BackColor =
|
|
Color.LightBlue;
|
|
|
|
treeNode.ToolTipText =
|
|
"Current mapping for: " +
|
|
(CurrentSourceName ?? string.Empty);
|
|
|
|
return;
|
|
}
|
|
|
|
if (mappedBy != null &&
|
|
mappedBy.Count > 0)
|
|
{
|
|
if (payloadNode.DestinationKind ==
|
|
PayloadDestinationKind.RepeatingPrototype)
|
|
{
|
|
treeNode.BackColor =
|
|
Color.PaleGreen;
|
|
|
|
treeNode.ToolTipText =
|
|
"Repeating prototype destination. Mapped by: " +
|
|
string.Join(
|
|
", ",
|
|
mappedBy);
|
|
}
|
|
else
|
|
{
|
|
treeNode.BackColor =
|
|
Color.LightGoldenrodYellow;
|
|
|
|
treeNode.ToolTipText =
|
|
"One-time destination already mapped by: " +
|
|
string.Join(
|
|
", ",
|
|
mappedBy);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void xmlTreeView_AfterSelect(
|
|
object sender,
|
|
TreeViewEventArgs e)
|
|
{
|
|
PayloadTemplateNode payloadNode =
|
|
e.Node != null
|
|
? e.Node.Tag as PayloadTemplateNode
|
|
: null;
|
|
|
|
UpdateSelectedNodeInformation(
|
|
payloadNode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the detail section for the selected structural destination.
|
|
/// </summary>
|
|
private void UpdateSelectedNodeInformation(
|
|
PayloadTemplateNode payloadNode)
|
|
{
|
|
chooseButton.Enabled =
|
|
false;
|
|
|
|
selectedPathTextBox.Text =
|
|
string.Empty;
|
|
|
|
mappedResultsTextBox.Text =
|
|
string.Empty;
|
|
|
|
mappingStatusLabel.Text =
|
|
"Select an XML attribute or value.";
|
|
|
|
if (payloadNode == null ||
|
|
!payloadNode.IsSelectable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
selectedPathTextBox.Text =
|
|
payloadNode.DestinationPath ??
|
|
string.Empty;
|
|
|
|
IList<string> mappedBy =
|
|
GetConfiguredMappings(
|
|
payloadNode.DestinationPath);
|
|
|
|
mappedResultsTextBox.Text =
|
|
mappedBy != null &&
|
|
mappedBy.Count > 0
|
|
? string.Join(
|
|
", ",
|
|
mappedBy)
|
|
: "Not mapped";
|
|
|
|
bool isCurrentMapping =
|
|
string.Equals(
|
|
payloadNode.DestinationPath,
|
|
CurrentDestinationPath,
|
|
StringComparison.Ordinal);
|
|
|
|
if (isCurrentMapping)
|
|
{
|
|
mappingStatusLabel.Text =
|
|
"Current mapping.";
|
|
|
|
chooseButton.Enabled =
|
|
true;
|
|
|
|
return;
|
|
}
|
|
|
|
if (payloadNode.DestinationKind ==
|
|
PayloadDestinationKind.RepeatingPrototype)
|
|
{
|
|
if (mappedBy != null &&
|
|
mappedBy.Count > 0)
|
|
{
|
|
mappingStatusLabel.Text =
|
|
"Repeating prototype destination. Multiple TBF results can use this mapping.";
|
|
}
|
|
else
|
|
{
|
|
mappingStatusLabel.Text =
|
|
"Repeating prototype destination.";
|
|
}
|
|
|
|
chooseButton.Enabled =
|
|
true;
|
|
|
|
return;
|
|
}
|
|
|
|
if (mappedBy != null &&
|
|
mappedBy.Count > 0)
|
|
{
|
|
mappingStatusLabel.Text =
|
|
"This one-time destination is already mapped.";
|
|
|
|
chooseButton.Enabled =
|
|
false;
|
|
|
|
return;
|
|
}
|
|
|
|
mappingStatusLabel.Text =
|
|
"Destination is available.";
|
|
|
|
chooseButton.Enabled =
|
|
true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns configured source names for a destination.
|
|
/// </summary>
|
|
private IList<string> GetConfiguredMappings(
|
|
string destinationPath)
|
|
{
|
|
if (ConfiguredMappings == null ||
|
|
string.IsNullOrWhiteSpace(
|
|
destinationPath))
|
|
{
|
|
return new List<string>();
|
|
}
|
|
|
|
foreach (KeyValuePair<string, IList<string>> pair
|
|
in ConfiguredMappings)
|
|
{
|
|
if (string.Equals(
|
|
pair.Key,
|
|
destinationPath,
|
|
StringComparison.Ordinal))
|
|
{
|
|
return pair.Value ??
|
|
new List<string>();
|
|
}
|
|
}
|
|
|
|
return new List<string>();
|
|
}
|
|
|
|
private void chooseButton_Click(
|
|
object sender,
|
|
EventArgs e)
|
|
{
|
|
ChooseSelectedDestination();
|
|
}
|
|
|
|
private void xmlTreeView_NodeMouseDoubleClick(
|
|
object sender,
|
|
TreeNodeMouseClickEventArgs e)
|
|
{
|
|
if (e.Node == null)
|
|
return;
|
|
|
|
xmlTreeView.SelectedNode =
|
|
e.Node;
|
|
|
|
if (chooseButton.Enabled)
|
|
{
|
|
ChooseSelectedDestination();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the selected destination and closes the dialog.
|
|
/// </summary>
|
|
private void ChooseSelectedDestination()
|
|
{
|
|
TreeNode selectedTreeNode =
|
|
xmlTreeView.SelectedNode;
|
|
|
|
if (selectedTreeNode == null)
|
|
return;
|
|
|
|
PayloadTemplateNode payloadNode =
|
|
selectedTreeNode.Tag
|
|
as PayloadTemplateNode;
|
|
|
|
if (payloadNode == null ||
|
|
!payloadNode.IsSelectable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
SelectedDestinationPath =
|
|
payloadNode.DestinationPath;
|
|
|
|
DialogResult =
|
|
DialogResult.OK;
|
|
|
|
Close();
|
|
}
|
|
|
|
private TreeNode FindTreeNode(
|
|
TreeNodeCollection nodes,
|
|
string destinationPath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(
|
|
destinationPath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (TreeNode node
|
|
in nodes)
|
|
{
|
|
PayloadTemplateNode payloadNode =
|
|
node.Tag
|
|
as PayloadTemplateNode;
|
|
|
|
if (payloadNode != null &&
|
|
string.Equals(
|
|
payloadNode.DestinationPath,
|
|
destinationPath,
|
|
StringComparison.Ordinal))
|
|
{
|
|
return node;
|
|
}
|
|
|
|
TreeNode childResult =
|
|
FindTreeNode(
|
|
node.Nodes,
|
|
destinationPath);
|
|
|
|
if (childResult != null)
|
|
return childResult;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void ExpandParents(
|
|
TreeNode node)
|
|
{
|
|
TreeNode parent =
|
|
node.Parent;
|
|
|
|
while (parent != null)
|
|
{
|
|
parent.Expand();
|
|
|
|
parent =
|
|
parent.Parent;
|
|
}
|
|
}
|
|
}
|
|
} |