Compare commits
2 Commits
a0d53f9ec9
...
d3c8813012
| Author | SHA1 | Date | |
|---|---|---|---|
| d3c8813012 | |||
| 054075a341 |
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
366
TBF/Rig/Output/DB/ResultsWriter/XmlDestinationPickerDlg.Designer.cs
generated
Normal file
366
TBF/Rig/Output/DB/ResultsWriter/XmlDestinationPickerDlg.Designer.cs
generated
Normal 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;
|
||||
}
|
||||
}
|
||||
587
TBF/Rig/Output/DB/ResultsWriter/XmlDestinationPickerDlg.cs
Normal file
587
TBF/Rig/Output/DB/ResultsWriter/XmlDestinationPickerDlg.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
259
TBF/Rig/Output/DataStorage/UniDataStorageWriter/UI/XmlPayloadViewerDlg.designer.cs
generated
Normal file
259
TBF/Rig/Output/DataStorage/UniDataStorageWriter/UI/XmlPayloadViewerDlg.designer.cs
generated
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user