Compare commits

...

2 Commits

Author SHA1 Message Date
d3c8813012 <Fix>: Correct test-dependent result evaluation in ResultsWriter, increase revision to 3.9.3145.101
Cause:

- ResultsWriter evaluated selected test-dependent items without the actual TestID.
- This caused values such as Test passed(), timestamps, flow, pressure, temperature, conductivity and error data to be empty or incorrect.

Solution:

1. Fixed test-aware result evaluation
   - Uses published regular meter test results.
   - Passes mtr.Name() as TestID to item.Print(wm, testId).

2. Restored correct test result mapping
   - Test-dependent values are now resolved from the correct TestRslt / MeterTestRslt.

3. Increased revision
   - Updated revision to 3.9.3145.101.
2026-09-03 10:00:40 +02:00
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
29 changed files with 8300 additions and 834 deletions

View File

@ -1,4 +1,8 @@
using System;
///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
@ -11,9 +15,9 @@ namespace Results.Forms
public partial class ResultsConfigCtrl : UserControl
{
/// <summary>
/// ListViewEx columns
/// ListViewEx columns.
/// </summary>
enum Column
private enum Column
{
Item,
Caption,
@ -22,83 +26,121 @@ namespace Results.Forms
Precision,
Width,
Alignment,
Merge,
TestID,
Count,
Merge,
TestID,
Count,
}
Control[] editors; /// all editors except of units
ComboBox unitsCB; /// units combo box
private Control[] editors;
private ComboBox unitsCB;
private bool unlocked;
private string captionColumnText;
public MetersKind MetersKind;
public IList<WMeterRsltItemSpec> SelectedItems;
public MetersKind MetersKind;
public IList<WMeterRsltItemSpec> SelectedItems;
public bool SupressTestIDColumn;
public bool Unlocked
{
set
{
availableTabControl.Enabled = value;
selectedResultsListViewEx.Enabled = value;
addButton.Enabled = value;
removeButton.Enabled = value;
removeAllButton.Enabled = value;
upButton.Enabled = value;
downButton.Enabled = value;
unlocked = value;
}
get { return unlocked; }
}
bool unlocked;
/// <summary>
/// Gets or sets an optional callback used to select a Caption value.
/// </summary>
/// <remarks>
/// When null, Caption keeps the original TextBox editor. When assigned,
/// clicking Caption invokes the callback. Returning null means Cancel.
/// </remarks>
public Func<WMeterRsltItemSpec, string> CaptionPicker
{
get;
set;
}
public ResultsConfigCtrl(bool supressTestIDColumn)
: this()
{
this.SupressTestIDColumn = supressTestIDColumn;
}
/// <summary>
/// Gets or sets the Caption column header text.
/// </summary>
public string CaptionColumnText
{
get { return captionColumnText; }
set
{
captionColumnText = value;
if (selectedResultsListViewEx.Columns.Count > (int)Column.Caption)
{
selectedResultsListViewEx.Columns[(int)Column.Caption].Text =
string.IsNullOrWhiteSpace(value)
? Strings.Caption
: value;
}
}
}
public bool Unlocked
{
set
{
availableTabControl.Enabled = value;
selectedResultsListViewEx.Enabled = value;
addButton.Enabled = value;
removeButton.Enabled = value;
removeAllButton.Enabled = value;
upButton.Enabled = value;
downButton.Enabled = value;
unlocked = value;
}
get { return unlocked; }
}
public ResultsConfigCtrl(bool supressTestIDColumn)
: this()
{
SupressTestIDColumn = supressTestIDColumn;
}
public ResultsConfigCtrl()
{
InitializeComponent();
}
{
InitializeComponent();
}
void Localize()
{
Text = Strings.Configuration;
availableResultsLabel.Text = Strings.Available_results;
availableTabControl.TabPages[0].Text = Strings.Quantity;
availableTabControl.TabPages[1].Text = Strings.Category;
availableTabControl.TabPages[2].Text = "A...Z";
void Localize()
{
Text = Strings.Configuration;
availableResultsLabel.Text = Strings.Available_results;
availableTabControl.TabPages[0].Text = Strings.Quantity;
availableTabControl.TabPages[1].Text = Strings.Category;
availableTabControl.TabPages[2].Text = "A...Z";
selectedResultsLabel.Text = Strings.Selected_results;
selectedResultsLabel.Text = Strings.Selected_results;
addButton.Text = Strings.Add;
removeButton.Text = Strings.Remove;
removeAllButton.Text = Strings.Remove_all;
addButton.Text = Strings.Add;
removeButton.Text = Strings.Remove;
removeAllButton.Text = Strings.Remove_all;
upButton.Text = Strings.UpBtnText;
downButton.Text = Strings.DownBtnText;
}
}
private void ResultsConfigCtrl_Load(object sender, EventArgs e)
{
Localize();
private void ResultsConfigCtrl_Load(object sender, EventArgs e)
{
Localize();
/// Add columns to ListViewEx
selectedResultsListViewEx.Columns.Add(new ColumnHeader { Text = Strings.Item, Width = 120 });
selectedResultsListViewEx.Columns.Add(new ColumnHeader { Text = Strings.Caption });
selectedResultsListViewEx.Columns.Add(
new ColumnHeader
{
Text = string.IsNullOrWhiteSpace(captionColumnText)
? Strings.Caption
: captionColumnText
});
selectedResultsListViewEx.Columns.Add(new ColumnHeader { Text = Strings.Units });
selectedResultsListViewEx.Columns.Add(new ColumnHeader { Text = Strings.Format });
selectedResultsListViewEx.Columns.Add(new ColumnHeader { Text = Strings.Precision });
selectedResultsListViewEx.Columns.Add(new ColumnHeader { Text = Strings.Width });
selectedResultsListViewEx.Columns.Add(new ColumnHeader { Text = Strings.Alignment });
selectedResultsListViewEx.Columns.Add(new ColumnHeader { Text = Strings.Merge });
if (!SupressTestIDColumn)
{
selectedResultsListViewEx.Columns.Add(new ColumnHeader { Text = Strings.Test_ID });
}
/// Create controls used by ListViewEx to edit items
if (!SupressTestIDColumn)
{
selectedResultsListViewEx.Columns.Add(new ColumnHeader { Text = Strings.Test_ID });
}
unitsCB = new ComboBox();
var alignmentCB = new ComboBox();
@ -112,17 +154,18 @@ namespace Results.Forms
mergeCB.Items.Add(Strings.Yes);
editors = new Control[]
{
null,
new TextBox(), /// caption
unitsCB,
new TextBox(), /// format
new TextBox(), /// precision
new TextBox(), /// width
alignmentCB,
mergeCB,
new TextBox(), /// testID
};
{
null,
new TextBox(),
unitsCB,
new TextBox(),
new TextBox(),
new TextBox(),
alignmentCB,
mergeCB,
new TextBox(),
};
foreach (var edi in editors)
{
if (edi != null)
@ -132,31 +175,59 @@ namespace Results.Forms
}
}
selectedResultsListViewEx.SubItemClicked += new SubItemEventHandler(selectedResultsListViewEx_SubItemClicked);
selectedResultsListViewEx.SubItemEndEditing += new SubItemEndEditingEventHandler(selectedResultsListViewEx_SubItemEndEditing);
availableByQuantityTreeView.ShowNodeToolTips = true;
availableByCategoryTreeView.ShowNodeToolTips = true;
availableAlphabeticTreeView.ShowNodeToolTips = true;
selectedResultsListViewEx.SubItemClicked +=
new SubItemEventHandler(selectedResultsListViewEx_SubItemClicked);
selectedResultsListViewEx.SubItemEndEditing +=
new SubItemEndEditingEventHandler(selectedResultsListViewEx_SubItemEndEditing);
availableByQuantityTreeView.ShowNodeToolTips = true;
availableByCategoryTreeView.ShowNodeToolTips = true;
availableAlphabeticTreeView.ShowNodeToolTips = true;
RedrawAvailable();
RedrawSelected();
}
RedrawSelected();
}
void selectedResultsListViewEx_SubItemClicked(object sender, SubItemEventArgs e)
{
if (e.SubItem == (int)Column.Units)
{
Quantity quantity = (e.Item.Tag as WMeterRsltItemSpec).Quantity;
unitsCB.Items.Clear();
unitsCB.Items.Add(Unit.None.ToDescription()); /// "---"
for (Unit u = (Unit)1; u < Unit.Count; u++)
{
if (Units.IsQuantity(u, quantity)) unitsCB.Items.Add(u.ToDescription());
}
selectedResultsListViewEx.StartEditing(unitsCB, e.Item, e.SubItem);
}
else if ((e.SubItem > 0) && (e.SubItem < (int)(SupressTestIDColumn ? Column.TestID : Column.Count)))
if (e.SubItem == (int)Column.Caption && CaptionPicker != null)
{
WMeterRsltItemSpec item = e.Item.Tag as WMeterRsltItemSpec;
if (item == null) return;
string selectedCaption = CaptionPicker(item);
if (selectedCaption != null)
{
item.Caption = selectedCaption;
e.Item.SubItems[e.SubItem].Text = selectedCaption;
}
return;
}
if (e.SubItem == (int)Column.Units)
{
WMeterRsltItemSpec item = e.Item.Tag as WMeterRsltItemSpec;
if (item == null) return;
Quantity quantity = item.Quantity;
unitsCB.Items.Clear();
unitsCB.Items.Add(Unit.None.ToDescription());
for (Unit u = (Unit)1; u < Unit.Count; u++)
{
if (Units.IsQuantity(u, quantity))
{
unitsCB.Items.Add(u.ToDescription());
}
}
selectedResultsListViewEx.StartEditing(unitsCB, e.Item, e.SubItem);
}
else if ((e.SubItem > 0) &&
(e.SubItem < (int)(SupressTestIDColumn ? Column.TestID : Column.Count)))
{
selectedResultsListViewEx.StartEditing(editors[e.SubItem], e.Item, e.SubItem);
}
@ -169,30 +240,39 @@ namespace Results.Forms
switch ((Column)e.SubItem)
{
case Column.Caption: item.Caption = e.DisplayText; return;
case Column.Caption:
item.Caption = e.DisplayText;
return;
case Column.Units:
for (Unit u = 0; u < Unit.Count; u++)
{
if (u.ToDescription().Equals(unitsCB.Text))
if (u.ToDescription().Equals(unitsCB.Text))
{
item.Units = u;
return; /// OK
return;
}
}
break; /// Error
break;
case Column.Format: item.Format = e.DisplayText; return;
case Column.Precision: item.Precision = e.DisplayText; return;
case Column.Width:
{
int width;
if (Int32.TryParse(editors[e.SubItem].Text, out width) && width >= 0)
{
item.Width = width;
return; /// OK
}
break; /// Error
}
case Column.Format:
item.Format = e.DisplayText;
return;
case Column.Precision:
item.Precision = e.DisplayText;
return;
case Column.Width:
{
int width;
if (Int32.TryParse(editors[e.SubItem].Text, out width) && width >= 0)
{
item.Width = width;
return;
}
break;
}
case Column.Alignment:
for (Alignment a = 0; a < Alignment.Count; a++)
@ -203,7 +283,7 @@ namespace Results.Forms
return;
}
}
break; /// Error
break;
case Column.Merge:
if (editors[e.SubItem].Text == Strings.Yes)
@ -216,254 +296,242 @@ namespace Results.Forms
item.Merge = false;
return;
}
break; /// Error
break;
case Column.TestID: item.TestID = e.DisplayText; return;
default:
return; /// OK
case Column.TestID:
item.TestID = e.DisplayText;
return;
default:
return;
}
e.DisplayText = e.Item.SubItems[e.SubItem].Text;
e.Cancel = true;
return;
}
void RedrawAvailable()
{
RedrawByQuantity(availableByQuantityTreeView);
RedrawByCategory(availableByCategoryTreeView);
RedrawInAlphabeticOrder(availableAlphabeticTreeView);
}
/// <summary>
/// Redraw available items (right hand side)
/// </summary>
void RedrawAvailable()
{
RedrawByQuantity(availableByQuantityTreeView);
RedrawByCategory(availableByCategoryTreeView);
RedrawInAlphabeticOrder(availableAlphabeticTreeView);
}
void RedrawInAlphabeticOrder(TreeView treeView)
{
treeView.Nodes.Clear();
IList<WMeterRsltItemSpec> alphabeticList =
WMeterRsltItemSpec.AllItems.OrderBy(x => x.Name).ToList();
void RedrawInAlphabeticOrder(TreeView treeView)
{
treeView.Nodes.Clear();
foreach (var item in alphabeticList)
{
TreeNode node = new TreeNode(item.Name);
node.Tag = item;
node.ToolTipText = item.ToolTipText;
treeView.Nodes.Add(node);
}
}
IList<WMeterRsltItemSpec> alphabeticlList = WMeterRsltItemSpec.AllItems.OrderBy(x => x.Name).ToList();
///
foreach (var item in alphabeticlList)
{
TreeNode node = new TreeNode(item.Name);
node.Tag = item;
node.ToolTipText = item.ToolTipText;
treeView.Nodes.Add(node);
}
}
void RedrawByQuantity(TreeView treeView)
{
treeView.Nodes.Clear();
IList<Quantity> quantities = new List<Quantity>();
for (Quantity q = 0; q < Quantity.Count; q++) quantities.Add(q);
void RedrawByQuantity(TreeView treeView)
{
treeView.Nodes.Clear();
IList<Quantity> sortedQuantities =
quantities.OrderBy(x => x.ToDescription()).ToList();
IList<Quantity> quantities = new List<Quantity>();
for (Quantity q = 0; q < Quantity.Count; q++) quantities.Add(q);
foreach (var q in sortedQuantities)
{
int n = 0;
IList<Quantity> sortedQuantities = quantities.OrderBy(x => x.ToDescription()).ToList();
foreach (var ri in WMeterRsltItemSpec.AllItems)
{
if (ri.Quantity == q) n++;
}
foreach (var q in sortedQuantities)
{
int n = 0;
foreach (var ri in WMeterRsltItemSpec.AllItems)
{
if (ri.Quantity == q) n++;
}
if (n > 0)
{
TreeNode[] array = new TreeNode[n];
int i = 0;
if (n > 0)
{
TreeNode[] array = new TreeNode[n];
int i = 0;
foreach (var ri in WMeterRsltItemSpec.AllItems)
{
if (ri.Quantity == q)
{
TreeNode node = new TreeNode(ri.Name);
node.Tag = ri;
node.ToolTipText = ri.ToolTipText;
array[i++] = node;
}
}
foreach (var ri in WMeterRsltItemSpec.AllItems)
{
if (ri.Quantity == q)
{
TreeNode node = new TreeNode(ri.Name);
node.Tag = ri;
node.ToolTipText = ri.ToolTipText;
array[i++] = node;
}
}
treeView.Nodes.Add(new TreeNode(q.ToDescription(), array));
}
}
}
treeView.Nodes.Add(new TreeNode(q.ToDescription(), array));
}
}
}
void RedrawByCategory(TreeView treeView)
{
treeView.Nodes.Clear();
void RedrawByCategory(TreeView treeView)
{
treeView.Nodes.Clear();
IList<ItemCategory> categories = new List<ItemCategory>();
for (ItemCategory c = 0; c < ItemCategory.Count; c++) categories.Add(c);
IList<ItemCategory> categories = new List<ItemCategory>();
for (ItemCategory c = 0; c < ItemCategory.Count; c++) categories.Add(c);
IList<ItemCategory> sortedCategories =
categories.OrderBy(x => x.ToDescription()).ToList();
IList<ItemCategory> sortedCategories = categories.OrderBy(x => x.ToDescription()).ToList();
foreach (var c in sortedCategories)
{
int n = 0;
foreach (var c in sortedCategories)
{
int n = 0;
foreach (var ri in WMeterRsltItemSpec.AllItems)
{
if (ri.Category == c) n++;
}
foreach (var ri in WMeterRsltItemSpec.AllItems)
{
if (ri.Category == c) n++;
}
if (n > 0)
{
TreeNode[] array = new TreeNode[n];
int i = 0;
foreach (var ri in WMeterRsltItemSpec.AllItems)
{
if (ri.Category == c)
{
TreeNode node = new TreeNode(ri.Name);
node.Tag = ri;
node.ToolTipText = ri.ToolTipText;
array[i++] = node;
}
}
if (n > 0)
{
TreeNode[] array = new TreeNode[n];
int i = 0;
treeView.Nodes.Add(new TreeNode(c.ToDescription(), array));
}
}
}
foreach (var ri in WMeterRsltItemSpec.AllItems)
{
if (ri.Category == c)
{
TreeNode node = new TreeNode(ri.Name);
node.Tag = ri;
node.ToolTipText = ri.ToolTipText;
array[i++] = node;
}
}
treeView.Nodes.Add(new TreeNode(c.ToDescription(), array));
}
}
}
/// <summary>
/// Redraw selected items (right hand side)
/// </summary>
void RedrawSelected()
{
selectedResultsListViewEx.Items.Clear();
void RedrawSelected()
{
selectedResultsListViewEx.Items.Clear();
if (SelectedItems == null) return;
if (SelectedItems == null) return;
foreach (var item in SelectedItems)
{
ListViewItem lvi = new ListViewItem(item.Name); /// Item
foreach (var item in SelectedItems)
{
ListViewItem lvi = new ListViewItem(item.Name);
lvi.Tag = item;
lvi.SubItems.Add(item.Caption); /// Header
lvi.SubItems.Add(item.Units.ToDescription()); /// Units
lvi.SubItems.Add(item.Format); /// Format
lvi.SubItems.Add(item.Precision); /// Precision
lvi.SubItems.Add(item.Width.ToString()); /// Width
lvi.SubItems.Add(item.Alignment.ToDescription()); /// Alignment
lvi.SubItems.Add(item.Merge ? Strings.Yes : Strings.No); /// Merge
if (!SupressTestIDColumn)
{
lvi.SubItems.Add(item.TestID); /// TestID
}
lvi.SubItems.Add(item.Caption);
lvi.SubItems.Add(item.Units.ToDescription());
lvi.SubItems.Add(item.Format);
lvi.SubItems.Add(item.Precision);
lvi.SubItems.Add(item.Width.ToString());
lvi.SubItems.Add(item.Alignment.ToDescription());
lvi.SubItems.Add(item.Merge ? Strings.Yes : Strings.No);
selectedResultsListViewEx.Items.Add(lvi);
}
}
if (!SupressTestIDColumn)
{
lvi.SubItems.Add(item.TestID);
}
selectedResultsListViewEx.Items.Add(lvi);
}
}
void UpdateSelectedFromView()
{
}
void addButton_Click(object sender, EventArgs e)
{
switch (availableTabControl.SelectedIndex)
{
case 0:
availableByQuantityTreeView_DoubleClick(this, null);
break;
case 1:
availableByCategoryTreeView_DoubleClick(this, null);
break;
case 2:
availableAlphabeticTreeView_DoubleClick(this, null);
break;
}
}
void addButton_Click(object sender, EventArgs e)
{
switch (availableTabControl.SelectedIndex)
{
case 0:
availableByQuantityTreeView_DoubleClick(this, null);
break;
case 1:
availableByCategoryTreeView_DoubleClick(this, null);
break;
case 2:
availableAlphabeticTreeView_DoubleClick(this, null);
break;
default:
break;
}
}
private void availableByQuantityTreeView_DoubleClick(object sender, EventArgs e)
{
if (availableByQuantityTreeView.SelectedNode != null &&
availableByQuantityTreeView.SelectedNode.Tag is WMeterRsltItemSpec)
{
AddItem(availableByQuantityTreeView.SelectedNode.Tag as WMeterRsltItemSpec);
}
}
private void availableByQuantityTreeView_DoubleClick(object sender, EventArgs e)
{
if (availableByQuantityTreeView.SelectedNode != null &&
availableByQuantityTreeView.SelectedNode.Tag is WMeterRsltItemSpec)
{
AddItem(availableByQuantityTreeView.SelectedNode.Tag as WMeterRsltItemSpec);
}
}
private void availableByCategoryTreeView_DoubleClick(object sender, EventArgs e)
{
if (availableByCategoryTreeView.SelectedNode != null &&
availableByCategoryTreeView.SelectedNode.Tag is WMeterRsltItemSpec)
{
AddItem(availableByCategoryTreeView.SelectedNode.Tag as WMeterRsltItemSpec);
}
}
private void availableByCategoryTreeView_DoubleClick(object sender, EventArgs e)
{
if (availableByCategoryTreeView.SelectedNode != null &&
availableByCategoryTreeView.SelectedNode.Tag is WMeterRsltItemSpec)
{
AddItem(availableByCategoryTreeView.SelectedNode.Tag as WMeterRsltItemSpec);
}
}
private void availableAlphabeticTreeView_DoubleClick(object sender, EventArgs e)
{
if (availableAlphabeticTreeView.SelectedNode != null &&
availableAlphabeticTreeView.SelectedNode.Tag is WMeterRsltItemSpec)
{
AddItem(availableAlphabeticTreeView.SelectedNode.Tag as WMeterRsltItemSpec);
}
}
private void availableAlphabeticTreeView_DoubleClick(object sender, EventArgs e)
{
if (availableAlphabeticTreeView.SelectedNode != null &&
availableAlphabeticTreeView.SelectedNode.Tag is WMeterRsltItemSpec)
{
AddItem(availableAlphabeticTreeView.SelectedNode.Tag as WMeterRsltItemSpec);
}
}
void AddItem(WMeterRsltItemSpec item)
{
if (SelectedItems == null)
{
SelectedItems = new List<WMeterRsltItemSpec>();
}
void AddItem(WMeterRsltItemSpec item)
{
WMeterRsltItemSpec newItem = item.Clone();
newItem.Caption = newItem.Name;
SelectedItems.Add(newItem);
RedrawSelected();
WMeterRsltItemSpec newItem = item.Clone();
newItem.Caption = newItem.Name;
SelectedItems.Add(newItem);
/// Select the last item
selectedResultsListViewEx.Focus();
selectedResultsListViewEx.Items[SelectedItems.Count - 1].Selected = true;
selectedResultsListViewEx.Items[SelectedItems.Count - 1].EnsureVisible();
}
RedrawSelected();
selectedResultsListViewEx.Focus();
selectedResultsListViewEx.Items[SelectedItems.Count - 1].Selected = true;
selectedResultsListViewEx.Items[SelectedItems.Count - 1].EnsureVisible();
}
private void selectedResultsListViewEx_DoubleClick(object sender, EventArgs e)
{
/// Double click works when just one item is selected
private void selectedResultsListViewEx_DoubleClick(object sender, EventArgs e)
{
if (selectedResultsListViewEx.SelectedIndices.Count == 1)
{
SelectedItems.RemoveAt(selectedResultsListViewEx.SelectedIndices[0]);
RedrawAvailable();
RedrawSelected();
}
}
if (selectedResultsListViewEx.SelectedIndices.Count == 1)
{
SelectedItems.RemoveAt(selectedResultsListViewEx.SelectedIndices[0]);
RedrawAvailable();
RedrawSelected();
}
}
void removeButton_Click(object sender, EventArgs e)
{
for (int i = selectedResultsListViewEx.SelectedIndices.Count - 1; i >= 0; i--)
{
SelectedItems.RemoveAt(selectedResultsListViewEx.SelectedIndices[i]);
}
void removeButton_Click(object sender, EventArgs e)
{
/// Remove from the list (the last selected item first so that the indexes are not affected)
RedrawAvailable();
RedrawSelected();
}
for (int i = selectedResultsListViewEx.SelectedIndices.Count - 1; i >= 0; i--)
{
SelectedItems.RemoveAt(selectedResultsListViewEx.SelectedIndices[i]);
}
RedrawAvailable();
RedrawSelected();
}
void removeAllButton_Click(object sender, EventArgs e)
{
/// Remove all items from 'Selected' list
SelectedItems.Clear();
RedrawAvailable();
RedrawSelected();
}
//void okButton_Click(object sender, EventArgs e)
//{
// DialogResult = DialogResult.OK;
// Close();
//}
void removeAllButton_Click(object sender, EventArgs e)
{
SelectedItems.Clear();
RedrawAvailable();
RedrawSelected();
}
private void ResultsConfigCtrl_KeyPress(object sender, KeyPressEventArgs e)
{
@ -475,9 +543,9 @@ namespace Results.Forms
if (selectedResultsListViewEx.SelectedIndices.Count != 1) return;
int selIdx = selectedResultsListViewEx.SelectedIndices[0];
if (selIdx == 0)
{
/// Cannot move up
selectedResultsListViewEx.Focus();
selectedResultsListViewEx.Items[0].Selected = true;
return;
@ -499,9 +567,9 @@ namespace Results.Forms
if (selectedResultsListViewEx.SelectedIndices.Count != 1) return;
int selIdx = selectedResultsListViewEx.SelectedIndices[0];
if (selIdx == SelectedItems.Count - 1)
{
/// Cannot move down
selectedResultsListViewEx.Focus();
selectedResultsListViewEx.Items[SelectedItems.Count - 1].Selected = true;
return;
@ -510,7 +578,7 @@ namespace Results.Forms
WMeterRsltItemSpec tmp = SelectedItems[selIdx + 1];
SelectedItems[selIdx + 1] = SelectedItems[selIdx];
SelectedItems[selIdx] = tmp;
RedrawSelected();
selectedResultsListViewEx.Focus();
@ -518,16 +586,12 @@ namespace Results.Forms
selectedResultsListViewEx.Items[selIdx + 1].EnsureVisible();
}
//private void cancelButton_Click(object sender, EventArgs e)
//{
// DialogResult = DialogResult.Cancel;
// Close();
//}
private void availableByCategoryTreeView_NodeMouseHover2(object sender, TreeNodeMouseHoverEventArgs e)
{
ToolTip toolTip = new ToolTip();
toolTip.SetToolTip(this, e.Node.ToolTipText);
}
private void availableByCategoryTreeView_NodeMouseHover2(
object sender,
TreeNodeMouseHoverEventArgs e)
{
ToolTip toolTip = new ToolTip();
toolTip.SetToolTip(this, e.Node.ToolTipText);
}
}
}
}

View File

@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// Build Number
// Revision
//
[assembly: AssemblyVersion("3.9.3145.0")]
[assembly: AssemblyFileVersion("3.9.3145.0")]
[assembly: AssemblyVersion("3.9.3145.101")]
[assembly: AssemblyFileVersion("3.9.3145.101")]

File diff suppressed because it is too large Load Diff

View File

@ -9,19 +9,27 @@ using TBF.Rig.Generic;
namespace TBF.Rig.Output.DB.ResultsWriter
{
public class ResultsWriterCfg : ComponentCfgBase, IComponentCfg
/// <summary>
/// Configuration of the ResultsWriter component.
/// </summary>
public class ResultsWriterCfg :
ComponentCfgBase,
IComponentCfg
{
public static XmlSerializer Serializer =
XmlSerializer.FromTypes(new[] { typeof(ResultsWriterCfg) })[0];
XmlSerializer.FromTypes(
new[] { typeof(ResultsWriterCfg) })[0];
public override XmlSerializer GetSerializer()
{
return Serializer;
}
public IComponentCfgCtrl GetControl(IList<Config.Entities.Component> cmpntEntities)
public IComponentCfgCtrl GetControl(
IList<Config.Entities.Component> cmpntEntities)
{
return new ResultsWriterCfgCtrl(cmpntEntities);
return new ResultsWriterCfgCtrl(
cmpntEntities);
}
/// <summary>
@ -34,25 +42,34 @@ namespace TBF.Rig.Output.DB.ResultsWriter
/// </summary>
public string StorageName;
/// Runtime model used ResultsConfigCtrl
/// <summary>
/// Runtime result model used by ResultsConfigCtrl.
/// </summary>
[XmlIgnore]
public List<WMeterRsltItemSpec> SelectedItems;
/// <summary>
/// Serialized representation of selected result items.
/// </summary>
public string[] Items;
/// Serializable model
/// <summary>
/// Serializable model retained for compatibility.
/// </summary>
public List<ResultsWriterItemCfg> SelectedItemsCfg;
ResultsWriterCfg()
{
ParentName = string.Empty; // here should be UniDataStorageWriter component name
ParentName = string.Empty;
SelectedItems = new List<WMeterRsltItemSpec>();
SelectedItemsCfg = new List<ResultsWriterItemCfg>();
Enabled = true;
StorageName = "Results";
}
public ResultsWriterCfg(string name, IComponentFactory factory)
public ResultsWriterCfg(
string name,
IComponentFactory factory)
: this()
{
Name = name;
@ -67,22 +84,35 @@ namespace TBF.Rig.Output.DB.ResultsWriter
ParentName,
Enabled,
StorageName,
SelectedItems != null ? SelectedItems.Count : 0);
SelectedItems != null
? SelectedItems.Count
: 0);
}
/// <summary>
/// Copies runtime result items into the serialized model.
/// </summary>
public void UpdateSerializableModel()
{
Items = WMeterRsltItemSpec.ToStrArray(SelectedItems);
Items =
WMeterRsltItemSpec.ToStrArray(
SelectedItems);
}
/// <summary>
/// Recreates runtime result items after deserialization.
/// </summary>
public void UpdateRuntimeModel()
{
SelectedItems = new List<WMeterRsltItemSpec>();
SelectedItems =
new List<WMeterRsltItemSpec>();
if (Items == null)
return;
SelectedItems.AddRange(WMeterRsltItemSpec.FromStrArray(Items));
SelectedItems.AddRange(
WMeterRsltItemSpec.FromStrArray(
Items));
}
}
}
}

View File

@ -8,37 +8,53 @@ using System.Windows.Forms;
using Common;
using Config.Entities;
using TBF.Rig.Generic;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters;
namespace TBF.Rig.Output.DB.ResultsWriter
{
public partial class ResultsWriterCfgCtrl : Configs.ConfigCtrlUtils, IComponentCfgCtrl
public partial class ResultsWriterCfgCtrl :
Configs.ConfigCtrlUtils,
IComponentCfgCtrl
{
public bool ShowMore { get { return false; } }
public bool ShowMore
{
get { return false; }
}
ResultsWriterCfg config;
IList<Component> cmpntEntities;
bool resultsConfigChanged;
private ResultsWriterCfg config;
private IList<Component> cmpntEntities;
private bool resultsConfigChanged;
public IComponentCfg Config
{
get { return config as IComponentCfg; }
set
{
config = value as ResultsWriterCfg;
config =
value as ResultsWriterCfg;
Redraw();
}
}
public ResultsWriterCfgCtrl(IList<Component> cmpntEntities)
public ResultsWriterCfgCtrl(
IList<Component> cmpntEntities)
{
InitializeComponent();
this.cmpntEntities = cmpntEntities;
this.cmpntEntities =
cmpntEntities;
}
private void ResultsWriterCfgCtrl_Load(object sender, EventArgs e)
private void ResultsWriterCfgCtrl_Load(
object sender,
EventArgs e)
{
if (config == null) return;
if (config == null)
return;
Redraw();
}
@ -46,42 +62,59 @@ namespace TBF.Rig.Output.DB.ResultsWriter
{
}
void Redraw()
private void Redraw()
{
if (config == null) return;
if (config == null)
return;
config.UpdateRuntimeModel();
classNameLabel.Text = config.Factory.ClassName;
nameTextBox.Text = config.Name;
enabledCheckBox.Checked = config.Enabled;
storageNameTextBox.Text = config.StorageName;
classNameLabel.Text =
config.Factory.ClassName;
nameTextBox.Text =
config.Name;
enabledCheckBox.Checked =
config.Enabled;
storageNameTextBox.Text =
config.StorageName;
parentComboBox.Items.Clear();
parentComboBox.Items.Add(string.Empty);
parentComboBox.Items.Add(
string.Empty);
if (cmpntEntities != null)
{
foreach (Component cmpnt in cmpntEntities)
foreach (Component cmpnt
in cmpntEntities)
{
if (cmpnt == null) continue;
if (cmpnt == null)
continue;
// for now, a simple filter by name/classname
if (cmpnt.ClassName != null &&
cmpnt.ClassName.IndexOf("UniDataStorageWriter") >= 0)
cmpnt.ClassName.IndexOf(
"UniDataStorageWriter") >= 0)
{
parentComboBox.Items.Add(cmpnt.Name);
parentComboBox.Items.Add(
cmpnt.Name);
}
}
}
parentComboBox.Text = config.ParentName;
parentComboBox.Text =
config.ParentName;
selectedItemsLabel.Text = string.Format(
"{0} selected item(s)",
config.SelectedItems != null ? config.SelectedItems.Count : 0);
selectedItemsLabel.Text =
string.Format(
"{0} selected item(s)",
config.SelectedItems != null
? config.SelectedItems.Count
: 0);
resultsConfigChanged = false;
resultsConfigChanged =
false;
}
public void Unlock()
@ -94,23 +127,35 @@ namespace TBF.Rig.Output.DB.ResultsWriter
previewRequestButton.Enabled = true;
}
public CfgUpdateFlags VerifyCfg(ref string message)
public CfgUpdateFlags VerifyCfg(
ref string message)
{
if (string.IsNullOrEmpty(nameTextBox.Text))
if (string.IsNullOrEmpty(
nameTextBox.Text))
{
message = "Component name is empty.";
message =
"Component name is empty.";
return CfgUpdateFlags.Error;
}
if (enabledCheckBox.Checked && string.IsNullOrEmpty(parentComboBox.Text))
if (enabledCheckBox.Checked &&
string.IsNullOrEmpty(
parentComboBox.Text))
{
message = "Parent UniDataStorageWriter is not selected.";
message =
"Parent UniDataStorageWriter is not selected.";
return CfgUpdateFlags.Error;
}
if (enabledCheckBox.Checked && string.IsNullOrEmpty(storageNameTextBox.Text))
if (enabledCheckBox.Checked &&
string.IsNullOrEmpty(
storageNameTextBox.Text))
{
message = "Storage name is empty.";
message =
"Storage name is empty.";
return CfgUpdateFlags.Error;
}
@ -119,89 +164,140 @@ namespace TBF.Rig.Output.DB.ResultsWriter
public CfgUpdateFlags UpdateCfg()
{
CfgUpdateFlags flags = CfgUpdateFlags.None;
CfgUpdateFlags flags =
CfgUpdateFlags.None;
if (config == null) return CfgUpdateFlags.Error;
if (config == null)
return CfgUpdateFlags.Error;
if (config.Name != nameTextBox.Text)
if (config.Name !=
nameTextBox.Text)
{
config.Name = nameTextBox.Text;
flags |= CfgUpdateFlags.AnyChange | CfgUpdateFlags.RestartRqrd;
config.Name =
nameTextBox.Text;
flags |=
CfgUpdateFlags.AnyChange |
CfgUpdateFlags.RestartRqrd;
}
if (config.ParentName != parentComboBox.Text)
if (config.ParentName !=
parentComboBox.Text)
{
config.ParentName = parentComboBox.Text;
flags |= CfgUpdateFlags.AnyChange | CfgUpdateFlags.RestartRqrd;
config.ParentName =
parentComboBox.Text;
flags |=
CfgUpdateFlags.AnyChange |
CfgUpdateFlags.RestartRqrd;
}
flags |= UpdateDifferent(
ref config.Enabled,
enabledCheckBox.Checked,
CfgUpdateFlags.AnyChange | CfgUpdateFlags.RestartRqrd);
flags |=
UpdateDifferent(
ref config.Enabled,
enabledCheckBox.Checked,
CfgUpdateFlags.AnyChange |
CfgUpdateFlags.RestartRqrd);
flags |= UpdateDifferent(
ref config.StorageName,
storageNameTextBox.Text,
CfgUpdateFlags.AnyChange | CfgUpdateFlags.RestartRqrd);
flags |=
UpdateDifferent(
ref config.StorageName,
storageNameTextBox.Text,
CfgUpdateFlags.AnyChange |
CfgUpdateFlags.RestartRqrd);
if (resultsConfigChanged)
{
flags |= CfgUpdateFlags.AnyChange | CfgUpdateFlags.RestartRqrd;
resultsConfigChanged = false;
config.UpdateSerializableModel();
flags |=
CfgUpdateFlags.AnyChange |
CfgUpdateFlags.RestartRqrd;
resultsConfigChanged =
false;
}
return flags;
}
private void configureResultsButton_Click(object sender, EventArgs e)
private void configureResultsButton_Click(
object sender,
EventArgs e)
{
if (config == null) return;
if (config == null)
return;
using (ResultsWriterResultsDlg dlg = new ResultsWriterResultsDlg())
string payloadTemplatePath =
ResolvePayloadTemplatePath();
using (ResultsWriterResultsDlg dlg =
new ResultsWriterResultsDlg())
{
dlg.SelectedItems = config.SelectedItems;
dlg.SelectedItems =
CloneSelectedItems(
config.SelectedItems);
if (dlg.ShowDialog(this) == DialogResult.OK)
dlg.PayloadTemplatePath =
payloadTemplatePath;
if (dlg.ShowDialog(this) ==
DialogResult.OK)
{
config.SelectedItems = new List<Results.WMeterRsltItemSpec>(dlg.SelectedItems);
config.UpdateSerializableModel();
resultsConfigChanged = true;
config.SelectedItems =
new List<Results.WMeterRsltItemSpec>(
dlg.SelectedItems);
selectedItemsLabel.Text = string.Format(
"{0} selected item(s)",
config.SelectedItems != null ? config.SelectedItems.Count : 0);
config.UpdateSerializableModel();
resultsConfigChanged =
true;
selectedItemsLabel.Text =
string.Format(
"{0} selected item(s)",
config.SelectedItems != null
? config.SelectedItems.Count
: 0);
}
}
}
private void previewRequestButton_Click(object sender, EventArgs e)
/// <summary>
/// Generates a five-test XML dry-run and never calls the database.
/// </summary>
private void previewRequestButton_Click(
object sender,
EventArgs e)
{
if (config == null) return;
Results.Entities.Batch batch = CreateSimulationBatch();
if (batch == null || batch.WaterMeters == null || batch.WaterMeters.Count == 0)
{
MessageBox.Show(
"No current batch results are available.",
"ResultsWriter",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
if (config == null)
return;
}
Results.Entities.Batch batch =
CreateSimulationBatch();
try
{
config.UpdateSerializableModel();
config.UpdateRuntimeModel();
ResultsWriter writer = new ResultsWriter(config);
ResultsWriter writer =
new ResultsWriter(
config);
writer.InitializeParent();
writer.WriteBatchResults(batch);
XmlPayloadBuildResult preview =
writer.GeneratePreviewPayload(
batch,
5);
MessageBox.Show(
"Current batch was written by ResultsWriter.",
"ResultsWriter",
string.Format(
"XML preview generated successfully.{0}{0}File:{0}{1}{0}{0}No production output operation was executed.",
Environment.NewLine,
preview.ArchiveFilePath),
"ResultsWriter XML preview",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
@ -209,35 +305,95 @@ namespace TBF.Rig.Output.DB.ResultsWriter
{
MessageBox.Show(
ex.Message,
"ResultsWriter write failed",
"ResultsWriter preview failed",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
private string ResolvePayloadTemplatePath()
{
WriterCfg writerCfg =
ResolveParentWriterCfg();
if (writerCfg == null ||
!writerCfg.UsesXmlPayload())
{
return null;
}
return writerCfg.PayloadTemplatePath;
}
private WriterCfg ResolveParentWriterCfg()
{
string parentName =
parentComboBox.Text;
if (string.IsNullOrWhiteSpace(
parentName))
{
return null;
}
IComponent parent =
TbfComponents.FindComponent(
parentName);
TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writer writer =
parent as
TBF.Rig.Output.DataStorage
.UniDataStorageWriter.Writer;
if (writer == null)
return null;
return writer.Cfg
as WriterCfg;
}
private IList<Results.WMeterRsltItemSpec> CloneSelectedItems(
IList<Results.WMeterRsltItemSpec> source)
{
List<Results.WMeterRsltItemSpec> result =
new List<Results.WMeterRsltItemSpec>();
if (source == null)
return result;
foreach (Results.WMeterRsltItemSpec item
in source)
{
if (item != null)
result.Add(
item.Clone());
}
return result;
}
private Results.Entities.Batch CreateSimulationBatch()
{
Results.Entities.Batch batch = new Results.Entities.Batch();
Results.Entities.Batch batch =
new Results.Entities.Batch();
batch.BatchNr = 999999;
batch.ProcedureName = "ResultsWriter simulation";
batch.ProcedureName = "ResultsWriter XML preview";
batch.StartTime = DateTime.Now;
batch.EndTime = DateTime.Now;
batch.TestBenchName = "Mexico";
batch.TestBenchName = "SIMULATION-BENCH";
Results.Entities.WaterMeter wm1 = new Results.Entities.WaterMeter();
wm1.Batch = batch;
wm1.WMPosition = 1;
wm1.SerialNr = "SN000001";
batch.WaterMeters.Add(wm1);
Results.Entities.WaterMeter wm =
new Results.Entities.WaterMeter();
Results.Entities.WaterMeter wm2 = new Results.Entities.WaterMeter();
wm2.Batch = batch;
wm2.WMPosition = 2;
wm2.SerialNr = "SN000002";
batch.WaterMeters.Add(wm2);
wm.Batch = batch;
wm.WMPosition = 1;
wm.SerialNr = "SIM000001";
batch.WaterMeters.Add(
wm);
return batch;
}
}
}
}

View File

@ -1,4 +1,5 @@
namespace TBF.Rig.Output.DB.ResultsWriter

namespace TBF.Rig.Output.DB.ResultsWriter
{
partial class ResultsWriterResultsDlg
{
@ -6,7 +7,9 @@
protected override void Dispose(bool disposing)
{
if (disposing && (components != null)) components.Dispose();
if (disposing && (components != null))
components.Dispose();
base.Dispose(disposing);
}
@ -34,10 +37,11 @@
this.okButton.Location = new System.Drawing.Point(714, 512);
this.okButton.Name = "okButton";
this.okButton.Size = new System.Drawing.Size(104, 30);
this.okButton.TabIndex = 1;
this.okButton.TabIndex = 2;
this.okButton.Text = "OK";
this.okButton.UseVisualStyleBackColor = true;
this.okButton.Click += new System.EventHandler(this.okButton_Click);
this.okButton.Click +=
new System.EventHandler(this.okButton_Click);
this.cancelButton.Anchor =
((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom |
@ -46,7 +50,7 @@
this.cancelButton.Location = new System.Drawing.Point(824, 512);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(104, 30);
this.cancelButton.TabIndex = 2;
this.cancelButton.TabIndex = 3;
this.cancelButton.Text = "Cancel";
this.cancelButton.UseVisualStyleBackColor = true;
@ -61,7 +65,8 @@
this.Name = "ResultsWriterResultsDlg";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "ResultsWriter configuration";
this.Load += new System.EventHandler(this.ResultsWriterResultsDlg_Load);
this.Load +=
new System.EventHandler(this.ResultsWriterResultsDlg_Load);
this.ResumeLayout(false);
}
@ -69,4 +74,4 @@
private System.Windows.Forms.Button okButton;
private System.Windows.Forms.Button cancelButton;
}
}
}

View File

@ -11,35 +11,181 @@ using TBF.Resources;
namespace TBF.Rig.Output.DB.ResultsWriter
{
/// <summary>
/// Configures ResultsWriter result items and optional XML destinations.
/// </summary>
public partial class ResultsWriterResultsDlg : Form
{
private string payloadTemplatePath;
public IList<WMeterRsltItemSpec> SelectedItems
{
set { resultsConfigCtrl.SelectedItems = value; }
get { return resultsConfigCtrl.SelectedItems; }
}
/// <summary>
/// Gets or sets the customer XML reference path.
/// </summary>
public string PayloadTemplatePath
{
get { return payloadTemplatePath; }
set { payloadTemplatePath = value; }
}
public ResultsWriterResultsDlg()
{
InitializeComponent();
this.Icon = Properties.Resources.TBF_icon;
resultsConfigCtrl.SupressTestIDColumn = true;
Icon =
Properties.Resources.TBF_icon;
resultsConfigCtrl.SupressTestIDColumn =
true;
}
private void ResultsWriterResultsDlg_Load(object sender, EventArgs e)
private void ResultsWriterResultsDlg_Load(
object sender,
EventArgs e)
{
Text = "ResultsWriter configuration";
okButton.Text = Strings.OkBtnText;
cancelButton.Text = Strings.CancelBtnText;
Text =
"ResultsWriter configuration";
resultsConfigCtrl.Unlocked = true;
okButton.Text =
Strings.OkBtnText;
cancelButton.Text =
Strings.CancelBtnText;
resultsConfigCtrl.Unlocked =
true;
ConfigureCaptionEditor();
}
private void okButton_Click(object sender, EventArgs e)
private void ConfigureCaptionEditor()
{
DialogResult = DialogResult.OK;
bool xmlMode =
!string.IsNullOrWhiteSpace(
PayloadTemplatePath);
if (!xmlMode)
{
resultsConfigCtrl.CaptionPicker =
null;
resultsConfigCtrl.CaptionColumnText =
"Caption";
return;
}
resultsConfigCtrl.CaptionPicker =
PickXmlDestination;
resultsConfigCtrl.CaptionColumnText =
"Destination [...]";
}
private string PickXmlDestination(
WMeterRsltItemSpec editedItem)
{
if (editedItem == null)
return null;
using (XmlDestinationPickerDlg dlg =
new XmlDestinationPickerDlg())
{
dlg.TemplatePath =
PayloadTemplatePath;
dlg.CurrentSourceName =
editedItem.Name;
dlg.CurrentDestinationPath =
editedItem.Caption;
dlg.ConfiguredMappings =
BuildConfiguredMappings(
editedItem);
if (dlg.ShowDialog(this) ==
DialogResult.OK)
{
return dlg.SelectedDestinationPath;
}
}
return null;
}
private IDictionary<string, IList<string>> BuildConfiguredMappings(
WMeterRsltItemSpec editedItem)
{
Dictionary<string, IList<string>> mappings =
new Dictionary<string, IList<string>>(
StringComparer.Ordinal);
if (resultsConfigCtrl.SelectedItems != null)
{
foreach (WMeterRsltItemSpec item
in resultsConfigCtrl.SelectedItems)
{
if (item == null ||
object.ReferenceEquals(
item,
editedItem) ||
string.IsNullOrWhiteSpace(
item.Caption))
{
continue;
}
AddMapping(
mappings,
item.Caption,
item.Name);
}
}
return mappings;
}
private void AddMapping(
IDictionary<string, IList<string>> mappings,
string path,
string sourceName)
{
if (string.IsNullOrWhiteSpace(path))
return;
IList<string> names;
if (!mappings.TryGetValue(
path,
out names))
{
names =
new List<string>();
mappings.Add(
path,
names);
}
if (!names.Contains(sourceName))
names.Add(sourceName);
}
private void okButton_Click(
object sender,
EventArgs e)
{
DialogResult =
DialogResult.OK;
Close();
}
}
}
}

View File

@ -0,0 +1,366 @@
///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
namespace TBF.Rig.Output.DB.ResultsWriter
{
partial class XmlDestinationPickerDlg
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(
bool disposing)
{
if (disposing &&
components != null)
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.templateLabel =
new System.Windows.Forms.Label();
this.templatePathTextBox =
new System.Windows.Forms.TextBox();
this.configuringResultLabel =
new System.Windows.Forms.Label();
this.configuringResultTextBox =
new System.Windows.Forms.TextBox();
this.xmlTreeView =
new System.Windows.Forms.TreeView();
this.selectedDestinationLabel =
new System.Windows.Forms.Label();
this.selectedPathTextBox =
new System.Windows.Forms.TextBox();
this.mappedResultsLabel =
new System.Windows.Forms.Label();
this.mappedResultsTextBox =
new System.Windows.Forms.TextBox();
this.mappingStatusLabel =
new System.Windows.Forms.Label();
this.legendCurrentLabel =
new System.Windows.Forms.Label();
this.legendMappedLabel =
new System.Windows.Forms.Label();
this.legendUsedLabel =
new System.Windows.Forms.Label();
this.chooseButton =
new System.Windows.Forms.Button();
this.cancelButton =
new System.Windows.Forms.Button();
this.SuspendLayout();
//
// templateLabel
//
this.templateLabel.AutoSize = true;
this.templateLabel.Location = new System.Drawing.Point(12, 15);
this.templateLabel.Name = "templateLabel";
this.templateLabel.Size = new System.Drawing.Size(90, 13);
this.templateLabel.TabIndex = 0;
this.templateLabel.Text = "XML reference:";
//
// templatePathTextBox
//
this.templatePathTextBox.Anchor =
((System.Windows.Forms.AnchorStyles)
(((System.Windows.Forms.AnchorStyles.Top |
System.Windows.Forms.AnchorStyles.Left) |
System.Windows.Forms.AnchorStyles.Right)));
this.templatePathTextBox.Location =
new System.Drawing.Point(108, 12);
this.templatePathTextBox.Name = "templatePathTextBox";
this.templatePathTextBox.ReadOnly = true;
this.templatePathTextBox.Size = new System.Drawing.Size(860, 20);
this.templatePathTextBox.TabIndex = 1;
//
// configuringResultLabel
//
this.configuringResultLabel.AutoSize = true;
this.configuringResultLabel.Location = new System.Drawing.Point(12, 43);
this.configuringResultLabel.Name = "configuringResultLabel";
this.configuringResultLabel.Size = new System.Drawing.Size(118, 13);
this.configuringResultLabel.TabIndex = 2;
this.configuringResultLabel.Text = "Configuring TBF result:";
//
// configuringResultTextBox
//
this.configuringResultTextBox.Anchor =
((System.Windows.Forms.AnchorStyles)
(((System.Windows.Forms.AnchorStyles.Top |
System.Windows.Forms.AnchorStyles.Left) |
System.Windows.Forms.AnchorStyles.Right)));
this.configuringResultTextBox.Location =
new System.Drawing.Point(136, 40);
this.configuringResultTextBox.Name = "configuringResultTextBox";
this.configuringResultTextBox.ReadOnly = true;
this.configuringResultTextBox.Size = new System.Drawing.Size(832, 20);
this.configuringResultTextBox.TabIndex = 3;
//
// xmlTreeView
//
this.xmlTreeView.Anchor =
((System.Windows.Forms.AnchorStyles)
((((System.Windows.Forms.AnchorStyles.Top |
System.Windows.Forms.AnchorStyles.Bottom) |
System.Windows.Forms.AnchorStyles.Left) |
System.Windows.Forms.AnchorStyles.Right)));
this.xmlTreeView.FullRowSelect = true;
this.xmlTreeView.HideSelection = false;
this.xmlTreeView.Location = new System.Drawing.Point(12, 72);
this.xmlTreeView.Name = "xmlTreeView";
this.xmlTreeView.ShowNodeToolTips = true;
this.xmlTreeView.Size = new System.Drawing.Size(956, 392);
this.xmlTreeView.TabIndex = 4;
this.xmlTreeView.AfterSelect +=
new System.Windows.Forms.TreeViewEventHandler(
this.xmlTreeView_AfterSelect);
this.xmlTreeView.NodeMouseDoubleClick +=
new System.Windows.Forms.TreeNodeMouseClickEventHandler(
this.xmlTreeView_NodeMouseDoubleClick);
//
// selectedDestinationLabel
//
this.selectedDestinationLabel.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left)));
this.selectedDestinationLabel.AutoSize = true;
this.selectedDestinationLabel.Location = new System.Drawing.Point(12, 477);
this.selectedDestinationLabel.Name = "selectedDestinationLabel";
this.selectedDestinationLabel.Size = new System.Drawing.Size(109, 13);
this.selectedDestinationLabel.TabIndex = 5;
this.selectedDestinationLabel.Text = "Selected destination:";
//
// selectedPathTextBox
//
this.selectedPathTextBox.Anchor =
((System.Windows.Forms.AnchorStyles)
(((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left) |
System.Windows.Forms.AnchorStyles.Right)));
this.selectedPathTextBox.Location = new System.Drawing.Point(15, 493);
this.selectedPathTextBox.Name = "selectedPathTextBox";
this.selectedPathTextBox.ReadOnly = true;
this.selectedPathTextBox.Size = new System.Drawing.Size(953, 20);
this.selectedPathTextBox.TabIndex = 6;
//
// mappedResultsLabel
//
this.mappedResultsLabel.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left)));
this.mappedResultsLabel.AutoSize = true;
this.mappedResultsLabel.Location = new System.Drawing.Point(12, 526);
this.mappedResultsLabel.Name = "mappedResultsLabel";
this.mappedResultsLabel.Size = new System.Drawing.Size(119, 13);
this.mappedResultsLabel.TabIndex = 7;
this.mappedResultsLabel.Text = "Mapped TBF result(s):";
//
// mappedResultsTextBox
//
this.mappedResultsTextBox.Anchor =
((System.Windows.Forms.AnchorStyles)
(((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left) |
System.Windows.Forms.AnchorStyles.Right)));
this.mappedResultsTextBox.Location = new System.Drawing.Point(137, 523);
this.mappedResultsTextBox.Name = "mappedResultsTextBox";
this.mappedResultsTextBox.ReadOnly = true;
this.mappedResultsTextBox.Size = new System.Drawing.Size(831, 20);
this.mappedResultsTextBox.TabIndex = 8;
//
// mappingStatusLabel
//
this.mappingStatusLabel.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left)));
this.mappingStatusLabel.AutoSize = true;
this.mappingStatusLabel.Location = new System.Drawing.Point(12, 556);
this.mappingStatusLabel.Name = "mappingStatusLabel";
this.mappingStatusLabel.Size = new System.Drawing.Size(0, 13);
this.mappingStatusLabel.TabIndex = 9;
//
// legendCurrentLabel
//
this.legendCurrentLabel.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left)));
this.legendCurrentLabel.AutoSize = true;
this.legendCurrentLabel.BackColor = System.Drawing.Color.LightBlue;
this.legendCurrentLabel.Location = new System.Drawing.Point(12, 582);
this.legendCurrentLabel.Name = "legendCurrentLabel";
this.legendCurrentLabel.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2);
this.legendCurrentLabel.Size = new System.Drawing.Size(100, 17);
this.legendCurrentLabel.TabIndex = 10;
this.legendCurrentLabel.Text = "Current mapping";
//
// legendMappedLabel
//
this.legendMappedLabel.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left)));
this.legendMappedLabel.AutoSize = true;
this.legendMappedLabel.BackColor = System.Drawing.Color.PaleGreen;
this.legendMappedLabel.Location = new System.Drawing.Point(122, 582);
this.legendMappedLabel.Name = "legendMappedLabel";
this.legendMappedLabel.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2);
this.legendMappedLabel.Size = new System.Drawing.Size(165, 17);
this.legendMappedLabel.TabIndex = 11;
this.legendMappedLabel.Text = "Mapped repeating destination";
//
// legendUsedLabel
//
this.legendUsedLabel.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left)));
this.legendUsedLabel.AutoSize = true;
this.legendUsedLabel.BackColor = System.Drawing.Color.LightGoldenrodYellow;
this.legendUsedLabel.Location = new System.Drawing.Point(297, 582);
this.legendUsedLabel.Name = "legendUsedLabel";
this.legendUsedLabel.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2);
this.legendUsedLabel.Size = new System.Drawing.Size(151, 17);
this.legendUsedLabel.TabIndex = 12;
this.legendUsedLabel.Text = "Used one-time destination";
//
// chooseButton
//
this.chooseButton.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Right)));
this.chooseButton.Location = new System.Drawing.Point(754, 610);
this.chooseButton.Name = "chooseButton";
this.chooseButton.Size = new System.Drawing.Size(104, 30);
this.chooseButton.TabIndex = 13;
this.chooseButton.Text = "Choose";
this.chooseButton.UseVisualStyleBackColor = true;
this.chooseButton.Click +=
new System.EventHandler(
this.chooseButton_Click);
//
// cancelButton
//
this.cancelButton.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Right)));
this.cancelButton.DialogResult =
System.Windows.Forms.DialogResult.Cancel;
this.cancelButton.Location = new System.Drawing.Point(864, 610);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(104, 30);
this.cancelButton.TabIndex = 14;
this.cancelButton.Text = "Cancel";
this.cancelButton.UseVisualStyleBackColor = true;
//
// XmlDestinationPickerDlg
//
this.AcceptButton = this.chooseButton;
this.CancelButton = this.cancelButton;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(980, 652);
this.Controls.Add(this.cancelButton);
this.Controls.Add(this.chooseButton);
this.Controls.Add(this.legendUsedLabel);
this.Controls.Add(this.legendMappedLabel);
this.Controls.Add(this.legendCurrentLabel);
this.Controls.Add(this.mappingStatusLabel);
this.Controls.Add(this.mappedResultsTextBox);
this.Controls.Add(this.mappedResultsLabel);
this.Controls.Add(this.selectedPathTextBox);
this.Controls.Add(this.selectedDestinationLabel);
this.Controls.Add(this.xmlTreeView);
this.Controls.Add(this.configuringResultTextBox);
this.Controls.Add(this.configuringResultLabel);
this.Controls.Add(this.templatePathTextBox);
this.Controls.Add(this.templateLabel);
this.MinimumSize = new System.Drawing.Size(760, 560);
this.Name = "XmlDestinationPickerDlg";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Select XML destination";
this.Load +=
new System.EventHandler(
this.XmlDestinationPickerDlg_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
private System.Windows.Forms.Label templateLabel;
private System.Windows.Forms.TextBox templatePathTextBox;
private System.Windows.Forms.Label configuringResultLabel;
private System.Windows.Forms.TextBox configuringResultTextBox;
private System.Windows.Forms.TreeView xmlTreeView;
private System.Windows.Forms.Label selectedDestinationLabel;
private System.Windows.Forms.TextBox selectedPathTextBox;
private System.Windows.Forms.Label mappedResultsLabel;
private System.Windows.Forms.TextBox mappedResultsTextBox;
private System.Windows.Forms.Label mappingStatusLabel;
private System.Windows.Forms.Label legendCurrentLabel;
private System.Windows.Forms.Label legendMappedLabel;
private System.Windows.Forms.Label legendUsedLabel;
private System.Windows.Forms.Button chooseButton;
private System.Windows.Forms.Button cancelButton;
}
}

View File

@ -0,0 +1,587 @@
///
/// 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;
}
}
}
}

View File

@ -9,30 +9,140 @@ using TBF.Rig.Generic;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
{
/// <summary>
/// Factory component 'UniDataStorageWriter' implements more storing modules
/// Modules:
/// Provides factory services for the
/// <see cref="UniDataStorageWriter"/> component.
/// </summary>
/// <remarks>
/// The factory is responsible for creating runtime
/// <see cref="Writer"/> instances and their corresponding
/// <see cref="WriterCfg"/> configuration objects.
///
/// The concrete storage technology is not selected by the factory.
/// Technology-specific writer selection is performed later by
/// <see cref="Writer"/> according to the active configuration.
///
/// Consequently, database write modes such as INSERT, UPDATE and
/// stored procedure execution do not require separate component
/// factories.
/// </remarks>
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 Writer(new WriterCfg("UniDataStorageWriter", this)); }
public IComponent GetComponent(IComponentCfg cfg, IList<IComponent> components)
{
/// <summary>
/// Gets the component class name used by the TBF component framework.
/// </summary>
/// <remarks>
/// The namespace prefix is removed for backward compatibility with
/// existing component configurations.
/// </remarks>
public string ClassName
{
WriterCfg WriterCfg = cfg as WriterCfg;
if (WriterCfg == null)
throw new ArgumentException("Invalid config for UniDataStorageWriter");
return new Writer(WriterCfg);
get { return GetType().Namespace.Substring(8); }
}
public IComponentCfg DefaultConfig() { return new WriterCfg("UniDataStorageWriter", this); }
public IComponentCfg CmpntCfgFromCmpntEntity(Config.Entities.Component component)
/// <summary>
/// Returns the component class name.
/// </summary>
/// <returns>
/// The value of <see cref="ClassName"/>.
/// </returns>
public override string ToString()
{
return ComponentCfgBase.CreateFromDbEntity(WriterCfg.Serializer, component, this);
return ClassName;
}
}
}
/// <summary>
/// Creates a default runtime instance of the
/// <see cref="Writer"/> component.
/// </summary>
/// <returns>
/// A new <see cref="Writer"/> configured with a default
/// <see cref="WriterCfg"/> instance.
/// </returns>
/// <remarks>
/// The dummy component is primarily used by the component framework
/// for discovery and configuration purposes.
/// </remarks>
public IComponent DummyComponent()
{
return new Writer(
new WriterCfg("UniDataStorageWriter", this));
}
/// <summary>
/// Creates a runtime <see cref="Writer"/> from an existing
/// component configuration.
/// </summary>
/// <param name="cfg">
/// Component configuration expected to be a
/// <see cref="WriterCfg"/> instance.
/// </param>
/// <param name="components">
/// Collection of already created components supplied by the
/// component framework.
/// </param>
/// <returns>
/// A new <see cref="Writer"/> instance using the supplied configuration.
/// </returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="cfg"/> is not a valid
/// <see cref="WriterCfg"/> instance.
/// </exception>
/// <remarks>
/// The <paramref name="components"/> collection is currently not
/// required by <see cref="Writer"/>, but remains part of the factory
/// contract defined by <see cref="IComponentFactory"/>.
/// </remarks>
public IComponent GetComponent(
IComponentCfg cfg,
IList<IComponent> components)
{
WriterCfg writerCfg = cfg as WriterCfg;
if (writerCfg == null)
{
throw new ArgumentException(
"Invalid config for UniDataStorageWriter");
}
return new Writer(writerCfg);
}
/// <summary>
/// Creates the default configuration for the
/// <see cref="UniDataStorageWriter"/> component.
/// </summary>
/// <returns>
/// A new default <see cref="WriterCfg"/> instance.
/// </returns>
public IComponentCfg DefaultConfig()
{
return new WriterCfg(
"UniDataStorageWriter",
this);
}
/// <summary>
/// Creates a <see cref="WriterCfg"/> instance from a persisted
/// component database entity.
/// </summary>
/// <param name="component">
/// Persisted component entity containing serialized configuration data.
/// </param>
/// <returns>
/// Deserialized component configuration compatible with
/// <see cref="Writer"/>.
/// </returns>
/// <remarks>
/// Configuration deserialization is delegated to
/// <see cref="ComponentCfgBase.CreateFromDbEntity"/>.
/// </remarks>
public IComponentCfg CmpntCfgFromCmpntEntity(
Config.Entities.Component component)
{
return ComponentCfgBase.CreateFromDbEntity(
WriterCfg.Serializer,
component,
this);
}
}
}

View File

@ -0,0 +1,156 @@
///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.Collections.Generic;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
{
/// <summary>
/// Maps one named runtime source value to one XML destination.
/// </summary>
public class PayloadMapping
{
/// <summary>
/// Gets or sets the logical source key.
/// </summary>
public string SourceKey { get; set; }
/// <summary>
/// Gets or sets the XML destination path.
/// </summary>
/// <remarks>
/// Repeating destinations use the <c>repeat:</c> prefix.
/// </remarks>
public string DestinationPath { get; set; }
}
/// <summary>
/// Contains named runtime values used during XML generation.
/// </summary>
public class PayloadValueSet
{
private readonly Dictionary<string, string> values;
/// <summary>
/// Initializes an empty value set.
/// </summary>
public PayloadValueSet()
{
values = new Dictionary<string, string>(
StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Sets a runtime value.
/// </summary>
public void SetValue(string key, object value)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException(
"Payload source key must not be empty.",
nameof(key));
values[key] = value != null
? Convert.ToString(value)
: string.Empty;
}
/// <summary>
/// Tries to get a runtime value.
/// </summary>
public bool TryGetValue(string key, out string value)
{
if (string.IsNullOrWhiteSpace(key))
{
value = null;
return false;
}
return values.TryGetValue(key, out value);
}
}
/// <summary>
/// Describes one generated instance of a repeating XML prototype.
/// </summary>
/// <remarks>
/// Each record owns its own mappings. Therefore multiple TBF result items
/// may use the same prototype destination while still creating separate
/// GROUP/TEST instances.
/// </remarks>
public class PayloadRepeatRecord
{
/// <summary>
/// Initializes an empty repeat record.
/// </summary>
public PayloadRepeatRecord()
{
Mappings = new List<PayloadMapping>();
Values = new PayloadValueSet();
}
/// <summary>
/// Gets mappings applied to this prototype instance.
/// </summary>
public IList<PayloadMapping> Mappings { get; private set; }
/// <summary>
/// Gets runtime values used by this prototype instance.
/// </summary>
public PayloadValueSet Values { get; private set; }
}
/// <summary>
/// Describes one XML payload generation operation.
/// </summary>
public class PayloadGenerationRequest
{
/// <summary>
/// Initializes an empty generation request.
/// </summary>
public PayloadGenerationRequest()
{
SingleMappings = new List<PayloadMapping>();
SingleValues = new PayloadValueSet();
RepeatRecords = new List<PayloadRepeatRecord>();
}
/// <summary>
/// Gets mappings applied once to the clean base document.
/// </summary>
public IList<PayloadMapping> SingleMappings { get; private set; }
/// <summary>
/// Gets values used by one-time mappings.
/// </summary>
public PayloadValueSet SingleValues { get; private set; }
/// <summary>
/// Gets generated repeating records.
/// </summary>
public IList<PayloadRepeatRecord> RepeatRecords { get; private set; }
/// <summary>
/// Gets or sets the logical repeat prototype path.
/// </summary>
public string RepeatPrototypePath { get; set; }
}
/// <summary>
/// Contains a generated XML payload.
/// </summary>
public class PayloadGenerationResult
{
/// <summary>
/// Gets or sets the generated XML document.
/// </summary>
public System.Xml.Linq.XDocument Document { get; internal set; }
/// <summary>
/// Gets or sets the generated XML text.
/// </summary>
public string Payload { get; internal set; }
}
}

View File

@ -0,0 +1,199 @@
///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
{
/// <summary>
/// Converts a customer result XML example into a clean base structure and repeat prototypes.
/// </summary>
public class PayloadReferenceAnalyzer
{
private const string RepeatPrefix = "repeat:";
/// <summary>
/// Analyzes a customer XML reference/example file.
/// </summary>
public PayloadTemplateDefinition Analyze(string referencePath)
{
if (string.IsNullOrWhiteSpace(referencePath))
throw new ArgumentException("XML reference path must not be empty.", nameof(referencePath));
if (!File.Exists(referencePath))
throw new FileNotFoundException("XML reference file was not found.", referencePath);
XDocument source = XDocument.Load(referencePath, LoadOptions.PreserveWhitespace);
if (source.Root == null)
throw new InvalidOperationException("XML reference does not contain a root element.");
List<PayloadRepeatPrototypeDefinition> prototypes =
new List<PayloadRepeatPrototypeDefinition>();
string rootPath = "/" + source.Root.Name.LocalName;
XElement cleanRoot = CreateCleanBaseElement(
source.Root,
rootPath,
prototypes);
XDocument cleanDocument = new XDocument(
source.Declaration != null ? new XDeclaration(source.Declaration) : null,
cleanRoot);
return new PayloadTemplateDefinition(
referencePath,
cleanDocument,
prototypes);
}
/// <summary>
/// Creates one clean base element and extracts repeated child sets as prototypes.
/// </summary>
private XElement CreateCleanBaseElement(
XElement source,
string currentPath,
IList<PayloadRepeatPrototypeDefinition> prototypes)
{
XElement clean = CreateElementShell(source);
List<XElement> children = source.Elements().ToList();
HashSet<XName> processedNames = new HashSet<XName>();
foreach (XElement child in children)
{
if (processedNames.Contains(child.Name))
continue;
processedNames.Add(child.Name);
List<XElement> sameNameChildren = children
.Where(candidate => candidate.Name == child.Name)
.ToList();
string childPath = currentPath + "/" + child.Name.LocalName;
if (sameNameChildren.Count > 1)
{
XElement representative = SelectRepresentative(sameNameChildren);
XElement cleanPrototype = CreateCleanPrototypeElement(representative);
prototypes.Add(
new PayloadRepeatPrototypeDefinition(
RepeatPrefix + childPath,
currentPath,
cleanPrototype,
sameNameChildren.Count));
// Example instances are deliberately not copied to the base.
continue;
}
clean.Add(
CreateCleanBaseElement(
child,
childPath,
prototypes));
}
return clean;
}
/// <summary>
/// Creates a clean prototype element. Nested repeated children are represented once.
/// </summary>
private XElement CreateCleanPrototypeElement(XElement source)
{
XElement clean = CreateElementShell(source);
List<XElement> children = source.Elements().ToList();
HashSet<XName> processedNames = new HashSet<XName>();
foreach (XElement child in children)
{
if (processedNames.Contains(child.Name))
continue;
processedNames.Add(child.Name);
List<XElement> sameNameChildren = children
.Where(candidate => candidate.Name == child.Name)
.ToList();
XElement representative = sameNameChildren.Count > 1
? SelectRepresentative(sameNameChildren)
: child;
clean.Add(CreateCleanPrototypeElement(representative));
}
return clean;
}
/// <summary>
/// Creates an XML element with original names but without example runtime values.
/// </summary>
private XElement CreateElementShell(XElement source)
{
XElement clean = new XElement(source.Name);
foreach (XAttribute attribute in source.Attributes())
{
clean.Add(
attribute.IsNamespaceDeclaration
? new XAttribute(attribute.Name, attribute.Value)
: new XAttribute(attribute.Name, string.Empty));
}
return clean;
}
/// <summary>
/// Selects the structurally richest example from a repeated sibling set.
/// </summary>
private XElement SelectRepresentative(IList<XElement> examples)
{
if (examples == null || examples.Count == 0)
throw new ArgumentException("Repeated XML example set is empty.", nameof(examples));
XElement best = examples[0];
int bestScore = GetStructuralRichnessScore(best);
for (int i = 1; i < examples.Count; i++)
{
int score = GetStructuralRichnessScore(examples[i]);
if (score > bestScore)
{
best = examples[i];
bestScore = score;
}
}
return best;
}
/// <summary>
/// Computes a structure-only richness score.
/// </summary>
private int GetStructuralRichnessScore(XElement element)
{
int ownAttributes = element.Attributes()
.Count(attribute => !attribute.IsNamespaceDeclaration);
int descendants = element.Descendants().Count();
int descendantAttributes = element.Descendants()
.SelectMany(descendant => descendant.Attributes())
.Count(attribute => !attribute.IsNamespaceDeclaration);
return ownAttributes + descendants + descendantAttributes;
}
}
}

View File

@ -0,0 +1,146 @@
///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
{
/// <summary>
/// Describes one repeating XML element extracted from a customer reference XML file.
/// </summary>
public class PayloadRepeatPrototypeDefinition
{
private readonly XElement prototypeElement;
/// <summary>
/// Initializes a new repeat prototype definition.
/// </summary>
public PayloadRepeatPrototypeDefinition(
string prototypePath,
string parentPath,
XElement prototypeElement,
int exampleInstanceCount)
{
if (string.IsNullOrWhiteSpace(prototypePath))
throw new ArgumentException("Prototype path must not be empty.", nameof(prototypePath));
if (string.IsNullOrWhiteSpace(parentPath))
throw new ArgumentException("Prototype parent path must not be empty.", nameof(parentPath));
if (prototypeElement == null)
throw new ArgumentNullException(nameof(prototypeElement));
PrototypePath = prototypePath;
ParentPath = parentPath;
this.prototypeElement = new XElement(prototypeElement);
ExampleInstanceCount = exampleInstanceCount;
}
/// <summary>
/// Gets the logical repeat path, for example
/// <c>repeat:/BATCH/PANEL/DUT/GROUP</c>.
/// </summary>
public string PrototypePath { get; private set; }
/// <summary>
/// Gets the XPath of the element into which generated prototype instances are inserted.
/// </summary>
public string ParentPath { get; private set; }
/// <summary>
/// Gets the number of repeated instances found in the customer example.
/// </summary>
public int ExampleInstanceCount { get; private set; }
/// <summary>
/// Creates an independent clean clone of the repeat prototype.
/// </summary>
public XElement CreatePrototypeElement()
{
return new XElement(prototypeElement);
}
/// <summary>
/// Returns true when a logical repeating destination belongs to this prototype.
/// </summary>
public bool ContainsDestination(string destinationPath)
{
if (string.IsNullOrWhiteSpace(destinationPath))
return false;
return string.Equals(destinationPath, PrototypePath, StringComparison.Ordinal) ||
destinationPath.StartsWith(PrototypePath + "/", StringComparison.Ordinal);
}
}
/// <summary>
/// Represents a clean XML payload structure derived from a customer reference/example XML file.
/// </summary>
/// <remarks>
/// <para>
/// The base document contains only structure. Example runtime values are removed.
/// Repeated sibling elements are removed from the base document and represented by
/// <see cref="RepeatPrototypes"/>.
/// </para>
/// </remarks>
public class PayloadTemplateDefinition
{
private readonly XDocument baseDocument;
private readonly List<PayloadRepeatPrototypeDefinition> repeatPrototypes;
/// <summary>
/// Initializes a new payload template definition.
/// </summary>
public PayloadTemplateDefinition(
string referencePath,
XDocument baseDocument,
IEnumerable<PayloadRepeatPrototypeDefinition> repeatPrototypes)
{
if (string.IsNullOrWhiteSpace(referencePath))
throw new ArgumentException("Reference path must not be empty.", nameof(referencePath));
if (baseDocument == null)
throw new ArgumentNullException(nameof(baseDocument));
ReferencePath = referencePath;
this.baseDocument = new XDocument(baseDocument);
this.repeatPrototypes = repeatPrototypes != null
? new List<PayloadRepeatPrototypeDefinition>(repeatPrototypes)
: new List<PayloadRepeatPrototypeDefinition>();
}
/// <summary>
/// Gets the customer reference/example XML path.
/// </summary>
public string ReferencePath { get; private set; }
/// <summary>
/// Gets the detected repeating XML prototypes.
/// </summary>
public IList<PayloadRepeatPrototypeDefinition> RepeatPrototypes
{
get { return repeatPrototypes.AsReadOnly(); }
}
/// <summary>
/// Creates an independent clone of the clean base XML document.
/// </summary>
public XDocument CreateBaseDocument()
{
return new XDocument(baseDocument);
}
/// <summary>
/// Finds the repeating prototype that owns a logical repeating destination.
/// </summary>
public PayloadRepeatPrototypeDefinition FindRepeatPrototype(string destinationPath)
{
return repeatPrototypes.FirstOrDefault(
prototype => prototype.ContainsDestination(destinationPath));
}
}
}

View File

@ -0,0 +1,386 @@
///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
{
/// <summary>
/// Analyzes a customer XML example and creates a structural payload model.
/// </summary>
/// <remarks>
/// <para>
/// The input XML is treated as a reference example, not as a clean template.
/// Runtime values contained in the example are deliberately ignored by the
/// viewer model.
/// </para>
///
/// <para>
/// Repeated sibling elements are collapsed into one structural prototype.
/// For example, multiple GROUP elements below one DUT are displayed as a
/// single GROUP repeating prototype. The prototype keeps the XML structure
/// and available attributes, while example values and repeated instances
/// are hidden.
/// </para>
///
/// <para>
/// This behavior allows configuration to describe the base structure once
/// even when the result XML contains many tests with the same structure.
/// </para>
/// </remarks>
public class PayloadTemplateInspector
{
private const string RepeatPrefix =
"repeat:";
/// <summary>
/// Loads and inspects an XML payload example.
/// </summary>
public PayloadTemplateInspectionResult Inspect(
string templatePath)
{
if (string.IsNullOrWhiteSpace(
templatePath))
{
throw new ArgumentException(
"XML example path must not be empty.",
nameof(templatePath));
}
if (!File.Exists(
templatePath))
{
throw new FileNotFoundException(
"XML payload example was not found.",
templatePath);
}
XDocument document =
XDocument.Load(
templatePath,
LoadOptions.PreserveWhitespace);
if (document.Root == null)
{
throw new InvalidOperationException(
"XML payload example does not contain a root element.");
}
string rootPath =
"/" +
document.Root.Name.LocalName;
PayloadTemplateNode rootNode =
BuildElementNode(
document.Root,
rootPath,
false,
false,
1);
return new PayloadTemplateInspectionResult(
templatePath,
rootNode);
}
/// <summary>
/// Builds one element node and its structural descendants.
/// </summary>
private PayloadTemplateNode BuildElementNode(
XElement element,
string structuralPath,
bool insideRepeatingPrototype,
bool isRepeatingPrototypeRoot,
int representedInstanceCount)
{
PayloadTemplateNode node =
new PayloadTemplateNode
{
NodeType =
PayloadTemplateNodeType.Element,
Name =
element.Name.LocalName,
DisplayText =
CreateElementDisplayText(
element.Name.LocalName,
isRepeatingPrototypeRoot,
representedInstanceCount),
DestinationPath =
CreateDestinationPath(
structuralPath,
insideRepeatingPrototype ||
isRepeatingPrototypeRoot),
IsSelectable =
false,
DestinationKind =
insideRepeatingPrototype ||
isRepeatingPrototypeRoot
? PayloadDestinationKind.RepeatingPrototype
: PayloadDestinationKind.SingleValue,
IsRepeatingPrototypeRoot =
isRepeatingPrototypeRoot,
ExampleInstanceCount =
representedInstanceCount
};
bool effectiveRepeatingState =
insideRepeatingPrototype ||
isRepeatingPrototypeRoot;
//
// Attributes are structural destinations. Their sample values are
// intentionally not copied to the presentation model.
//
foreach (XAttribute attribute
in element.Attributes()
.Where(
attribute =>
!attribute.IsNamespaceDeclaration))
{
string attributeStructuralPath =
structuralPath +
"/@" +
attribute.Name.LocalName;
node.Children.Add(
new PayloadTemplateNode
{
NodeType =
PayloadTemplateNodeType.Attribute,
Name =
attribute.Name.LocalName,
DisplayText =
"@" +
attribute.Name.LocalName,
DestinationPath =
CreateDestinationPath(
attributeStructuralPath,
effectiveRepeatingState),
IsSelectable =
true,
DestinationKind =
effectiveRepeatingState
? PayloadDestinationKind.RepeatingPrototype
: PayloadDestinationKind.SingleValue,
IsRepeatingPrototypeRoot =
false,
ExampleInstanceCount =
representedInstanceCount
});
}
//
// Leaf element text can also be a writable destination. The sample
// text itself is not exposed.
//
if (!element.Elements().Any() &&
!string.IsNullOrWhiteSpace(
element.Value))
{
node.Children.Add(
new PayloadTemplateNode
{
NodeType =
PayloadTemplateNodeType.Text,
Name =
"#text",
DisplayText =
"#text",
DestinationPath =
CreateDestinationPath(
structuralPath +
"/text()",
effectiveRepeatingState),
IsSelectable =
true,
DestinationKind =
effectiveRepeatingState
? PayloadDestinationKind.RepeatingPrototype
: PayloadDestinationKind.SingleValue,
IsRepeatingPrototypeRoot =
false,
ExampleInstanceCount =
representedInstanceCount
});
}
//
// Group direct children by element name. When one name occurs more
// than once, all examples are represented by one prototype.
//
IEnumerable<IGrouping<XName, XElement>> childGroups =
element
.Elements()
.GroupBy(
child =>
child.Name);
foreach (IGrouping<XName, XElement> childGroup
in childGroups)
{
List<XElement> examples =
childGroup
.ToList();
bool isRepeated =
examples.Count > 1;
XElement representative =
SelectRepresentative(
examples);
string childStructuralPath =
structuralPath +
"/" +
representative.Name.LocalName;
node.Children.Add(
BuildElementNode(
representative,
childStructuralPath,
effectiveRepeatingState,
isRepeated,
examples.Count));
}
return node;
}
/// <summary>
/// Selects the richest example from a repeated sibling set.
/// </summary>
/// <remarks>
/// The representative is chosen by structural richness rather than by
/// runtime values. This gives the prototype the best chance of exposing
/// all attributes and child elements present in the example set.
/// </remarks>
private XElement SelectRepresentative(
IList<XElement> examples)
{
if (examples == null ||
examples.Count == 0)
{
throw new ArgumentException(
"Repeated XML example set is empty.",
nameof(examples));
}
XElement representative =
examples[0];
int bestScore =
GetStructuralRichnessScore(
representative);
for (int i = 1;
i < examples.Count;
i++)
{
int score =
GetStructuralRichnessScore(
examples[i]);
if (score > bestScore)
{
representative =
examples[i];
bestScore =
score;
}
}
return representative;
}
/// <summary>
/// Calculates a simple structure-only richness score.
/// </summary>
private int GetStructuralRichnessScore(
XElement element)
{
int attributeCount =
element
.Attributes()
.Count(
attribute =>
!attribute.IsNamespaceDeclaration);
int childCount =
element
.Descendants()
.Count();
int descendantAttributeCount =
element
.Descendants()
.SelectMany(
descendant =>
descendant.Attributes())
.Count(
attribute =>
!attribute.IsNamespaceDeclaration);
return attributeCount +
childCount +
descendantAttributeCount;
}
/// <summary>
/// Creates the element text shown in the structure viewer.
/// </summary>
private string CreateElementDisplayText(
string elementName,
bool isRepeatingPrototypeRoot,
int representedInstanceCount)
{
if (!isRepeatingPrototypeRoot)
return elementName;
return string.Format(
"{0} [repeating prototype x{1}]",
elementName,
representedInstanceCount);
}
/// <summary>
/// Creates a logical destination identifier.
/// </summary>
private string CreateDestinationPath(
string structuralPath,
bool isRepeating)
{
if (!isRepeating)
return structuralPath;
return RepeatPrefix +
structuralPath;
}
}
}

View File

@ -0,0 +1,281 @@
///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.Collections.Generic;
using System.Linq;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
{
/// <summary>
/// Defines the type of a node displayed by an XML payload structure viewer.
/// </summary>
public enum PayloadTemplateNodeType
{
/// <summary>
/// XML element used primarily for structural navigation.
/// </summary>
Element,
/// <summary>
/// XML attribute that can be used as a runtime destination.
/// </summary>
Attribute,
/// <summary>
/// Text content of an XML element that can be used as a runtime destination.
/// </summary>
Text
}
/// <summary>
/// Defines how a destination behaves in the payload structure.
/// </summary>
public enum PayloadDestinationKind
{
/// <summary>
/// Destination exists only once in the payload.
/// </summary>
SingleValue,
/// <summary>
/// Destination belongs to a repeating XML prototype.
/// Multiple TBF results may therefore use the same prototype destination.
/// </summary>
RepeatingPrototype
}
/// <summary>
/// Represents one structural node of an inspected XML payload example.
/// </summary>
/// <remarks>
/// <para>
/// The model intentionally does not expose sample runtime values from the
/// customer example XML. The example file is treated as a structural
/// reference from which writable destinations and repeating prototypes are
/// derived.
/// </para>
///
/// <para>
/// Element nodes are navigation nodes. Attribute and text nodes are
/// selectable runtime destinations.
/// </para>
/// </remarks>
public class PayloadTemplateNode
{
/// <summary>
/// Initializes a new instance of the
/// <see cref="PayloadTemplateNode"/> class.
/// </summary>
public PayloadTemplateNode()
{
Children =
new List<PayloadTemplateNode>();
}
/// <summary>
/// Gets or sets the node type.
/// </summary>
public PayloadTemplateNodeType NodeType
{
get;
set;
}
/// <summary>
/// Gets or sets the original XML element or attribute name.
/// </summary>
public string Name
{
get;
set;
}
/// <summary>
/// Gets or sets the text shown by a structure viewer.
/// </summary>
public string DisplayText
{
get;
set;
}
/// <summary>
/// Gets or sets the logical destination path.
/// </summary>
/// <remarks>
/// <para>
/// Normal one-time destinations use XPath-like paths, for example:
/// </para>
///
/// <code>
/// /BATCH/FACTORY/@TESTER
/// </code>
///
/// <para>
/// Destinations belonging to a detected repeating prototype use the
/// <c>repeat:</c> prefix, for example:
/// </para>
///
/// <code>
/// repeat:/BATCH/PANEL/DUT/GROUP/TEST/@VALUE
/// </code>
///
/// <para>
/// The repeat prefix is a logical mapping identifier. It is not intended
/// to be evaluated directly as XPath.
/// </para>
/// </remarks>
public string DestinationPath
{
get;
set;
}
/// <summary>
/// Gets or sets a value indicating whether this node can be selected
/// as a runtime mapping destination.
/// </summary>
public bool IsSelectable
{
get;
set;
}
/// <summary>
/// Gets or sets the destination behavior.
/// </summary>
public PayloadDestinationKind DestinationKind
{
get;
set;
}
/// <summary>
/// Gets or sets a value indicating whether this element is the root of
/// a collapsed repeating prototype.
/// </summary>
public bool IsRepeatingPrototypeRoot
{
get;
set;
}
/// <summary>
/// Gets or sets the number of example instances represented by a
/// collapsed repeating prototype.
/// </summary>
public int ExampleInstanceCount
{
get;
set;
}
/// <summary>
/// Gets child structural nodes.
/// </summary>
public List<PayloadTemplateNode> Children
{
get;
private set;
}
/// <summary>
/// Enumerates this node and all descendant nodes.
/// </summary>
public IEnumerable<PayloadTemplateNode> Traverse()
{
yield return this;
foreach (PayloadTemplateNode child
in Children)
{
foreach (PayloadTemplateNode descendant
in child.Traverse())
{
yield return descendant;
}
}
}
}
/// <summary>
/// Contains the result of inspecting an XML payload example.
/// </summary>
public class PayloadTemplateInspectionResult
{
/// <summary>
/// Initializes a new inspection result.
/// </summary>
public PayloadTemplateInspectionResult(
string templatePath,
PayloadTemplateNode root)
{
TemplatePath =
templatePath;
Root =
root;
}
/// <summary>
/// Gets the inspected XML example path.
/// </summary>
public string TemplatePath
{
get;
private set;
}
/// <summary>
/// Gets the root structural node.
/// </summary>
public PayloadTemplateNode Root
{
get;
private set;
}
/// <summary>
/// Gets all selectable destinations.
/// </summary>
public IEnumerable<PayloadTemplateNode> Destinations
{
get
{
if (Root == null)
{
return Enumerable.Empty<
PayloadTemplateNode>();
}
return Root
.Traverse()
.Where(
node =>
node.IsSelectable);
}
}
/// <summary>
/// Finds a destination by its logical destination path.
/// </summary>
public PayloadTemplateNode FindDestination(
string destinationPath)
{
if (string.IsNullOrWhiteSpace(
destinationPath))
{
return null;
}
return Destinations.FirstOrDefault(
node =>
string.Equals(
node.DestinationPath,
destinationPath,
StringComparison.Ordinal));
}
}
}

View File

@ -0,0 +1,407 @@
///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
{
/// <summary>
/// Generates XML from a clean payload base, mappings and runtime values.
/// </summary>
/// <remarks>
/// Customer example runtime values are never copied to the output.
/// Only explicitly mapped runtime values are written.
/// </remarks>
public class XmlPayloadGenerator
{
private const string RepeatPrefix = "repeat:";
/// <summary>
/// Generates one XML payload.
/// </summary>
public PayloadGenerationResult Generate(
PayloadTemplateDefinition definition,
PayloadGenerationRequest request)
{
if (definition == null)
throw new ArgumentNullException(nameof(definition));
if (request == null)
throw new ArgumentNullException(nameof(request));
XDocument document = definition.CreateBaseDocument();
ApplySingleMappings(
document,
request.SingleMappings,
request.SingleValues);
if (request.RepeatRecords != null &&
request.RepeatRecords.Count > 0)
{
GenerateRepeatRecords(
definition,
document,
request);
}
return new PayloadGenerationResult
{
Document = document,
Payload = document.ToString()
};
}
private void ApplySingleMappings(
XDocument document,
IEnumerable<PayloadMapping> mappings,
PayloadValueSet values)
{
HashSet<string> usedDestinations =
new HashSet<string>(StringComparer.Ordinal);
foreach (PayloadMapping mapping
in mappings ?? Enumerable.Empty<PayloadMapping>())
{
ValidateMapping(mapping);
if (mapping.DestinationPath.StartsWith(
RepeatPrefix,
StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Repeating destination was supplied as a single mapping: " +
mapping.DestinationPath);
}
if (!usedDestinations.Add(mapping.DestinationPath))
{
throw new InvalidOperationException(
"One-time XML destination is mapped more than once: " +
mapping.DestinationPath);
}
string value;
if (!values.TryGetValue(mapping.SourceKey, out value))
continue;
ApplyValue(
ResolveSingleDestination(
document,
mapping.DestinationPath),
value);
}
}
private void GenerateRepeatRecords(
PayloadTemplateDefinition definition,
XDocument document,
PayloadGenerationRequest request)
{
PayloadRepeatPrototypeDefinition prototype =
ResolvePrototype(
definition,
request);
XElement parent =
ResolveElement(
document,
prototype.ParentPath);
foreach (PayloadRepeatRecord record
in request.RepeatRecords)
{
XElement instance =
prototype.CreatePrototypeElement();
HashSet<string> usedDestinations =
new HashSet<string>(
StringComparer.Ordinal);
foreach (PayloadMapping mapping
in record.Mappings ??
Enumerable.Empty<PayloadMapping>())
{
ValidateMapping(mapping);
if (!mapping.DestinationPath.StartsWith(
RepeatPrefix,
StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Non-repeating destination was supplied inside a repeat record: " +
mapping.DestinationPath);
}
if (!prototype.ContainsDestination(
mapping.DestinationPath))
{
throw new InvalidOperationException(
"Repeating destination does not belong to prototype '" +
prototype.PrototypePath +
"': " +
mapping.DestinationPath);
}
if (!usedDestinations.Add(
mapping.DestinationPath))
{
throw new InvalidOperationException(
"One repeat record maps the same XML destination more than once: " +
mapping.DestinationPath);
}
string value;
if (!record.Values.TryGetValue(
mapping.SourceKey,
out value))
{
continue;
}
ApplyValue(
ResolvePrototypeDestination(
instance,
prototype,
mapping.DestinationPath),
value);
}
parent.Add(instance);
}
}
private PayloadRepeatPrototypeDefinition ResolvePrototype(
PayloadTemplateDefinition definition,
PayloadGenerationRequest request)
{
if (!string.IsNullOrWhiteSpace(
request.RepeatPrototypePath))
{
PayloadRepeatPrototypeDefinition selected =
definition.RepeatPrototypes
.FirstOrDefault(
prototype =>
string.Equals(
prototype.PrototypePath,
request.RepeatPrototypePath,
StringComparison.Ordinal));
if (selected == null)
{
throw new InvalidOperationException(
"Repeat prototype was not found: " +
request.RepeatPrototypePath);
}
return selected;
}
foreach (PayloadRepeatRecord record
in request.RepeatRecords)
{
PayloadMapping firstMapping =
record.Mappings.FirstOrDefault();
if (firstMapping == null)
continue;
PayloadRepeatPrototypeDefinition inferred =
definition.FindRepeatPrototype(
firstMapping.DestinationPath);
if (inferred != null)
return inferred;
}
if (definition.RepeatPrototypes.Count == 1)
return definition.RepeatPrototypes[0];
throw new InvalidOperationException(
"Repeat prototype cannot be inferred.");
}
private XObject ResolveSingleDestination(
XDocument document,
string destinationPath)
{
List<XObject> matches =
EvaluateNodes(
document,
destinationPath);
if (matches.Count == 0)
throw new InvalidOperationException(
"XML destination was not found in the clean base: " +
destinationPath);
if (matches.Count > 1)
throw new InvalidOperationException(
"XML destination is ambiguous in the clean base: " +
destinationPath);
return matches[0];
}
private XObject ResolvePrototypeDestination(
XElement instance,
PayloadRepeatPrototypeDefinition prototype,
string destinationPath)
{
string suffix =
destinationPath.Substring(
prototype.PrototypePath.Length);
string relativePath =
string.IsNullOrEmpty(suffix)
? "."
: "." + suffix;
List<XObject> matches =
EvaluateNodes(
instance,
relativePath);
if (matches.Count == 0)
throw new InvalidOperationException(
"Repeat prototype destination was not found: " +
destinationPath);
if (matches.Count > 1)
throw new InvalidOperationException(
"Repeat prototype destination is ambiguous: " +
destinationPath);
return matches[0];
}
private XElement ResolveElement(
XDocument document,
string path)
{
List<XObject> matches =
EvaluateNodes(
document,
path);
if (matches.Count != 1 ||
!(matches[0] is XElement))
{
throw new InvalidOperationException(
"Prototype parent element cannot be uniquely resolved: " +
path);
}
return (XElement)matches[0];
}
private List<XObject> EvaluateNodes(
XNode context,
string xpath)
{
object evaluationResult;
try
{
evaluationResult =
context.XPathEvaluate(xpath);
}
catch (Exception exc)
{
throw new InvalidOperationException(
"Invalid XML destination path: " +
xpath,
exc);
}
IEnumerable enumerable =
evaluationResult as IEnumerable;
if (enumerable == null)
return new List<XObject>();
return enumerable
.Cast<object>()
.OfType<XObject>()
.ToList();
}
private void ApplyValue(
XObject destination,
string value)
{
string safeValue =
value ?? string.Empty;
XAttribute attribute =
destination as XAttribute;
if (attribute != null)
{
attribute.Value = safeValue;
return;
}
XElement element =
destination as XElement;
if (element != null)
{
element.Value = safeValue;
return;
}
XText text =
destination as XText;
if (text != null)
{
text.Value = safeValue;
return;
}
XCData cdata =
destination as XCData;
if (cdata != null)
{
cdata.Value = safeValue;
return;
}
throw new InvalidOperationException(
"Unsupported XML destination node type: " +
destination.GetType().FullName);
}
private void ValidateMapping(
PayloadMapping mapping)
{
if (mapping == null)
throw new InvalidOperationException(
"Payload mapping is null.");
if (string.IsNullOrWhiteSpace(
mapping.SourceKey))
{
throw new InvalidOperationException(
"Payload mapping source key is empty.");
}
if (string.IsNullOrWhiteSpace(
mapping.DestinationPath))
{
throw new InvalidOperationException(
"Payload mapping destination path is empty.");
}
}
}
}

View File

@ -0,0 +1,609 @@
///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.IO;
using System.Text;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
{
/// <summary>
/// Generates XML from the configured customer reference file and prepares
/// either a stored-procedure request or an XML-file request.
/// </summary>
public class XmlPayloadRequestBuilder
{
private readonly WriterCfg cfg;
private readonly PayloadReferenceAnalyzer analyzer;
private readonly XmlPayloadGenerator generator;
/// <summary>
/// Initializes a new XML payload request builder.
/// </summary>
public XmlPayloadRequestBuilder(
WriterCfg cfg)
{
this.cfg =
cfg ??
throw new ArgumentNullException(
nameof(cfg));
analyzer =
new PayloadReferenceAnalyzer();
generator =
new XmlPayloadGenerator();
}
/// <summary>
/// Returns the first detected repeat prototype path.
/// </summary>
public string GetDefaultRepeatPrototypePath()
{
ValidateReference();
PayloadTemplateDefinition definition =
analyzer.Analyze(
cfg.PayloadTemplatePath);
if (definition.RepeatPrototypes.Count == 0)
return null;
return definition
.RepeatPrototypes[0]
.PrototypePath;
}
/// <summary>
/// Generates a production payload and creates a stored-procedure request.
/// </summary>
/// <remarks>
/// This method preserves the original stored-procedure behavior.
/// </remarks>
public XmlPayloadBuildResult Build(
PayloadGenerationRequest generationRequest,
string archiveFilePrefix = null)
{
if (generationRequest == null)
throw new ArgumentNullException(
nameof(generationRequest));
ValidateStoredProcedureConfiguration();
string payload =
GeneratePayload(
generationRequest);
string archiveFilePath =
null;
string outputFileName =
CreatePayloadFileName(
archiveFilePrefix,
false);
if (cfg.ArchivePayload)
{
archiveFilePath =
ArchivePayload(
payload,
outputFileName,
cfg.PayloadArchivePath);
}
DataWriteRequest request =
new DataWriteRequest
{
Mode =
WriteMode.StoredProcedure
};
request.StoredProcedureParameters.Add(
new StoredProcedureWriteParameter
{
ParameterName =
cfg.PayloadParameterName,
ParameterType =
StoredProcedureParameterType.Xml,
Value =
payload
});
return new XmlPayloadBuildResult
{
Payload =
payload,
Request =
request,
ArchiveFilePath =
archiveFilePath,
OutputFileName =
outputFileName
};
}
/// <summary>
/// Generates a production payload and creates an XML-file request.
/// </summary>
/// <remarks>
/// The physical file is not written here. The returned request is sent
/// through UniDataStorageWriter and is handled by
/// <see cref="TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers.XmlFileWriter"/>.
/// </remarks>
public XmlPayloadBuildResult BuildFile(
PayloadGenerationRequest generationRequest,
string filePrefix = null)
{
if (generationRequest == null)
throw new ArgumentNullException(
nameof(generationRequest));
ValidateXmlFileConfiguration();
string payload =
GeneratePayload(
generationRequest);
string outputFileName =
CreatePayloadFileName(
filePrefix,
false);
DataWriteRequest request =
new DataWriteRequest
{
Mode =
WriteMode.Insert,
Payload =
payload,
OutputFileName =
outputFileName
};
return new XmlPayloadBuildResult
{
Payload =
payload,
Request =
request,
ArchiveFilePath =
null,
OutputFileName =
outputFileName
};
}
/// <summary>
/// Generates and saves a preview payload without executing a database
/// operation or a production XML-file write request.
/// </summary>
public XmlPayloadBuildResult BuildPreview(
PayloadGenerationRequest generationRequest,
string archiveFilePrefix = null)
{
if (generationRequest == null)
throw new ArgumentNullException(
nameof(generationRequest));
ValidateReference();
string payload =
GeneratePayload(
generationRequest);
string previewDirectory =
ResolvePreviewDirectory();
string outputFileName =
CreatePayloadFileName(
archiveFilePrefix,
true);
string archiveFilePath =
ArchivePayload(
payload,
outputFileName,
previewDirectory);
return new XmlPayloadBuildResult
{
Payload =
payload,
Request =
null,
ArchiveFilePath =
archiveFilePath,
OutputFileName =
outputFileName
};
}
/// <summary>
/// Generates the complete XML payload.
/// </summary>
private string GeneratePayload(
PayloadGenerationRequest generationRequest)
{
PayloadTemplateDefinition definition =
analyzer.Analyze(
cfg.PayloadTemplatePath);
return generator.Generate(
definition,
generationRequest)
.Payload;
}
/// <summary>
/// Validates the common XML reference configuration.
/// </summary>
private void ValidateReference()
{
if (string.IsNullOrWhiteSpace(
cfg.PayloadTemplatePath))
{
throw new InvalidOperationException(
"Payload reference XML path is not configured.");
}
if (!File.Exists(
cfg.PayloadTemplatePath))
{
throw new InvalidOperationException(
"Payload reference XML does not exist: " +
cfg.PayloadTemplatePath);
}
}
/// <summary>
/// Validates Microsoft SQL stored-procedure payload configuration.
/// </summary>
private void ValidateStoredProcedureConfiguration()
{
ValidateReference();
if (!cfg.IsStoredProcedurePayloadTarget())
{
throw new InvalidOperationException(
"Writer configuration is not an XML stored-procedure target.");
}
if (string.IsNullOrWhiteSpace(
cfg.PayloadParameterName))
{
throw new InvalidOperationException(
"Payload parameter name is not configured.");
}
string storedProcedureName =
cfg.GetTemplate(
WriteMode.StoredProcedure);
if (string.IsNullOrWhiteSpace(
storedProcedureName))
{
throw new InvalidOperationException(
"Stored procedure name is not configured.");
}
if (cfg.ArchivePayload &&
string.IsNullOrWhiteSpace(
cfg.PayloadArchivePath))
{
throw new InvalidOperationException(
"Payload archive path is not configured.");
}
}
/// <summary>
/// Validates XML-file payload configuration.
/// </summary>
private void ValidateXmlFileConfiguration()
{
ValidateReference();
if (!cfg.IsXmlFileTarget())
{
throw new InvalidOperationException(
"Writer configuration is not an XML file target.");
}
if (string.IsNullOrWhiteSpace(
cfg.DataSource))
{
throw new InvalidOperationException(
"XML output directory is not configured.");
}
}
/// <summary>
/// Resolves the directory used by Preview request.
/// </summary>
private string ResolvePreviewDirectory()
{
if (cfg.IsXmlFileTarget() &&
!string.IsNullOrWhiteSpace(
cfg.DataSource))
{
return cfg.DataSource;
}
if (!string.IsNullOrWhiteSpace(
cfg.PayloadArchivePath))
{
return cfg.PayloadArchivePath;
}
return Path.Combine(
Path.GetTempPath(),
"TBF",
"ResultsWriterPreview");
}
/// <summary>
/// Archives one generated XML payload.
/// </summary>
private string ArchivePayload(
string payload,
string fileName,
string archiveDirectory)
{
Directory.CreateDirectory(
archiveDirectory);
string fullPath =
Path.Combine(
archiveDirectory,
fileName);
fullPath =
CreateUniqueFilePath(
fullPath);
File.WriteAllText(
fullPath,
payload,
new UTF8Encoding(false));
return fullPath;
}
/// <summary>
/// Creates the standard generated payload file name.
/// </summary>
private string CreatePayloadFileName(
string payloadIdentifier,
bool preview)
{
string payloadName =
GetPayloadBaseName();
string safeIdentifier =
CreateSafeFileNamePart(
payloadIdentifier);
string timestamp =
DateTime.Now.ToString(
"yyyyMMdd_HHmmss_fff");
string fileName =
payloadName;
if (preview)
{
fileName +=
"_PREVIEW";
}
if (!string.IsNullOrWhiteSpace(
safeIdentifier))
{
fileName +=
"_" +
safeIdentifier;
}
fileName +=
"_" +
timestamp +
".xml";
return fileName;
}
/// <summary>
/// Derives a generic payload name from the customer reference file.
/// </summary>
/// <example>
/// <c>sp_InsertDashboardResults_FF_Example.xml</c> becomes
/// <c>DashboardResults_FF</c>.
/// </example>
private string GetPayloadBaseName()
{
string name =
Path.GetFileNameWithoutExtension(
cfg.PayloadTemplatePath) ??
string.Empty;
string[] suffixes =
{
"_ReferenceExample",
"_Reference",
"_Example"
};
foreach (string suffix
in suffixes)
{
if (name.EndsWith(
suffix,
StringComparison.OrdinalIgnoreCase))
{
name =
name.Substring(
0,
name.Length -
suffix.Length);
break;
}
}
const string storedProcedurePrefix =
"sp_Insert";
if (name.StartsWith(
storedProcedurePrefix,
StringComparison.OrdinalIgnoreCase))
{
name =
name.Substring(
storedProcedurePrefix.Length);
}
name =
CreateSafeFileNamePart(
name);
return string.IsNullOrWhiteSpace(
name)
? "Payload"
: name;
}
/// <summary>
/// Prevents accidental overwrite of archived preview files.
/// </summary>
private string CreateUniqueFilePath(
string fullPath)
{
if (!File.Exists(
fullPath))
{
return fullPath;
}
string directory =
Path.GetDirectoryName(
fullPath);
string name =
Path.GetFileNameWithoutExtension(
fullPath);
string extension =
Path.GetExtension(
fullPath);
int index =
1;
string candidate;
do
{
candidate =
Path.Combine(
directory,
string.Format(
"{0}_{1}{2}",
name,
index,
extension));
index++;
}
while (File.Exists(
candidate));
return candidate;
}
/// <summary>
/// Converts arbitrary text to a safe file-name component.
/// </summary>
private string CreateSafeFileNamePart(
string value)
{
if (string.IsNullOrWhiteSpace(
value))
{
return string.Empty;
}
string result =
value.Trim();
foreach (char invalidCharacter
in Path.GetInvalidFileNameChars())
{
result =
result.Replace(
invalidCharacter,
'_');
}
return result;
}
}
/// <summary>
/// Contains generated XML and the optional physical write request.
/// </summary>
public class XmlPayloadBuildResult
{
/// <summary>
/// Gets the complete generated XML.
/// </summary>
public string Payload
{
get;
internal set;
}
/// <summary>
/// Gets the request sent to UniDataStorageWriter.
/// </summary>
/// <remarks>
/// The value is null for Preview request.
/// </remarks>
public DataWriteRequest Request
{
get;
internal set;
}
/// <summary>
/// Gets the preview/archive file path when the builder wrote a diagnostic copy.
/// </summary>
public string ArchiveFilePath
{
get;
internal set;
}
/// <summary>
/// Gets the generated production or preview file name.
/// </summary>
public string OutputFileName
{
get;
internal set;
}
}
}

View File

@ -12,6 +12,9 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces
{
InsertItems = new List<InsertWriteItem>();
UpdateItems = new List<UpdateWriteItem>();
StoredProcedureParameters = new List<StoredProcedureWriteParameter>();
Payload = string.Empty;
OutputFileName = string.Empty;
Mode = WriteMode.Insert;
}
@ -20,6 +23,32 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces
public List<InsertWriteItem> InsertItems { get; private set; }
public List<UpdateWriteItem> UpdateItems { get; private set; }
/// <summary>
/// Parameters passed to a stored procedure when Mode is StoredProcedure.
/// The stored procedure name itself is resolved from WriterCfg.WriteTemplates.
/// </summary>
public List<StoredProcedureWriteParameter> StoredProcedureParameters { get; private set; }
/// <summary>
/// Optional complete payload generated by a higher-level component.
/// </summary>
/// <remarks>
/// File-oriented payload writers use this field when the complete
/// serialized document is already available and must be written without
/// converting it back to individual column/value pairs.
/// </remarks>
public string Payload { get; set; }
/// <summary>
/// Optional output file name suggested by the caller.
/// </summary>
/// <remarks>
/// The physical output directory is still defined by
/// <see cref="WriterCfg.DataSource"/>. Writers may generate a safe default
/// file name when this value is empty.
/// </remarks>
public string OutputFileName { get; set; }
}
public enum WriteMode
@ -27,7 +56,22 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces
Insert,
Update,
Upsert,
Append
Append,
StoredProcedure
}
/// <summary>
/// Generic parameter types supported by the Microsoft SQL stored procedure writer.
/// </summary>
public enum StoredProcedureParameterType
{
String,
Xml,
Int32,
Int64,
Decimal,
Boolean,
DateTime
}
public class InsertWriteItem
@ -54,4 +98,25 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces
return $"WHERE {WhereParameterName} = {WhereValue} -> SET {SetParameterName} = {SetValue}";
}
}
}
/// <summary>
/// One parameter passed to a stored procedure.
/// Value is object so the writer can preserve the requested SQL data type.
/// </summary>
public class StoredProcedureWriteParameter
{
public string ParameterName { get; set; }
public object Value { get; set; }
public StoredProcedureParameterType ParameterType { get; set; }
public StoredProcedureWriteParameter()
{
ParameterType = StoredProcedureParameterType.String;
}
public override string ToString()
{
return $"{ParameterName} ({ParameterType}) = {Value}";
}
}
}

View File

@ -1,13 +1,90 @@
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Diagnostic;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces
{
/// <summary>
/// Defines the common contract implemented by all storage writer
/// technologies supported by <see cref="Writer"/>.
/// </summary>
/// <remarks>
/// Implementations of this interface encapsulate the technology-specific
/// logic required to persist data.
///
/// Examples include:
/// <list type="bullet">
/// <item>
/// <description>Microsoft SQL database writer.</description>
/// </item>
/// <item>
/// <description>CSV file writer.</description>
/// </item>
/// <item>
/// <description>XLS/XLSX file writer.</description>
/// </item>
/// <item>
/// <description>JSON file writer.</description>
/// </item>
/// <item>
/// <description>REST API writer.</description>
/// </item>
/// </list>
///
/// The caller does not need to know the concrete writer implementation.
/// The appropriate implementation is selected by <see cref="Writer"/>
/// according to the active <see cref="WriterCfg"/> configuration.
/// </remarks>
public interface IDataStorageWriter
{
/// <summary>
/// Gets the storage types, technologies and write modes supported
/// by this writer implementation.
/// </summary>
/// <remarks>
/// The capabilities are validated by <see cref="Writer"/> before
/// a write request is executed.
///
/// For example, <c>DatabaseWriter</c> can advertise support for
/// <see cref="WriteMode.Insert"/>,
/// <see cref="WriteMode.Update"/> and
/// <see cref="WriteMode.StoredProcedure"/>.
/// </remarks>
WriterCapabilities Capabilities { get; }
/// <summary>
/// Tests whether the configured data source is accessible and valid
/// for the current writer.
/// </summary>
/// <param name="validateOnly">
/// When <c>true</c>, the writer should perform only the minimum
/// validation required to verify the configured source.
/// When <c>false</c>, the implementation may perform an additional
/// lightweight connectivity test.
/// </param>
/// <returns>
/// A <see cref="WriterDiagnosticResult"/> describing whether the
/// configured source is valid and accessible.
/// </returns>
WriterDiagnosticResult TestSource(bool validateOnly);
/// <summary>
/// Writes data to the configured storage target.
/// </summary>
/// <param name="request">
/// Write request containing the operation mode and data required
/// by the selected storage implementation.
/// </param>
/// <returns>
/// A <see cref="WriterDiagnosticResult"/> describing the result
/// of the write operation.
/// </returns>
/// <remarks>
/// The interpretation of <paramref name="request"/> depends on
/// <see cref="DataWriteRequest.Mode"/>.
///
/// For example, a database writer may execute an INSERT, UPDATE
/// or stored procedure call, while a file writer may append or
/// serialize the supplied values.
/// </remarks>
WriterDiagnosticResult WriteData(DataWriteRequest request);
}
}
}

View File

@ -33,6 +33,7 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
public const string Xls = ".xls";
public const string Xlsx = ".xlsx";
public const string Json = ".json";
public const string Xml = ".xml";
}
}
}

View File

@ -72,11 +72,22 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
this.examples1Button = new System.Windows.Forms.Button();
this.technologyTypeLabel = new System.Windows.Forms.Label();
this.technologyTypeComboBox = new System.Windows.Forms.ComboBox();
this.groupBox6 = new System.Windows.Forms.GroupBox();
this.browsePayloadArchiveButton = new System.Windows.Forms.Button();
this.payloadArchivePathTextBox = new System.Windows.Forms.TextBox();
this.payloadArchivePathLabel = new System.Windows.Forms.Label();
this.archivePayloadCheckBox = new System.Windows.Forms.CheckBox();
this.payloadParameterNameTextBox = new System.Windows.Forms.TextBox();
this.payloadParameterNameLabel = new System.Windows.Forms.Label();
this.browsePayloadTemplateButton = new System.Windows.Forms.Button();
this.payloadTemplatePathTextBox = new System.Windows.Forms.TextBox();
this.payloadTemplatePathLabel = new System.Windows.Forms.Label();
this.groupBox1.SuspendLayout();
this.groupBox2.SuspendLayout();
this.groupBox3.SuspendLayout();
this.groupBox4.SuspendLayout();
this.groupBox5.SuspendLayout();
this.groupBox6.SuspendLayout();
this.SuspendLayout();
//
// nameTextBox
@ -128,9 +139,9 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
this.groupBox1.Controls.Add(this.info2Button);
this.groupBox1.Controls.Add(this.sourceTestResultTextBox);
this.groupBox1.Controls.Add(this.connectToDataSourceButton);
this.groupBox1.Location = new System.Drawing.Point(8, 258);
this.groupBox1.Location = new System.Drawing.Point(8, 394);
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(473, 461);
this.groupBox1.Size = new System.Drawing.Size(473, 325);
this.groupBox1.TabIndex = 15;
this.groupBox1.TabStop = false;
this.groupBox1.Text = "Data storage source testing";
@ -151,7 +162,7 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
this.sourceTestResultTextBox.Multiline = true;
this.sourceTestResultTextBox.Name = "sourceTestResultTextBox";
this.sourceTestResultTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.sourceTestResultTextBox.Size = new System.Drawing.Size(437, 403);
this.sourceTestResultTextBox.Size = new System.Drawing.Size(437, 267);
this.sourceTestResultTextBox.TabIndex = 15;
//
// connectToDataSourceButton
@ -168,9 +179,9 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
this.groupBox2.Controls.Add(this.info4Button);
this.groupBox2.Controls.Add(this.writeTestResultTextBox);
this.groupBox2.Controls.Add(this.writeDataByParamAndTemplateButton);
this.groupBox2.Location = new System.Drawing.Point(487, 258);
this.groupBox2.Location = new System.Drawing.Point(487, 394);
this.groupBox2.Name = "groupBox2";
this.groupBox2.Size = new System.Drawing.Size(463, 461);
this.groupBox2.Size = new System.Drawing.Size(463, 325);
this.groupBox2.TabIndex = 16;
this.groupBox2.TabStop = false;
this.groupBox2.Text = "Complete write testing";
@ -191,7 +202,7 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
this.writeTestResultTextBox.Multiline = true;
this.writeTestResultTextBox.Name = "writeTestResultTextBox";
this.writeTestResultTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.writeTestResultTextBox.Size = new System.Drawing.Size(446, 402);
this.writeTestResultTextBox.Size = new System.Drawing.Size(446, 266);
this.writeTestResultTextBox.TabIndex = 18;
//
// writeDataByParamAndTemplateButton
@ -213,9 +224,9 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
this.groupBox3.Controls.Add(this.listBoxWriteParams);
this.groupBox3.Controls.Add(this.writeParamValueTextBox);
this.groupBox3.Controls.Add(this.info5Button);
this.groupBox3.Location = new System.Drawing.Point(956, 258);
this.groupBox3.Location = new System.Drawing.Point(956, 394);
this.groupBox3.Name = "groupBox3";
this.groupBox3.Size = new System.Drawing.Size(316, 461);
this.groupBox3.Size = new System.Drawing.Size(316, 325);
this.groupBox3.TabIndex = 17;
this.groupBox3.TabStop = false;
this.groupBox3.Text = "Component interface testing";
@ -240,7 +251,7 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
//
// buttonRemoveParam
//
this.buttonRemoveParam.Location = new System.Drawing.Point(87, 94);
this.buttonRemoveParam.Location = new System.Drawing.Point(90, 159);
this.buttonRemoveParam.Name = "buttonRemoveParam";
this.buttonRemoveParam.Size = new System.Drawing.Size(75, 23);
this.buttonRemoveParam.TabIndex = 25;
@ -249,7 +260,7 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
//
// buttonAddParam
//
this.buttonAddParam.Location = new System.Drawing.Point(8, 94);
this.buttonAddParam.Location = new System.Drawing.Point(9, 159);
this.buttonAddParam.Name = "buttonAddParam";
this.buttonAddParam.Size = new System.Drawing.Size(75, 23);
this.buttonAddParam.TabIndex = 24;
@ -268,17 +279,22 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
// listBoxWriteParams
//
this.listBoxWriteParams.FormattingEnabled = true;
this.listBoxWriteParams.Location = new System.Drawing.Point(9, 123);
this.listBoxWriteParams.Location = new System.Drawing.Point(9, 188);
this.listBoxWriteParams.Name = "listBoxWriteParams";
this.listBoxWriteParams.Size = new System.Drawing.Size(297, 329);
this.listBoxWriteParams.Size = new System.Drawing.Size(297, 121);
this.listBoxWriteParams.TabIndex = 22;
//
// writeParamValueTextBox
//
this.writeParamValueTextBox.AcceptsReturn = true;
this.writeParamValueTextBox.AcceptsTab = true;
this.writeParamValueTextBox.Location = new System.Drawing.Point(9, 68);
this.writeParamValueTextBox.Multiline = true;
this.writeParamValueTextBox.Name = "writeParamValueTextBox";
this.writeParamValueTextBox.Size = new System.Drawing.Size(297, 20);
this.writeParamValueTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.writeParamValueTextBox.Size = new System.Drawing.Size(297, 85);
this.writeParamValueTextBox.TabIndex = 21;
this.writeParamValueTextBox.WordWrap = false;
//
// info5Button
//
@ -390,7 +406,6 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
this.buttonUpdateTemplate.TabIndex = 35;
this.buttonUpdateTemplate.Text = "Update";
this.buttonUpdateTemplate.UseVisualStyleBackColor = true;
this.buttonUpdateTemplate.Click += new System.EventHandler(this.buttonUpdateTemplate_Click);
//
// buttonAddTemplate
//
@ -401,7 +416,6 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
this.buttonAddTemplate.TabIndex = 34;
this.buttonAddTemplate.Text = "Add";
this.buttonAddTemplate.UseVisualStyleBackColor = true;
this.buttonAddTemplate.Click += new System.EventHandler(this.buttonAddTemplate_Click);
//
// templateEditTextBox
//
@ -468,10 +482,111 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
this.technologyTypeComboBox.Size = new System.Drawing.Size(326, 21);
this.technologyTypeComboBox.TabIndex = 25;
//
// groupBox6
//
this.groupBox6.Controls.Add(this.browsePayloadArchiveButton);
this.groupBox6.Controls.Add(this.payloadArchivePathTextBox);
this.groupBox6.Controls.Add(this.payloadArchivePathLabel);
this.groupBox6.Controls.Add(this.archivePayloadCheckBox);
this.groupBox6.Controls.Add(this.payloadParameterNameTextBox);
this.groupBox6.Controls.Add(this.payloadParameterNameLabel);
this.groupBox6.Controls.Add(this.browsePayloadTemplateButton);
this.groupBox6.Controls.Add(this.payloadTemplatePathTextBox);
this.groupBox6.Controls.Add(this.payloadTemplatePathLabel);
this.groupBox6.Location = new System.Drawing.Point(8, 258);
this.groupBox6.Name = "groupBox6";
this.groupBox6.Size = new System.Drawing.Size(1264, 130);
this.groupBox6.TabIndex = 26;
this.groupBox6.TabStop = false;
this.groupBox6.Text = "Payload template setting";
//
// browsePayloadArchiveButton
//
this.browsePayloadArchiveButton.Enabled = false;
this.browsePayloadArchiveButton.Location = new System.Drawing.Point(1130, 86);
this.browsePayloadArchiveButton.Name = "browsePayloadArchiveButton";
this.browsePayloadArchiveButton.Size = new System.Drawing.Size(110, 23);
this.browsePayloadArchiveButton.TabIndex = 8;
this.browsePayloadArchiveButton.Text = "Browse...";
this.browsePayloadArchiveButton.UseVisualStyleBackColor = true;
//
// payloadArchivePathTextBox
//
this.payloadArchivePathTextBox.Enabled = false;
this.payloadArchivePathTextBox.Location = new System.Drawing.Point(120, 88);
this.payloadArchivePathTextBox.Name = "payloadArchivePathTextBox";
this.payloadArchivePathTextBox.Size = new System.Drawing.Size(1000, 20);
this.payloadArchivePathTextBox.TabIndex = 7;
//
// payloadArchivePathLabel
//
this.payloadArchivePathLabel.AutoSize = true;
this.payloadArchivePathLabel.Location = new System.Drawing.Point(9, 91);
this.payloadArchivePathLabel.Name = "payloadArchivePathLabel";
this.payloadArchivePathLabel.Size = new System.Drawing.Size(110, 13);
this.payloadArchivePathLabel.TabIndex = 6;
this.payloadArchivePathLabel.Text = "Payload archive path:";
//
// archivePayloadCheckBox
//
this.archivePayloadCheckBox.AutoSize = true;
this.archivePayloadCheckBox.Enabled = false;
this.archivePayloadCheckBox.Location = new System.Drawing.Point(450, 57);
this.archivePayloadCheckBox.Name = "archivePayloadCheckBox";
this.archivePayloadCheckBox.Size = new System.Drawing.Size(153, 17);
this.archivePayloadCheckBox.TabIndex = 5;
this.archivePayloadCheckBox.Text = "Archive generated payload";
this.archivePayloadCheckBox.UseVisualStyleBackColor = true;
//
// payloadParameterNameTextBox
//
this.payloadParameterNameTextBox.Enabled = false;
this.payloadParameterNameTextBox.Location = new System.Drawing.Point(120, 55);
this.payloadParameterNameTextBox.Name = "payloadParameterNameTextBox";
this.payloadParameterNameTextBox.Size = new System.Drawing.Size(300, 20);
this.payloadParameterNameTextBox.TabIndex = 4;
//
// payloadParameterNameLabel
//
this.payloadParameterNameLabel.AutoSize = true;
this.payloadParameterNameLabel.Location = new System.Drawing.Point(9, 58);
this.payloadParameterNameLabel.Name = "payloadParameterNameLabel";
this.payloadParameterNameLabel.Size = new System.Drawing.Size(98, 13);
this.payloadParameterNameLabel.TabIndex = 3;
this.payloadParameterNameLabel.Text = "Payload parameter:";
//
// browsePayloadTemplateButton
//
this.browsePayloadTemplateButton.Enabled = false;
this.browsePayloadTemplateButton.Location = new System.Drawing.Point(1130, 21);
this.browsePayloadTemplateButton.Name = "browsePayloadTemplateButton";
this.browsePayloadTemplateButton.Size = new System.Drawing.Size(110, 23);
this.browsePayloadTemplateButton.TabIndex = 2;
this.browsePayloadTemplateButton.Text = "Browse...";
this.browsePayloadTemplateButton.UseVisualStyleBackColor = true;
//
// payloadTemplatePathTextBox
//
this.payloadTemplatePathTextBox.Enabled = false;
this.payloadTemplatePathTextBox.Location = new System.Drawing.Point(120, 23);
this.payloadTemplatePathTextBox.Name = "payloadTemplatePathTextBox";
this.payloadTemplatePathTextBox.Size = new System.Drawing.Size(1000, 20);
this.payloadTemplatePathTextBox.TabIndex = 1;
//
// payloadTemplatePathLabel
//
this.payloadTemplatePathLabel.AutoSize = true;
this.payloadTemplatePathLabel.Location = new System.Drawing.Point(9, 26);
this.payloadTemplatePathLabel.Name = "payloadTemplatePathLabel";
this.payloadTemplatePathLabel.Size = new System.Drawing.Size(91, 13);
this.payloadTemplatePathLabel.TabIndex = 0;
this.payloadTemplatePathLabel.Text = "Payload template:";
//
// WriterCfgCtrl
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.groupBox6);
this.Controls.Add(this.technologyTypeComboBox);
this.Controls.Add(this.technologyTypeLabel);
this.Controls.Add(this.groupBox5);
@ -497,6 +612,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
this.groupBox4.PerformLayout();
this.groupBox5.ResumeLayout(false);
this.groupBox5.PerformLayout();
this.groupBox6.ResumeLayout(false);
this.groupBox6.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
@ -547,5 +664,15 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
private System.Windows.Forms.Button buttonUpdateTemplate;
private System.Windows.Forms.Button buttonRemoveTemplate;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.GroupBox groupBox6;
private System.Windows.Forms.Label payloadTemplatePathLabel;
private System.Windows.Forms.TextBox payloadTemplatePathTextBox;
private System.Windows.Forms.Button browsePayloadTemplateButton;
private System.Windows.Forms.Label payloadParameterNameLabel;
private System.Windows.Forms.TextBox payloadParameterNameTextBox;
private System.Windows.Forms.CheckBox archivePayloadCheckBox;
private System.Windows.Forms.Label payloadArchivePathLabel;
private System.Windows.Forms.TextBox payloadArchivePathTextBox;
private System.Windows.Forms.Button browsePayloadArchiveButton;
}
}

View File

@ -0,0 +1,307 @@
///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.Linq;
using System.Windows.Forms;
using System.Xml.Linq;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
{
/// <summary>
/// Displays one XML stored-procedure payload in a read-only tree and raw XML view.
/// </summary>
/// <remarks>
/// This dialog is intended for diagnostic inspection of the exact XML value
/// that will be supplied to a stored procedure parameter.
/// </remarks>
public partial class XmlPayloadViewerDlg : Form
{
/// <summary>
/// Gets or sets the stored-procedure parameter name.
/// </summary>
public string ParameterName
{
get;
set;
}
/// <summary>
/// Gets or sets the XML payload displayed by the dialog.
/// </summary>
public string XmlPayload
{
get;
set;
}
/// <summary>
/// Initializes a new XML payload viewer.
/// </summary>
public XmlPayloadViewerDlg()
{
InitializeComponent();
}
private void XmlPayloadViewerDlg_Load(
object sender,
EventArgs e)
{
Text =
string.IsNullOrWhiteSpace(
ParameterName)
? "XML payload viewer"
: "XML payload viewer - " +
ParameterName;
parameterNameTextBox.Text =
ParameterName ??
string.Empty;
LoadPayload();
}
/// <summary>
/// Parses and displays the configured XML payload.
/// </summary>
private void LoadPayload()
{
structureTreeView.Nodes.Clear();
rawXmlTextBox.Clear();
if (string.IsNullOrWhiteSpace(
XmlPayload))
{
statusLabel.Text =
"XML payload is empty.";
return;
}
try
{
XDocument document =
XDocument.Parse(
XmlPayload,
LoadOptions.PreserveWhitespace);
rawXmlTextBox.Text =
document.ToString();
if (document.Root != null)
{
TreeNode rootNode =
CreateElementNode(
document.Root);
structureTreeView.Nodes.Add(
rootNode);
rootNode.Expand();
ExpandInitialLevels(
rootNode,
2);
}
statusLabel.Text =
"Valid XML payload.";
}
catch (Exception ex)
{
//
// Preserve the exact supplied text even when it is invalid XML.
//
rawXmlTextBox.Text =
XmlPayload;
statusLabel.Text =
"Invalid XML: " +
ex.Message;
tabControl.SelectedTab =
rawXmlTabPage;
}
}
/// <summary>
/// Creates a tree node for one XML element.
/// </summary>
private TreeNode CreateElementNode(
XElement element)
{
TreeNode elementNode =
new TreeNode(
element.Name.LocalName);
foreach (XAttribute attribute
in element.Attributes())
{
string attributeName =
attribute.IsNamespaceDeclaration
? "xmlns" +
(attribute.Name.LocalName == "xmlns"
? string.Empty
: ":" + attribute.Name.LocalName)
: "@" +
attribute.Name.LocalName;
TreeNode attributeNode =
new TreeNode(
string.Format(
"{0} = {1}",
attributeName,
FormatValue(
attribute.Value)));
attributeNode.ToolTipText =
attribute.Value ??
string.Empty;
elementNode.Nodes.Add(
attributeNode);
}
foreach (XElement child
in element.Elements())
{
elementNode.Nodes.Add(
CreateElementNode(
child));
}
if (!element.Elements().Any())
{
string textValue =
string.Concat(
element.Nodes()
.OfType<XText>()
.Select(
node =>
node.Value));
if (!string.IsNullOrWhiteSpace(
textValue))
{
TreeNode textNode =
new TreeNode(
"#text = " +
FormatValue(
textValue));
textNode.ToolTipText =
textValue;
elementNode.Nodes.Add(
textNode);
}
}
return elementNode;
}
/// <summary>
/// Formats a potentially long XML value for the tree.
/// </summary>
private string FormatValue(
string value)
{
if (value == null)
return string.Empty;
const int maxLength =
160;
string singleLine =
value
.Replace(
"\r",
" ")
.Replace(
"\n",
" ");
if (singleLine.Length <=
maxLength)
{
return singleLine;
}
return singleLine.Substring(
0,
maxLength) +
"...";
}
/// <summary>
/// Expands the first levels of the XML tree without expanding a large
/// payload completely.
/// </summary>
private void ExpandInitialLevels(
TreeNode node,
int remainingLevels)
{
if (node == null ||
remainingLevels < 0)
{
return;
}
node.Expand();
if (remainingLevels == 0)
return;
foreach (TreeNode child
in node.Nodes)
{
if (child.Nodes.Count > 0)
{
ExpandInitialLevels(
child,
remainingLevels - 1);
}
}
}
private void expandAllButton_Click(
object sender,
EventArgs e)
{
structureTreeView.ExpandAll();
}
private void collapseAllButton_Click(
object sender,
EventArgs e)
{
structureTreeView.CollapseAll();
if (structureTreeView.Nodes.Count > 0)
{
structureTreeView.Nodes[0]
.Expand();
}
}
private void copyXmlButton_Click(
object sender,
EventArgs e)
{
if (!string.IsNullOrEmpty(
rawXmlTextBox.Text))
{
Clipboard.SetText(
rawXmlTextBox.Text);
}
}
private void closeButton_Click(
object sender,
EventArgs e)
{
Close();
}
}
}

View File

@ -0,0 +1,259 @@
///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
{
partial class XmlPayloadViewerDlg
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(
bool disposing)
{
if (disposing &&
components != null)
{
components.Dispose();
}
base.Dispose(
disposing);
}
private void InitializeComponent()
{
this.parameterLabel = new System.Windows.Forms.Label();
this.parameterNameTextBox = new System.Windows.Forms.TextBox();
this.tabControl = new System.Windows.Forms.TabControl();
this.structureTabPage = new System.Windows.Forms.TabPage();
this.structureTreeView = new System.Windows.Forms.TreeView();
this.rawXmlTabPage = new System.Windows.Forms.TabPage();
this.rawXmlTextBox = new System.Windows.Forms.TextBox();
this.statusLabel = new System.Windows.Forms.Label();
this.expandAllButton = new System.Windows.Forms.Button();
this.collapseAllButton = new System.Windows.Forms.Button();
this.copyXmlButton = new System.Windows.Forms.Button();
this.closeButton = new System.Windows.Forms.Button();
this.tabControl.SuspendLayout();
this.structureTabPage.SuspendLayout();
this.rawXmlTabPage.SuspendLayout();
this.SuspendLayout();
//
// parameterLabel
//
this.parameterLabel.AutoSize = true;
this.parameterLabel.Location = new System.Drawing.Point(12, 15);
this.parameterLabel.Name = "parameterLabel";
this.parameterLabel.Size = new System.Drawing.Size(61, 13);
this.parameterLabel.TabIndex = 0;
this.parameterLabel.Text = "Parameter:";
//
// parameterNameTextBox
//
this.parameterNameTextBox.Anchor =
((System.Windows.Forms.AnchorStyles)
(((System.Windows.Forms.AnchorStyles.Top |
System.Windows.Forms.AnchorStyles.Left) |
System.Windows.Forms.AnchorStyles.Right)));
this.parameterNameTextBox.Location = new System.Drawing.Point(79, 12);
this.parameterNameTextBox.Name = "parameterNameTextBox";
this.parameterNameTextBox.ReadOnly = true;
this.parameterNameTextBox.Size = new System.Drawing.Size(809, 20);
this.parameterNameTextBox.TabIndex = 1;
//
// tabControl
//
this.tabControl.Anchor =
((System.Windows.Forms.AnchorStyles)
((((System.Windows.Forms.AnchorStyles.Top |
System.Windows.Forms.AnchorStyles.Bottom) |
System.Windows.Forms.AnchorStyles.Left) |
System.Windows.Forms.AnchorStyles.Right)));
this.tabControl.Controls.Add(this.structureTabPage);
this.tabControl.Controls.Add(this.rawXmlTabPage);
this.tabControl.Location = new System.Drawing.Point(12, 42);
this.tabControl.Name = "tabControl";
this.tabControl.SelectedIndex = 0;
this.tabControl.Size = new System.Drawing.Size(876, 520);
this.tabControl.TabIndex = 2;
//
// structureTabPage
//
this.structureTabPage.Controls.Add(this.structureTreeView);
this.structureTabPage.Location = new System.Drawing.Point(4, 22);
this.structureTabPage.Name = "structureTabPage";
this.structureTabPage.Padding = new System.Windows.Forms.Padding(3);
this.structureTabPage.Size = new System.Drawing.Size(868, 494);
this.structureTabPage.TabIndex = 0;
this.structureTabPage.Text = "Structure";
this.structureTabPage.UseVisualStyleBackColor = true;
//
// structureTreeView
//
this.structureTreeView.Dock = System.Windows.Forms.DockStyle.Fill;
this.structureTreeView.FullRowSelect = true;
this.structureTreeView.HideSelection = false;
this.structureTreeView.Location = new System.Drawing.Point(3, 3);
this.structureTreeView.Name = "structureTreeView";
this.structureTreeView.ShowNodeToolTips = true;
this.structureTreeView.Size = new System.Drawing.Size(862, 488);
this.structureTreeView.TabIndex = 0;
//
// rawXmlTabPage
//
this.rawXmlTabPage.Controls.Add(this.rawXmlTextBox);
this.rawXmlTabPage.Location = new System.Drawing.Point(4, 22);
this.rawXmlTabPage.Name = "rawXmlTabPage";
this.rawXmlTabPage.Padding = new System.Windows.Forms.Padding(3);
this.rawXmlTabPage.Size = new System.Drawing.Size(868, 494);
this.rawXmlTabPage.TabIndex = 1;
this.rawXmlTabPage.Text = "Raw XML";
this.rawXmlTabPage.UseVisualStyleBackColor = true;
//
// rawXmlTextBox
//
this.rawXmlTextBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.rawXmlTextBox.Font = new System.Drawing.Font(
"Consolas",
9F,
System.Drawing.FontStyle.Regular,
System.Drawing.GraphicsUnit.Point,
((byte)(238)));
this.rawXmlTextBox.Location = new System.Drawing.Point(3, 3);
this.rawXmlTextBox.Multiline = true;
this.rawXmlTextBox.Name = "rawXmlTextBox";
this.rawXmlTextBox.ReadOnly = true;
this.rawXmlTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.rawXmlTextBox.Size = new System.Drawing.Size(862, 488);
this.rawXmlTextBox.TabIndex = 0;
this.rawXmlTextBox.WordWrap = false;
//
// statusLabel
//
this.statusLabel.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left)));
this.statusLabel.AutoSize = true;
this.statusLabel.Location = new System.Drawing.Point(12, 579);
this.statusLabel.Name = "statusLabel";
this.statusLabel.Size = new System.Drawing.Size(0, 13);
this.statusLabel.TabIndex = 3;
//
// expandAllButton
//
this.expandAllButton.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left)));
this.expandAllButton.Location = new System.Drawing.Point(12, 608);
this.expandAllButton.Name = "expandAllButton";
this.expandAllButton.Size = new System.Drawing.Size(95, 30);
this.expandAllButton.TabIndex = 4;
this.expandAllButton.Text = "Expand all";
this.expandAllButton.UseVisualStyleBackColor = true;
this.expandAllButton.Click +=
new System.EventHandler(this.expandAllButton_Click);
//
// collapseAllButton
//
this.collapseAllButton.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left)));
this.collapseAllButton.Location = new System.Drawing.Point(113, 608);
this.collapseAllButton.Name = "collapseAllButton";
this.collapseAllButton.Size = new System.Drawing.Size(95, 30);
this.collapseAllButton.TabIndex = 5;
this.collapseAllButton.Text = "Collapse all";
this.collapseAllButton.UseVisualStyleBackColor = true;
this.collapseAllButton.Click +=
new System.EventHandler(this.collapseAllButton_Click);
//
// copyXmlButton
//
this.copyXmlButton.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Right)));
this.copyXmlButton.Location = new System.Drawing.Point(682, 608);
this.copyXmlButton.Name = "copyXmlButton";
this.copyXmlButton.Size = new System.Drawing.Size(95, 30);
this.copyXmlButton.TabIndex = 6;
this.copyXmlButton.Text = "Copy XML";
this.copyXmlButton.UseVisualStyleBackColor = true;
this.copyXmlButton.Click +=
new System.EventHandler(this.copyXmlButton_Click);
//
// closeButton
//
this.closeButton.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Right)));
this.closeButton.DialogResult = System.Windows.Forms.DialogResult.OK;
this.closeButton.Location = new System.Drawing.Point(793, 608);
this.closeButton.Name = "closeButton";
this.closeButton.Size = new System.Drawing.Size(95, 30);
this.closeButton.TabIndex = 7;
this.closeButton.Text = "Close";
this.closeButton.UseVisualStyleBackColor = true;
this.closeButton.Click +=
new System.EventHandler(this.closeButton_Click);
//
// XmlPayloadViewerDlg
//
this.AcceptButton = this.closeButton;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(900, 650);
this.Controls.Add(this.closeButton);
this.Controls.Add(this.copyXmlButton);
this.Controls.Add(this.collapseAllButton);
this.Controls.Add(this.expandAllButton);
this.Controls.Add(this.statusLabel);
this.Controls.Add(this.tabControl);
this.Controls.Add(this.parameterNameTextBox);
this.Controls.Add(this.parameterLabel);
this.MinimumSize = new System.Drawing.Size(650, 450);
this.Name = "XmlPayloadViewerDlg";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "XML payload viewer";
this.Load +=
new System.EventHandler(this.XmlPayloadViewerDlg_Load);
this.tabControl.ResumeLayout(false);
this.structureTabPage.ResumeLayout(false);
this.rawXmlTabPage.ResumeLayout(false);
this.rawXmlTabPage.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
private System.Windows.Forms.Label parameterLabel;
private System.Windows.Forms.TextBox parameterNameTextBox;
private System.Windows.Forms.TabControl tabControl;
private System.Windows.Forms.TabPage structureTabPage;
private System.Windows.Forms.TreeView structureTreeView;
private System.Windows.Forms.TabPage rawXmlTabPage;
private System.Windows.Forms.TextBox rawXmlTextBox;
private System.Windows.Forms.Label statusLabel;
private System.Windows.Forms.Button expandAllButton;
private System.Windows.Forms.Button collapseAllButton;
private System.Windows.Forms.Button copyXmlButton;
private System.Windows.Forms.Button closeButton;
}
}

View File

@ -1,5 +1,6 @@
using Common;
using Config.Entities;
using FluentNHibernate.MappingModel.Output;
using System;
using System.Collections.Generic;
using TBF.Rig.Generic;
@ -114,6 +115,9 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
case TechnologyTypes.Json:
return new JsonWriter(cfg);
case TechnologyTypes.Xml:
return new XmlFileWriter(cfg);
default:
throw new NotSupportedException(
string.Format("Unsupported file technology type: '{0}'", cfg.TechnologyType));
@ -156,7 +160,7 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
public void Initialize()
{
// throw new NotImplementedException();
// throw new NotImplementedException();
}
public void StartChangeHandler()
@ -166,7 +170,7 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
public void StopChangeHandler()
{
// throw new NotImplementedException();
// throw new NotImplementedException();
}
private WriterDiagnosticResult ValidateCapabilities(IDataStorageWriter writer, DataWriteRequest request)

View File

@ -5,6 +5,7 @@ using Config.Entities;
///
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Serialization;
using TBF.Resources;
using TBF.Rig.Generic;
@ -14,86 +15,199 @@ using static TBF.Rig.Output.DataStorage.UniDataStorageWriter.Types;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
{
///
/// Class and file name is preserved for backward compatibility
///
/// <summary>
/// Provides persistent configuration for the <see cref="Writer"/> component.
/// </summary>
/// <remarks>
/// <para>
/// The class name and file name are preserved for backward compatibility
/// with existing TBF configurations.
/// </para>
/// <para>
/// Configuration is serializable through <see cref="XmlSerializer"/> and is
/// also exposed through <see cref="IParamsProvider"/>. The two representations
/// are intentionally kept symmetrical so that values survive component
/// save/reload regardless of which persistence path is used by the host.
/// </para>
/// </remarks>
public class WriterCfg : ComponentCfgBase, Generic.IComponentCfg, IParamsProvider
{
public static XmlSerializer Serializer = XmlSerializer.FromTypes(new[] { typeof(WriterCfg) })[0];
public override XmlSerializer GetSerializer() { return Serializer; }
public IComponentCfgCtrl GetControl(IList<Config.Entities.Component> cmpntEntities) { return new WriterCfgCtrl(); }
/// <summary>
/// Serializer used by the TBF configuration framework.
/// </summary>
public static XmlSerializer Serializer =
XmlSerializer.FromTypes(new[] { typeof(WriterCfg) })[0];
/// <summary>
/// Internal storage type identifier.
/// Returns the serializer associated with this configuration type.
/// </summary>
public override XmlSerializer GetSerializer()
{
return Serializer;
}
/// <summary>
/// Creates the configuration control used to edit this component.
/// </summary>
public IComponentCfgCtrl GetControl(
IList<Config.Entities.Component> cmpntEntities)
{
return new WriterCfgCtrl();
}
/// <summary>
/// Storage type identifier, for example local database, remote database
/// or local file.
/// </summary>
public string DataStorageType;
/// <summary>
/// Data source definition, e.g. file path, connection string, URL, etc.
/// Data source definition. Its interpretation depends on the selected
/// technology and can represent a connection string, file path or URL.
/// </summary>
public string DataSource;
/// <summary>
/// Original field name preserved for backward compatibility.
/// For writer semantics this represents the write template.
/// Legacy single-template field preserved for backward compatibility.
/// </summary>
/// <remarks>
/// New configurations should use <see cref="WriteTemplates"/>. The field
/// is kept synchronized with the currently selected <see cref="WriteMode"/>
/// by the configuration control whenever possible.
/// </remarks>
public string QueryTemplate;
/// <summary>
/// Technology used by the configured storage target.
/// </summary>
public string TechnologyType;
/// <summary>
/// Default write operation used by the component.
/// </summary>
public WriteMode WriteMode;
/// <summary>
/// Collection of write-mode-specific templates.
/// </summary>
/// <remarks>
/// Each entry uses the format <c>WriteMode|Template</c>, for example:
/// <code>
/// Insert|INSERT INTO dbo.Results ({0}) VALUES ({1})
/// Update|UPDATE dbo.Results SET {2}={3} WHERE {0}={1}
/// StoredProcedure|dbo.sp_InsertDashboardResults_FF
/// </code>
/// </remarks>
public List<string> WriteTemplates;
/// <summary>
/// Private parameterless constructor invoked by all other constructors.
/// Path to an optional external XML payload template.
/// </summary>
WriterCfg()
public string PayloadTemplatePath;
/// <summary>
/// Name of the stored procedure parameter receiving the generated payload.
/// </summary>
public string PayloadParameterName;
/// <summary>
/// Specifies whether generated payloads should also be archived to disk.
/// </summary>
public bool ArchivePayload;
/// <summary>
/// Directory used for optional payload archiving.
/// </summary>
public string PayloadArchivePath;
/// <summary>
/// Initializes a new configuration instance with safe defaults.
/// </summary>
private WriterCfg()
{
InitializeAll();
}
public WriterCfg(string name, IComponentFactory factory)
/// <summary>
/// Initializes a new configuration instance.
/// </summary>
/// <param name="name">Component name.</param>
/// <param name="factory">Factory owning the component.</param>
public WriterCfg(
string name,
IComponentFactory factory)
: this()
{
this.Name = name;
this.Factory = factory;
Name = name;
Factory = factory;
}
/// <summary>
/// Gets the configured component name.
/// </summary>
public string ComponentName
{
get { return Name; }
}
/// <summary>
/// Initializes all UniDataStorageWriter-specific configuration fields.
/// </summary>
public void InitializeAll()
{
DataStorageType = string.Empty;
DataSource = string.Empty;
QueryTemplate = string.Empty;
TechnologyType = string.Empty;
WriteMode = WriteMode.Insert;
WriteTemplates = new List<string>();
PayloadTemplatePath = string.Empty;
PayloadParameterName = string.Empty;
ArchivePayload = false;
PayloadArchivePath = string.Empty;
}
private readonly string[] paramNames = new string[]
private readonly string[] paramNames =
{
"Data Storage type",
"Technology type",
"Data source",
"Query template",
"Data Storage type", // 0
"Technology type", // 1
"Data source", // 2
"Query template", // 3 - legacy/backward-compatible mirror
"Write mode", // 4
"Write templates", // 5
"Payload template path", // 6
"Payload parameter name", // 7
"Archive payload", // 8
"Payload archive path", // 9
};
public string ParamName(int i) { return paramNames[i]; }
/// <summary>
/// Returns the display name of one exposed configuration parameter.
/// </summary>
public string ParamName(int i)
{
return paramNames[i];
}
public int ParamsCount() { return paramNames.Length; }
/// <summary>
/// Returns the number of parameters exposed through <see cref="IParamsProvider"/>.
/// </summary>
public int ParamsCount()
{
return paramNames.Length;
}
/// <summary>
/// Returns predefined values for parameters that use a fixed set of options.
/// </summary>
public ICollection<string> ParamValues(int i)
{
switch (i)
{
case 0:
return new string[]
return new[]
{
StorageTypes.RestApi,
StorageTypes.LocalDatabase,
@ -107,55 +221,182 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
{
case StorageTypes.LocalDatabase:
case StorageTypes.RemoteDatabase:
return new string[]
return new[]
{
TechnologyTypes.MicrosoftSql,
TechnologyTypes.MySqlMariaDb,
TechnologyTypes.SQLite,
TechnologyTypes.MicrosoftSql,
TechnologyTypes.MySqlMariaDb,
TechnologyTypes.SQLite,
};
case StorageTypes.LocalFile:
case StorageTypes.RemoteFile:
return new string[]
return new[]
{
TechnologyTypes.Csv,
TechnologyTypes.Xls,
TechnologyTypes.Json,
TechnologyTypes.Csv,
TechnologyTypes.Xls,
TechnologyTypes.Xlsx,
TechnologyTypes.Json,
TechnologyTypes.Xml,
};
default:
return null;
}
case 2:
case 3:
case 4:
return new[]
{
WriteMode.Insert.ToString(),
WriteMode.Update.ToString(),
WriteMode.StoredProcedure.ToString(),
};
case 8:
return new[]
{
bool.FalseString,
bool.TrueString,
};
default:
return null;
}
}
/// <summary>
/// Returns either one parameter value or a configuration summary.
/// </summary>
/// <param name="i">
/// Parameter index. A negative value returns a human-readable summary.
/// </param>
public string ToString(int i)
{
return string.Format(
"Name={0}, DataStorageType={1}, TechnologyType={2}",
Name,
DataStorageType,
TechnologyType);
}
public CfgUpdateFlags UpdateParam(int i, string strValue)
{
switch (i)
{
case 0: DataStorageType = strValue; return CfgUpdateFlags.RestartRqrd;
case 1: TechnologyType = strValue; return CfgUpdateFlags.RestartRqrd;
case 2: DataSource = strValue; return CfgUpdateFlags.RestartRqrd;
case 3: QueryTemplate = strValue; return CfgUpdateFlags.RestartRqrd;
default: return CfgUpdateFlags.None;
case 0:
return DataStorageType ?? string.Empty;
case 1:
return TechnologyType ?? string.Empty;
case 2:
return DataSource ?? string.Empty;
case 3:
return QueryTemplate ?? string.Empty;
case 4:
return WriteMode.ToString();
case 5:
return SerializeWriteTemplates();
case 6:
return PayloadTemplatePath ?? string.Empty;
case 7:
return PayloadParameterName ?? string.Empty;
case 8:
return ArchivePayload.ToString();
case 9:
return PayloadArchivePath ?? string.Empty;
default:
return string.Format(
"Name={0}, DataStorageType={1}, TechnologyType={2}, WriteMode={3}",
Name,
DataStorageType,
TechnologyType,
WriteMode);
}
}
public bool ValidateParam(int i, string strValue, out string message)
/// <summary>
/// Updates one parameter exposed through <see cref="IParamsProvider"/>.
/// </summary>
public CfgUpdateFlags UpdateParam(
int i,
string strValue)
{
strValue = strValue ?? string.Empty;
switch (i)
{
case 0:
DataStorageType = strValue;
return CfgUpdateFlags.RestartRqrd;
case 1:
TechnologyType = strValue;
return CfgUpdateFlags.RestartRqrd;
case 2:
DataSource = strValue;
return CfgUpdateFlags.RestartRqrd;
case 3:
QueryTemplate = strValue;
return CfgUpdateFlags.RestartRqrd;
case 4:
{
WriteMode parsedMode;
if (Enum.TryParse(
strValue,
true,
out parsedMode))
{
WriteMode = parsedMode;
}
return CfgUpdateFlags.RestartRqrd;
}
case 5:
DeserializeWriteTemplates(strValue);
return CfgUpdateFlags.RestartRqrd;
case 6:
PayloadTemplatePath = strValue;
return CfgUpdateFlags.RestartRqrd;
case 7:
PayloadParameterName = strValue;
return CfgUpdateFlags.RestartRqrd;
case 8:
{
bool parsedValue;
if (bool.TryParse(
strValue,
out parsedValue))
{
ArchivePayload = parsedValue;
}
return CfgUpdateFlags.RestartRqrd;
}
case 9:
PayloadArchivePath = strValue;
return CfgUpdateFlags.RestartRqrd;
default:
return CfgUpdateFlags.None;
}
}
/// <summary>
/// Validates one parameter exposed through <see cref="IParamsProvider"/>.
/// </summary>
public bool ValidateParam(
int i,
string strValue,
out string message)
{
message = string.Empty;
strValue = strValue ?? string.Empty;
@ -171,8 +412,10 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
return true;
case 1:
if ((DataStorageType == StorageTypes.LocalDatabase || DataStorageType == StorageTypes.RemoteDatabase ||
DataStorageType == StorageTypes.LocalFile || DataStorageType == StorageTypes.RemoteFile) &&
if ((DataStorageType == StorageTypes.LocalDatabase ||
DataStorageType == StorageTypes.RemoteDatabase ||
DataStorageType == StorageTypes.LocalFile ||
DataStorageType == StorageTypes.RemoteFile) &&
string.IsNullOrWhiteSpace(strValue))
{
message = "Technology type must be selected.";
@ -189,11 +432,80 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
return true;
case 3:
if (string.IsNullOrWhiteSpace(strValue))
// Legacy mirror only. New configurations use WriteTemplates.
return true;
case 4:
{
message = "Query template must not be empty.";
WriteMode parsedMode;
if (!Enum.TryParse(
strValue,
true,
out parsedMode))
{
message = "Invalid write mode.";
return false;
}
return true;
}
case 5:
// An empty template list is valid for technologies that do
// not use templates. Technology-specific validation is done
// by the writer/configuration UI.
return true;
case 6:
if (UsesXmlPayload() &&
string.IsNullOrWhiteSpace(strValue))
{
message =
"Payload reference XML path must not be empty for XML payload output.";
return false;
}
return true;
case 7:
if (IsStoredProcedurePayloadTarget() &&
string.IsNullOrWhiteSpace(strValue))
{
message =
"Payload parameter name must not be empty for stored procedure XML output.";
return false;
}
return true;
case 8:
{
bool parsedValue;
if (!bool.TryParse(
strValue,
out parsedValue))
{
message = "Archive payload must be True or False.";
return false;
}
return true;
}
case 9:
if (ArchivePayload &&
string.IsNullOrWhiteSpace(strValue))
{
message =
"Payload archive path must not be empty when payload archiving is enabled.";
return false;
}
return true;
default:
@ -202,48 +514,140 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
}
}
private void CopyContentTo(WriterCfg prms)
/// <summary>
/// Returns whether the configured target is an XML payload file.
/// </summary>
public bool IsXmlFileTarget()
{
prms.DataStorageType = this.DataStorageType;
prms.TechnologyType = this.TechnologyType;
prms.DataSource = this.DataSource;
prms.QueryTemplate = this.QueryTemplate;
prms.WriteMode = this.WriteMode;
string storageType =
(DataStorageType ?? string.Empty).Trim();
prms.WriteTemplates = new List<string>();
string technologyType =
(TechnologyType ?? string.Empty).Trim();
if (this.WriteTemplates != null)
{
foreach (string item in this.WriteTemplates)
{
prms.WriteTemplates.Add(item);
}
}
}
public IParamsProvider Clone()
{
WriterCfg pars = new WriterCfg();
CopyContentTo(pars);
return pars;
return
(storageType == StorageTypes.LocalFile ||
storageType == StorageTypes.RemoteFile) &&
technologyType == TechnologyTypes.Xml;
}
/// <summary>
/// Strongly typed helper for internal use.
/// Returns whether the configured target is a Microsoft SQL stored
/// procedure receiving a generated XML payload.
/// </summary>
public WriterCfg ShallowCopy()
public bool IsStoredProcedurePayloadTarget()
{
WriterCfg copy = new WriterCfg(this.Name, this.Factory);
string storageType =
(DataStorageType ?? string.Empty).Trim();
string technologyType =
(TechnologyType ?? string.Empty).Trim();
return
(storageType == StorageTypes.LocalDatabase ||
storageType == StorageTypes.RemoteDatabase) &&
technologyType == TechnologyTypes.MicrosoftSql &&
WriteMode == WriteMode.StoredProcedure;
}
/// <summary>
/// Returns whether the configuration requires ResultsWriter XML payload
/// generation.
/// </summary>
public bool UsesXmlPayload()
{
return
IsXmlFileTarget() ||
IsStoredProcedurePayloadTarget();
}
/// <summary>
/// Copies all UniDataStorageWriter-specific fields to another instance.
/// </summary>
private void CopyContentTo(
WriterCfg target)
{
target.DataStorageType = DataStorageType;
target.TechnologyType = TechnologyType;
target.DataSource = DataSource;
target.QueryTemplate = QueryTemplate;
target.WriteMode = WriteMode;
target.WriteTemplates =
WriteTemplates != null
? new List<string>(WriteTemplates)
: new List<string>();
target.PayloadTemplatePath = PayloadTemplatePath;
target.PayloadParameterName = PayloadParameterName;
target.ArchivePayload = ArchivePayload;
target.PayloadArchivePath = PayloadArchivePath;
}
/// <summary>
/// Creates a copy exposed through <see cref="IParamsProvider"/>.
/// </summary>
public IParamsProvider Clone()
{
WriterCfg copy = new WriterCfg();
CopyContentTo(copy);
return copy;
}
public bool UpdateEmbeddedDbEntity()
/// <summary>
/// Creates a strongly typed shallow copy of the configuration.
/// </summary>
public WriterCfg ShallowCopy()
{
return true; /// =OK, do nothing
WriterCfg copy =
new WriterCfg(Name, Factory);
CopyContentTo(copy);
return copy;
}
private static bool TryParseTemplateItem(string item, out WriteMode mode, out string template)
/// <summary>
/// Indicates that no additional embedded database update is required.
/// </summary>
public bool UpdateEmbeddedDbEntity()
{
return true;
}
/// <summary>
/// Resolves the template associated with the requested write mode.
/// </summary>
public string GetTemplate(
WriteMode mode)
{
if (WriteTemplates != null)
{
foreach (string item in WriteTemplates)
{
WriteMode parsedMode;
string template;
if (TryParseTemplateItem(
item,
out parsedMode,
out template) &&
parsedMode == mode)
{
return template;
}
}
}
return QueryTemplate ?? string.Empty;
}
/// <summary>
/// Parses a <c>WriteMode|Template</c> configuration entry.
/// </summary>
private static bool TryParseTemplateItem(
string item,
out WriteMode mode,
out string template)
{
mode = WriteMode.Insert;
template = string.Empty;
@ -252,35 +656,72 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
return false;
int separatorIndex = item.IndexOf('|');
if (separatorIndex <= 0)
return false;
string modeText = item.Substring(0, separatorIndex).Trim();
template = item.Substring(separatorIndex + 1).Trim();
string modeText =
item.Substring(0, separatorIndex).Trim();
if (!Enum.TryParse(modeText, true, out mode))
return false;
template =
item.Substring(separatorIndex + 1).Trim();
return true;
return Enum.TryParse(
modeText,
true,
out mode);
}
public string GetTemplate(WriteMode mode)
/// <summary>
/// Serializes the write-template collection into one parameter string.
/// </summary>
/// <remarks>
/// The UI stores one template per line, therefore newline is used as
/// the parameter-level separator. This representation is intended for
/// <see cref="IParamsProvider"/> compatibility; XML serialization still
/// persists <see cref="WriteTemplates"/> as a normal list.
/// </remarks>
private string SerializeWriteTemplates()
{
if (WriteTemplates == null)
return string.Empty;
foreach (string item in WriteTemplates)
if (WriteTemplates == null ||
WriteTemplates.Count == 0)
{
WriteMode m;
string t;
if (TryParseTemplateItem(item, out m, out t) && m == mode)
{
return t;
}
return string.Empty;
}
return QueryTemplate ?? string.Empty; // fallback
return string.Join(
"\n",
WriteTemplates.Where(
item => item != null));
}
/// <summary>
/// Restores the write-template collection from one parameter string.
/// </summary>
private void DeserializeWriteTemplates(
string value)
{
WriteTemplates = new List<string>();
if (string.IsNullOrWhiteSpace(value))
return;
string normalized =
value.Replace("\r\n", "\n")
.Replace('\r', '\n');
string[] items =
normalized.Split(
new[] { '\n' },
StringSplitOptions.RemoveEmptyEntries);
foreach (string item in items)
{
string trimmed = item.Trim();
if (!string.IsNullOrWhiteSpace(trimmed))
WriteTemplates.Add(trimmed);
}
}
}
}
}

View File

@ -1,27 +1,68 @@
using System;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Diagnostic;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI;
using static TBF.Rig.Output.DataStorage.UniDataStorageWriter.Types;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers
{
/// <summary>
/// Database writer implementation for Microsoft SQL.
/// Uses full SQL template defined in cfg.QueryTemplate.
/// Provides data writing support for Microsoft SQL databases.
/// </summary>
/// <remarks>
/// The writer supports multiple write modes and resolves the corresponding
/// SQL command or stored procedure name from <see cref="WriterCfg"/>.
///
/// Currently supported operations are:
/// <list type="bullet">
/// <item>
/// <description>
/// <see cref="WriteMode.Insert"/> - executes an INSERT statement generated
/// from the configured template.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="WriteMode.Update"/> - executes one or more UPDATE statements
/// generated from the configured template.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="WriteMode.StoredProcedure"/> - executes a configured stored
/// procedure using strongly typed parameters supplied by the write request.
/// </description>
/// </item>
/// </list>
///
/// Database connection information is taken from
/// <see cref="WriterCfg.DataSource"/>.
/// </remarks>
public class DatabaseWriter : IDataStorageWriter
{
private readonly WriterCfg cfg;
/// <summary>
/// Initializes a new instance of the <see cref="DatabaseWriter"/> class.
/// </summary>
/// <param name="cfg">
/// Writer configuration containing the database connection string and
/// write templates.
/// </param>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="cfg"/> is <c>null</c>.
/// </exception>
public DatabaseWriter(WriterCfg cfg)
{
this.cfg = cfg ?? throw new ArgumentNullException(nameof(cfg));
}
/// <summary>
/// Gets the storage, technology and write modes supported by this writer.
/// </summary>
public WriterCapabilities Capabilities
{
get
@ -35,11 +76,23 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers
caps.SupportedWriteModes.Add(WriteMode.Insert);
caps.SupportedWriteModes.Add(WriteMode.Update);
caps.SupportedWriteModes.Add(WriteMode.StoredProcedure);
return caps;
}
}
/// <summary>
/// Tests whether the configured Microsoft SQL data source is accessible.
/// </summary>
/// <param name="validateOnly">
/// When <c>true</c>, only the connection is opened and validated.
/// When <c>false</c>, an additional lightweight <c>SELECT 1</c> command
/// is executed.
/// </param>
/// <returns>
/// Diagnostic information describing whether the connection test succeeded.
/// </returns>
public WriterDiagnosticResult TestSource(bool validateOnly)
{
if (string.IsNullOrWhiteSpace(cfg.DataSource))
@ -68,6 +121,20 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers
}
}
/// <summary>
/// Executes a data write operation according to the mode specified
/// in the supplied request.
/// </summary>
/// <param name="request">
/// Write request containing the write mode and data required by the
/// selected operation.
/// </param>
/// <returns>
/// Diagnostic result describing the outcome of the operation.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="request"/> is <c>null</c>.
/// </exception>
public WriterDiagnosticResult WriteData(DataWriteRequest request)
{
if (request == null)
@ -81,95 +148,266 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers
case WriteMode.Update:
return ExecuteUpdate(request);
case WriteMode.StoredProcedure:
return ExecuteStoredProcedure(request);
default:
return Fail("Mode not supported: " + request.Mode);
}
}
/// <summary>
/// Executes an INSERT operation using the configured insert template.
/// </summary>
/// <param name="request">
/// Request containing the column/value pairs to be inserted.
/// </param>
/// <returns>
/// Diagnostic result describing the INSERT operation.
/// </returns>
private WriterDiagnosticResult ExecuteInsert(DataWriteRequest request)
{
// Validate input
if (request.InsertItems == null || request.InsertItems.Count == 0)
return Fail("No insert items provided.");
// Resolve template for current write mode
string template = cfg.GetTemplate(request.Mode);
if (string.IsNullOrWhiteSpace(template))
return Fail("Insert template is empty.");
// Build comma-separated list of column names
string columns = string.Join(", ",
request.InsertItems.Select(i => i.ColumnName));
// Build comma-separated list of SQL-formatted values
string values = string.Join(", ",
request.InsertItems.Select(i => ToSqlLiteral(i.Value)));
// Replace template placeholders:
// {0} -> column list
// {1} -> value list
string sql = template
.Replace("{0}", columns)
.Replace("{1}", values);
// Execute final SQL command
return ExecuteSql(sql, "Insert OK.");
}
/// <summary>
/// Executes one or more UPDATE operations using the configured update template.
/// </summary>
/// <param name="request">
/// Request containing update conditions and values.
/// </param>
/// <returns>
/// Diagnostic result containing the total affected row count and executed SQL.
/// </returns>
private WriterDiagnosticResult ExecuteUpdate(DataWriteRequest request)
{
// Validate input: at least one update item must be provided
if (request.UpdateItems == null || request.UpdateItems.Count == 0)
return Fail("No update items provided.");
// Resolve template for current write mode
string template = cfg.GetTemplate(request.Mode);
if (string.IsNullOrWhiteSpace(template))
return Fail("Update template is empty.");
int totalRows = 0;
// Collect all executed SQL statements for diagnostics
StringBuilder executedSql = new StringBuilder();
// Open database connection
using (SqlConnection connection = new SqlConnection(cfg.DataSource))
try
{
connection.Open();
// Process each update item separately
foreach (UpdateWriteItem item in request.UpdateItems)
using (SqlConnection connection = new SqlConnection(cfg.DataSource))
{
string sql = template;
connection.Open();
// Replace placeholders:
// {0} -> WHERE column name
// {1} -> WHERE value
// {2} -> SET column name
// {3} -> SET value
sql = sql.Replace("{0}", item.WhereParameterName);
sql = sql.Replace("{1}", ToSqlLiteral(item.WhereValue));
sql = sql.Replace("{2}", item.SetParameterName);
sql = sql.Replace("{3}", ToSqlLiteral(item.SetValue));
using (SqlCommand command = new SqlCommand(sql, connection))
foreach (UpdateWriteItem item in request.UpdateItems)
{
totalRows += command.ExecuteNonQuery();
string sql = template;
sql = sql.Replace("{0}", item.WhereParameterName);
sql = sql.Replace("{1}", ToSqlLiteral(item.WhereValue));
sql = sql.Replace("{2}", item.SetParameterName);
sql = sql.Replace("{3}", ToSqlLiteral(item.SetValue));
using (SqlCommand command = new SqlCommand(sql, connection))
{
totalRows += command.ExecuteNonQuery();
}
executedSql.AppendLine(sql);
}
executedSql.AppendLine(sql);
}
}
return new WriterDiagnosticResult
return new WriterDiagnosticResult
{
Success = true,
Message = "Update OK. Rows: " + totalRows,
ExecutedTemplate = executedSql.ToString().TrimEnd()
};
}
catch (Exception ex)
{
Success = true,
Message = "Update OK. Rows: " + totalRows,
ExecutedTemplate = executedSql.ToString().TrimEnd()
};
return Fail("Database update failed: " + ex.Message);
}
}
private WriterDiagnosticResult ExecuteSql(string sql, string successMessage)
/// <summary>
/// Executes a configured Microsoft SQL stored procedure.
/// </summary>
/// <param name="request">
/// Request containing the parameters passed to the stored procedure.
/// </param>
/// <returns>
/// Diagnostic result describing whether the stored procedure completed
/// successfully.
/// </returns>
/// <remarks>
/// The stored procedure name is resolved from
/// <see cref="WriterCfg.GetTemplate(WriteMode)"/> using
/// <see cref="WriteMode.StoredProcedure"/>.
///
/// Each item from
/// <see cref="DataWriteRequest.StoredProcedureParameters"/>
/// is converted to a strongly typed <see cref="SqlParameter"/>.
///
/// This mechanism allows XML, strings, numbers, Boolean values and
/// date/time values to be passed without manually concatenating SQL.
///
/// For example, the dashboard integration can execute:
/// <c>dbo.sp_InsertDashboardResults_FF</c>
/// with an XML parameter named <c>@DashboardResults</c>.
/// </remarks>
private WriterDiagnosticResult ExecuteStoredProcedure(DataWriteRequest request)
{
if (request.StoredProcedureParameters == null)
return Fail("Stored procedure parameter collection is not initialized.");
string procedureName = cfg.GetTemplate(WriteMode.StoredProcedure);
if (string.IsNullOrWhiteSpace(procedureName))
return Fail("Stored procedure name is empty.");
try
{
using (SqlConnection connection = new SqlConnection(cfg.DataSource))
{
connection.Open();
using (SqlCommand command = new SqlCommand(procedureName, connection))
{
command.CommandType = CommandType.StoredProcedure;
foreach (StoredProcedureWriteParameter parameter
in request.StoredProcedureParameters)
{
SqlParameter sqlParameter = CreateSqlParameter(parameter);
command.Parameters.Add(sqlParameter);
}
command.ExecuteNonQuery();
}
}
return new WriterDiagnosticResult
{
Success = true,
Message = "Stored procedure executed successfully.",
ExecutedTemplate = procedureName
};
}
catch (Exception ex)
{
return Fail("Stored procedure execution failed: " + ex.Message);
}
}
/// <summary>
/// Creates a strongly typed SQL Server parameter from a generic
/// stored procedure parameter definition.
/// </summary>
/// <param name="parameter">
/// Parameter definition supplied by the caller.
/// </param>
/// <returns>
/// A configured <see cref="SqlParameter"/> instance.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="parameter"/> is <c>null</c>.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown when the parameter name is empty.
/// </exception>
/// <exception cref="NotSupportedException">
/// Thrown when the requested parameter type is not supported.
/// </exception>
private SqlParameter CreateSqlParameter(
StoredProcedureWriteParameter parameter)
{
if (parameter == null)
throw new ArgumentNullException(nameof(parameter));
if (string.IsNullOrWhiteSpace(parameter.ParameterName))
throw new ArgumentException(
"Stored procedure parameter name must not be empty.",
nameof(parameter));
SqlDbType sqlDbType;
switch (parameter.ParameterType)
{
case StoredProcedureParameterType.String:
sqlDbType = SqlDbType.NVarChar;
break;
case StoredProcedureParameterType.Xml:
sqlDbType = SqlDbType.Xml;
break;
case StoredProcedureParameterType.Int32:
sqlDbType = SqlDbType.Int;
break;
case StoredProcedureParameterType.Int64:
sqlDbType = SqlDbType.BigInt;
break;
case StoredProcedureParameterType.Decimal:
sqlDbType = SqlDbType.Decimal;
break;
case StoredProcedureParameterType.Boolean:
sqlDbType = SqlDbType.Bit;
break;
case StoredProcedureParameterType.DateTime:
sqlDbType = SqlDbType.DateTime;
break;
default:
throw new NotSupportedException(
"Stored procedure parameter type is not supported: "
+ parameter.ParameterType);
}
SqlParameter sqlParameter =
new SqlParameter(parameter.ParameterName, sqlDbType);
sqlParameter.Value = parameter.Value ?? DBNull.Value;
return sqlParameter;
}
/// <summary>
/// Executes a raw SQL statement against the configured database.
/// </summary>
/// <param name="sql">
/// SQL statement to execute.
/// </param>
/// <param name="successMessage">
/// Message included in the successful diagnostic result.
/// </param>
/// <returns>
/// Diagnostic result containing execution status and affected row count.
/// </returns>
private WriterDiagnosticResult ExecuteSql(
string sql,
string successMessage)
{
try
{
@ -196,6 +434,24 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers
}
}
/// <summary>
/// Converts a string value to a SQL string literal.
/// </summary>
/// <param name="value">
/// Value to convert.
/// </param>
/// <returns>
/// A SQL-compatible quoted literal, or <c>NULL</c> when the input
/// value is <c>null</c>.
/// </returns>
/// <remarks>
/// Single quotes are escaped by duplication.
///
/// This method is retained for compatibility with the existing
/// template-based INSERT and UPDATE implementation. Stored procedure
/// parameters do not use this method and are passed as strongly typed
/// SQL parameters instead.
/// </remarks>
private string ToSqlLiteral(string value)
{
if (value == null)
@ -204,6 +460,15 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers
return "'" + value.Replace("'", "''") + "'";
}
/// <summary>
/// Creates a successful writer diagnostic result.
/// </summary>
/// <param name="message">
/// Human-readable diagnostic message.
/// </param>
/// <returns>
/// Successful diagnostic result.
/// </returns>
private WriterDiagnosticResult Ok(string message)
{
return new WriterDiagnosticResult
@ -213,6 +478,15 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers
};
}
/// <summary>
/// Creates a failed writer diagnostic result.
/// </summary>
/// <param name="message">
/// Human-readable error description.
/// </param>
/// <returns>
/// Failed diagnostic result.
/// </returns>
private WriterDiagnosticResult Fail(string message)
{
return new WriterDiagnosticResult

View File

@ -0,0 +1,312 @@
///
/// 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
};
}
}
}

View File

@ -1237,11 +1237,25 @@
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Diagnostic\WriterDiagnosticResult.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Enums.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Factory.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Formatters\PayloadMapping.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Formatters\PayloadReferenceAnalyzer.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Formatters\PayloadTemplateDefinition.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Formatters\PayloadTemplateInspector.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Formatters\PayloadTemplateNode.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Formatters\XmlPayloadGenerator.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Formatters\XmlPayloadRequestBuilder.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Interfaces\DataWriteRequest.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Interfaces\IDataStorageWriter.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Types.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\UI\XmlPayloadViewerDlg.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\UI\XmlPayloadViewerDlg.designer.cs">
<DependentUpon>XmlPayloadViewerDlg.cs</DependentUpon>
</Compile>
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Writer.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\WriterCfg.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Writers\XmlFileWriter.cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Writers\CsvWriter .cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Writers\DatabaseWriter .cs" />
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Writers\JsonWriter.cs" />
@ -1296,6 +1310,12 @@
<Compile Include="Rig\Output\DB\ResultsWriter\ResultsWriterResultsDlg.Designer.cs">
<DependentUpon>ResultsWriterResultsDlg.cs</DependentUpon>
</Compile>
<Compile Include="Rig\Output\DB\ResultsWriter\XmlDestinationPickerDlg.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Rig\Output\DB\ResultsWriter\XmlDestinationPickerDlg.Designer.cs">
<DependentUpon>XmlDestinationPickerDlg.cs</DependentUpon>
</Compile>
<Compile Include="Rig\Output\DB\SaveDiverterCorrections\Factory.cs" />
<Compile Include="Rig\Output\DB\SaveDiverterCorrections\SaveDiverterCorr.cs" />
<Compile Include="Rig\Output\DB\SaveDiverterCorrections\SaveDiverterCorrCfg.cs" />
@ -4633,4 +4653,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>