<Feat>: Add XML-based payload archive filename configuration, increase revision to 3.9.3145.103
Cause:
- Payload archive filenames required values from the generated XML payload.
- Date/time values needed filename-safe formatting.
- XML result values such as Passed and Failed needed configurable conversion to P and F.
Solution:
1. Added XML filename tokens
- Supports any unique non-repeating XML value using {Xml:/path}.
- Resolves filename values directly from the generated payload.
2. Added user-friendly token configuration
- Clicking inside a token opens the XML value chooser.
- Added Time format and Mask transformations.
3. Improved filename generation
- Validates XML paths and rejects ambiguous repeating values.
- Sanitizes resolved values for safe filesystem usage.
- Passes the per-meter payload context from ResultsWriter.
4. Integrated the new dialog
- Added PayloadFileNameXmlTokenDlg and its Designer file.
- Updated the TBF project configuration.
5. Increased revision
- Updated revision to 3.9.3145.103.
This commit is contained in:
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
[assembly: AssemblyVersion("3.9.3145.102")]
|
||||
[assembly: AssemblyFileVersion("3.9.3145.102")]
|
||||
[assembly: AssemblyVersion("3.9.3145.103")]
|
||||
[assembly: AssemblyFileVersion("3.9.3145.103")]
|
||||
|
||||
@@ -995,89 +995,21 @@ namespace TBF.Rig.Output.DB.ResultsWriter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the P/F file-name token from the standard TBF meter result.
|
||||
/// Resolves the legacy P/F file-name token from the aggregate meter result.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The file name is metadata and is not derived by parsing a
|
||||
/// customer-specific XML path. Existing configurations normally expose
|
||||
/// the standard result as "Test passed()". Unknown or unavailable values
|
||||
/// are represented by U so that the payload is never mislabeled as a
|
||||
/// pass or failure.
|
||||
/// This keeps the legacy {Result} token independent of test-dependent
|
||||
/// mappings such as Test passed(). New configurations may instead use an
|
||||
/// {Xml:...} token to read and optionally format a value directly from the
|
||||
/// generated payload.
|
||||
/// </remarks>
|
||||
private string ResolveMeterResultCode(
|
||||
WaterMeter wm)
|
||||
{
|
||||
if (wm == null ||
|
||||
resultsWriterCfg.SelectedItems == null)
|
||||
{
|
||||
return "U";
|
||||
}
|
||||
|
||||
Results.WMeterRsltItemSpec resultItem =
|
||||
resultsWriterCfg.SelectedItems
|
||||
.FirstOrDefault(
|
||||
item =>
|
||||
item != null &&
|
||||
string.Equals(
|
||||
item.Name,
|
||||
"Test passed()",
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (resultItem == null)
|
||||
{
|
||||
resultItem =
|
||||
resultsWriterCfg.SelectedItems
|
||||
.FirstOrDefault(
|
||||
item =>
|
||||
item != null &&
|
||||
!IsRepeatDestination(
|
||||
item.Caption) &&
|
||||
!string.IsNullOrWhiteSpace(
|
||||
item.Caption) &&
|
||||
item.Caption.EndsWith(
|
||||
"/@STATUS",
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (resultItem == null)
|
||||
if (wm == null)
|
||||
return "U";
|
||||
|
||||
string resultValue =
|
||||
resultItem.Print(
|
||||
wm);
|
||||
|
||||
string normalized =
|
||||
(resultValue ??
|
||||
string.Empty)
|
||||
.Trim()
|
||||
.ToUpperInvariant();
|
||||
|
||||
switch (normalized)
|
||||
{
|
||||
case "1":
|
||||
case "TRUE":
|
||||
case "YES":
|
||||
case "PASS":
|
||||
case "PASSED":
|
||||
case "P":
|
||||
return "P";
|
||||
|
||||
case "0":
|
||||
case "FALSE":
|
||||
case "NO":
|
||||
case "FAIL":
|
||||
case "FAILED":
|
||||
case "F":
|
||||
return "F";
|
||||
|
||||
default:
|
||||
log.WarnFormat(
|
||||
"Meter result could not be converted to P/F for WM position {0}. Value='{1}'. Using U.",
|
||||
wm.WMPosition,
|
||||
resultValue);
|
||||
|
||||
return "U";
|
||||
}
|
||||
return wm.PassedFromAllTests() ? "P" : "F";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+291
-4
@@ -7,6 +7,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
|
||||
|
||||
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
||||
@@ -100,7 +101,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
||||
string outputFileName =
|
||||
CreatePayloadFileName(
|
||||
fileNameContext,
|
||||
false);
|
||||
false,
|
||||
payload);
|
||||
|
||||
if (cfg.ArchivePayload)
|
||||
{
|
||||
@@ -188,7 +190,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
||||
string outputFileName =
|
||||
CreatePayloadFileName(
|
||||
fileNameContext,
|
||||
false);
|
||||
false,
|
||||
payload);
|
||||
|
||||
DataWriteRequest request =
|
||||
new DataWriteRequest
|
||||
@@ -257,7 +260,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
||||
string outputFileName =
|
||||
CreatePayloadFileName(
|
||||
fileNameContext,
|
||||
true);
|
||||
true,
|
||||
payload);
|
||||
|
||||
string archiveFilePath =
|
||||
ArchivePayload(
|
||||
@@ -456,7 +460,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
||||
/// </summary>
|
||||
private string CreatePayloadFileName(
|
||||
PayloadFileNameContext context,
|
||||
bool preview)
|
||||
bool preview,
|
||||
string payload)
|
||||
{
|
||||
context =
|
||||
context ??
|
||||
@@ -503,6 +508,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
||||
"PayloadName",
|
||||
GetPayloadBaseName());
|
||||
|
||||
fileName = ReplaceXmlFileNameTokens(fileName, payload);
|
||||
|
||||
if (fileName.IndexOf('{') >= 0 ||
|
||||
fileName.IndexOf('}') >= 0)
|
||||
{
|
||||
@@ -545,6 +552,286 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces XML filename tokens with values read from the generated payload.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Token syntax:
|
||||
/// {Xml:/BATCH/PANEL/@TIMESTAMP|timeformat:yyyyMMdd_HHmmss}
|
||||
/// {Xml:/BATCH/PANEL/@STATUS|mask:Passed=P;Failed=F}
|
||||
///
|
||||
/// The transformation is optional. Repeating destinations are intentionally
|
||||
/// rejected because one filename must resolve to exactly one XML value.
|
||||
/// </remarks>
|
||||
private string ReplaceXmlFileNameTokens(string template, string payload)
|
||||
{
|
||||
const string pattern = @"\{Xml:(?<definition>[^{}]+)\}";
|
||||
|
||||
return Regex.Replace(
|
||||
template,
|
||||
pattern,
|
||||
match =>
|
||||
{
|
||||
string destinationPath;
|
||||
string transformation;
|
||||
|
||||
ParseXmlFileNameTokenDefinition(
|
||||
match.Groups["definition"].Value,
|
||||
out destinationPath,
|
||||
out transformation);
|
||||
|
||||
string value = ResolveXmlFileNameValue(payload, destinationPath);
|
||||
string formattedValue = ApplyXmlFileNameTransformation(value, transformation);
|
||||
|
||||
// Sanitize every resolved XML value before the complete filename
|
||||
// is passed to Path.GetFileName(). Otherwise slashes contained in
|
||||
// values such as dates would be interpreted as directory separators.
|
||||
return CreateSafeFileNamePart(formattedValue);
|
||||
},
|
||||
RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the part following the Xml: prefix.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A vertical bar introduces a named transformation. The earlier
|
||||
/// path:DateTimeFormat syntax remains accepted for backward compatibility.
|
||||
/// </remarks>
|
||||
private void ParseXmlFileNameTokenDefinition(
|
||||
string definition,
|
||||
out string destinationPath,
|
||||
out string transformation)
|
||||
{
|
||||
string tokenDefinition = (definition ?? string.Empty).Trim();
|
||||
int transformationSeparator = tokenDefinition.IndexOf('|');
|
||||
|
||||
if (transformationSeparator >= 0)
|
||||
{
|
||||
destinationPath = tokenDefinition.Substring(0, transformationSeparator).Trim();
|
||||
transformation = tokenDefinition.Substring(transformationSeparator + 1).Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(transformation))
|
||||
throw new InvalidOperationException("XML filename transformation is empty.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Backward compatibility with the earlier syntax:
|
||||
// {Xml:/BATCH/PANEL/@TIMESTAMP:yyyyMMdd_HHmmss}
|
||||
int finalPathSeparator = tokenDefinition.LastIndexOf('/');
|
||||
int legacyFormatSeparator = tokenDefinition.IndexOf(':', finalPathSeparator + 1);
|
||||
|
||||
if (legacyFormatSeparator >= 0)
|
||||
{
|
||||
destinationPath = tokenDefinition.Substring(0, legacyFormatSeparator).Trim();
|
||||
transformation = "timeformat:" +
|
||||
tokenDefinition.Substring(legacyFormatSeparator + 1).Trim();
|
||||
|
||||
if (string.Equals(transformation, "timeformat:", StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException("XML filename time format is empty.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
destinationPath = tokenDefinition;
|
||||
transformation = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads one non-repeating attribute or element value from the generated XML.
|
||||
/// Element and attribute names are compared by local name so that a customer
|
||||
/// reference using XML namespaces can still be used without namespace setup.
|
||||
/// </summary>
|
||||
private string ResolveXmlFileNameValue(string payload, string destinationPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(destinationPath))
|
||||
throw new InvalidOperationException("XML filename token does not contain a destination path.");
|
||||
|
||||
if (destinationPath.StartsWith("repeat:", StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException("A repeating XML destination cannot be used in a filename: " + destinationPath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(payload))
|
||||
throw new InvalidOperationException("The generated XML payload is empty.");
|
||||
|
||||
string path = destinationPath.Trim();
|
||||
string attributeName = null;
|
||||
bool readText = path.EndsWith("/text()", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (readText)
|
||||
{
|
||||
path = path.Substring(0, path.Length - "/text()".Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
int attributeSeparator = path.LastIndexOf("/@", StringComparison.Ordinal);
|
||||
if (attributeSeparator >= 0)
|
||||
{
|
||||
attributeName = path.Substring(attributeSeparator + 2);
|
||||
path = path.Substring(0, attributeSeparator);
|
||||
}
|
||||
}
|
||||
|
||||
string[] segments = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
XDocument document = XDocument.Parse(payload, LoadOptions.PreserveWhitespace);
|
||||
|
||||
if (document.Root == null || segments.Length == 0 ||
|
||||
!string.Equals(document.Root.Name.LocalName, segments[0], StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("XML filename destination was not found: " + destinationPath);
|
||||
}
|
||||
|
||||
XElement element = document.Root;
|
||||
|
||||
for (int index = 1; index < segments.Length; index++)
|
||||
{
|
||||
XElement matchingChild = null;
|
||||
|
||||
foreach (XElement child in element.Elements())
|
||||
{
|
||||
if (!string.Equals(child.Name.LocalName, segments[index], StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
if (matchingChild != null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"XML filename destination is not unique. Select a non-repeating destination: " + destinationPath);
|
||||
}
|
||||
|
||||
matchingChild = child;
|
||||
}
|
||||
|
||||
if (matchingChild == null)
|
||||
throw new InvalidOperationException("XML filename destination was not found: " + destinationPath);
|
||||
|
||||
element = matchingChild;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attributeName))
|
||||
{
|
||||
XAttribute matchingAttribute = null;
|
||||
|
||||
foreach (XAttribute attribute in element.Attributes())
|
||||
{
|
||||
if (attribute.IsNamespaceDeclaration ||
|
||||
!string.Equals(attribute.Name.LocalName, attributeName, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
matchingAttribute = attribute;
|
||||
break;
|
||||
}
|
||||
|
||||
if (matchingAttribute == null)
|
||||
throw new InvalidOperationException("XML filename attribute was not found: " + destinationPath);
|
||||
|
||||
return matchingAttribute.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
return element.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies an optional named transformation to an XML value.
|
||||
/// </summary>
|
||||
private string ApplyXmlFileNameTransformation(string value, string transformation)
|
||||
{
|
||||
string sourceValue = value ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(transformation))
|
||||
return sourceValue;
|
||||
|
||||
const string timeFormatPrefix = "timeformat:";
|
||||
if (transformation.StartsWith(timeFormatPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string dateTimeFormat = transformation.Substring(timeFormatPrefix.Length).Trim();
|
||||
if (string.IsNullOrWhiteSpace(dateTimeFormat))
|
||||
throw new InvalidOperationException("XML filename time format is empty.");
|
||||
|
||||
return FormatXmlDateTime(sourceValue, dateTimeFormat);
|
||||
}
|
||||
|
||||
const string maskPrefix = "mask:";
|
||||
if (transformation.StartsWith(maskPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string mapping = transformation.Substring(maskPrefix.Length).Trim();
|
||||
if (string.IsNullOrWhiteSpace(mapping))
|
||||
throw new InvalidOperationException("XML filename mask is empty.");
|
||||
|
||||
return ApplyXmlFileNameValueMapping(sourceValue, mapping);
|
||||
}
|
||||
|
||||
// Backward compatibility with configurations created before named
|
||||
// transformations were introduced.
|
||||
const string legacyDateTimePrefix = "DateTime:";
|
||||
if (transformation.StartsWith(legacyDateTimePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string dateTimeFormat = transformation.Substring(legacyDateTimePrefix.Length).Trim();
|
||||
if (string.IsNullOrWhiteSpace(dateTimeFormat))
|
||||
throw new InvalidOperationException("XML filename DateTime format is empty.");
|
||||
|
||||
return FormatXmlDateTime(sourceValue, dateTimeFormat);
|
||||
}
|
||||
|
||||
if (transformation.IndexOf('=') >= 0)
|
||||
return ApplyXmlFileNameValueMapping(sourceValue, transformation);
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Unsupported XML filename transformation: " + transformation);
|
||||
}
|
||||
|
||||
private string ApplyXmlFileNameValueMapping(string sourceValue, string mapping)
|
||||
{
|
||||
string[] entries = mapping.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (string entry in entries)
|
||||
{
|
||||
int separator = entry.IndexOf('=');
|
||||
if (separator <= 0)
|
||||
throw new InvalidOperationException("Invalid XML filename value mapping: " + mapping);
|
||||
|
||||
string expectedValue = entry.Substring(0, separator).Trim();
|
||||
string replacementValue = entry.Substring(separator + 1).Trim();
|
||||
|
||||
if (string.Equals(sourceValue.Trim(), expectedValue, StringComparison.OrdinalIgnoreCase))
|
||||
return replacementValue;
|
||||
}
|
||||
|
||||
return sourceValue;
|
||||
}
|
||||
|
||||
private string FormatXmlDateTime(string value, string format)
|
||||
{
|
||||
DateTime parsedValue;
|
||||
bool parsed = DateTime.TryParse(
|
||||
value,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AllowWhiteSpaces,
|
||||
out parsedValue);
|
||||
|
||||
if (!parsed)
|
||||
{
|
||||
parsed = DateTime.TryParse(
|
||||
value,
|
||||
CultureInfo.CurrentCulture,
|
||||
DateTimeStyles.AllowWhiteSpaces,
|
||||
out parsedValue);
|
||||
}
|
||||
|
||||
if (!parsed)
|
||||
throw new InvalidOperationException("XML filename value is not a valid DateTime: " + value);
|
||||
|
||||
try
|
||||
{
|
||||
return parsedValue.ToString(format, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid XML filename DateTime format: " + format, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the timestamp token and supports an optional .NET format string.
|
||||
/// </summary>
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
|
||||
{
|
||||
partial class PayloadFileNameXmlTokenDlg
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.components = new System.ComponentModel.Container();
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(800, 450);
|
||||
this.Text = "PayloadFileNameXmlTokenDlg";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
///
|
||||
/// Copyright (c) 2026 Sensus Slovensko a.s.
|
||||
///
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters;
|
||||
|
||||
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects one non-repeating XML payload destination for a filename token
|
||||
/// and optionally configures a filename-safe value transformation.
|
||||
/// </summary>
|
||||
internal sealed partial class PayloadFileNameXmlTokenDlg : Form
|
||||
{
|
||||
private const string NoTransformation = "None";
|
||||
private const string TimeFormatTransformation = "Time format";
|
||||
private const string MaskTransformation = "Mask";
|
||||
|
||||
private readonly string templatePath;
|
||||
private readonly string currentDestinationPath;
|
||||
private readonly string currentTransformation;
|
||||
private readonly TreeView xmlTreeView;
|
||||
private readonly TextBox selectedPathTextBox;
|
||||
private readonly ComboBox transformationComboBox;
|
||||
private readonly ComboBox timeFormatComboBox;
|
||||
private readonly Label timeFormatHintLabel;
|
||||
private readonly Label maskHintLabel;
|
||||
private readonly DataGridView maskGrid;
|
||||
private readonly Button okButton;
|
||||
|
||||
public PayloadFileNameXmlTokenDlg(
|
||||
string templatePath,
|
||||
string currentDestinationPath,
|
||||
string currentTransformation)
|
||||
{
|
||||
this.templatePath = templatePath;
|
||||
this.currentDestinationPath = currentDestinationPath;
|
||||
this.currentTransformation = currentTransformation;
|
||||
|
||||
Text = "Select XML value for payload filename";
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
MinimumSize = new Size(760, 720);
|
||||
Size = new Size(900, 760);
|
||||
|
||||
Label templateLabel = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Location = new Point(12, 16),
|
||||
Text = "XML reference:"
|
||||
};
|
||||
|
||||
TextBox templateTextBox = new TextBox
|
||||
{
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
|
||||
Location = new Point(120, 12),
|
||||
ReadOnly = true,
|
||||
Size = new Size(750, 20),
|
||||
Text = templatePath ?? string.Empty
|
||||
};
|
||||
|
||||
xmlTreeView = new TreeView
|
||||
{
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
|
||||
HideSelection = false,
|
||||
Location = new Point(12, 44),
|
||||
ShowNodeToolTips = true,
|
||||
Size = new Size(858, 400)
|
||||
};
|
||||
|
||||
Label selectedPathLabel = new Label
|
||||
{
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
|
||||
AutoSize = true,
|
||||
Location = new Point(12, 458),
|
||||
Text = "Selected XML path:"
|
||||
};
|
||||
|
||||
selectedPathTextBox = new TextBox
|
||||
{
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
|
||||
Location = new Point(120, 454),
|
||||
ReadOnly = true,
|
||||
Size = new Size(750, 20)
|
||||
};
|
||||
|
||||
Label transformationLabel = new Label
|
||||
{
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
|
||||
AutoSize = true,
|
||||
Location = new Point(12, 492),
|
||||
Text = "Transformation:"
|
||||
};
|
||||
|
||||
transformationComboBox = new ComboBox
|
||||
{
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
Location = new Point(120, 488),
|
||||
Size = new Size(220, 21)
|
||||
};
|
||||
transformationComboBox.Items.AddRange(new object[]
|
||||
{
|
||||
NoTransformation,
|
||||
TimeFormatTransformation,
|
||||
MaskTransformation
|
||||
});
|
||||
|
||||
timeFormatComboBox = new ComboBox
|
||||
{
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
Location = new Point(120, 523),
|
||||
Size = new Size(300, 21)
|
||||
};
|
||||
timeFormatComboBox.Items.AddRange(new object[]
|
||||
{
|
||||
"yyyyMMdd_HHmmss",
|
||||
"yyyy-MM-dd_HH-mm-ss",
|
||||
"yyyyMMdd-HHmmss",
|
||||
"ddMMyyyy_HHmmss",
|
||||
"yyyyMMdd_HHmm",
|
||||
"yyyyMMdd"
|
||||
});
|
||||
|
||||
timeFormatHintLabel = new Label
|
||||
{
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
|
||||
AutoSize = true,
|
||||
ForeColor = SystemColors.GrayText,
|
||||
Location = new Point(120, 550),
|
||||
Text = "Only filename-safe formats are offered. Characters /, \\ and : are not allowed."
|
||||
};
|
||||
|
||||
maskHintLabel = new Label
|
||||
{
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
|
||||
AutoSize = true,
|
||||
ForeColor = SystemColors.GrayText,
|
||||
Location = new Point(12, 523),
|
||||
Text = "Value mask:"
|
||||
};
|
||||
|
||||
maskGrid = new DataGridView
|
||||
{
|
||||
AllowUserToAddRows = true,
|
||||
AllowUserToDeleteRows = true,
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
|
||||
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
|
||||
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize,
|
||||
Location = new Point(120, 523),
|
||||
RowHeadersVisible = false,
|
||||
Size = new Size(750, 130)
|
||||
};
|
||||
maskGrid.Columns.Add("SourceValue", "XML value");
|
||||
maskGrid.Columns.Add("TargetValue", "Filename value");
|
||||
|
||||
okButton = new Button
|
||||
{
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
|
||||
Enabled = false,
|
||||
Location = new Point(714, 674),
|
||||
Size = new Size(75, 25),
|
||||
Text = "OK"
|
||||
};
|
||||
|
||||
Button cancelButton = new Button
|
||||
{
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
|
||||
DialogResult = DialogResult.Cancel,
|
||||
Location = new Point(795, 674),
|
||||
Size = new Size(75, 25),
|
||||
Text = "Cancel"
|
||||
};
|
||||
|
||||
Controls.Add(templateLabel);
|
||||
Controls.Add(templateTextBox);
|
||||
Controls.Add(xmlTreeView);
|
||||
Controls.Add(selectedPathLabel);
|
||||
Controls.Add(selectedPathTextBox);
|
||||
Controls.Add(transformationLabel);
|
||||
Controls.Add(transformationComboBox);
|
||||
Controls.Add(timeFormatComboBox);
|
||||
Controls.Add(timeFormatHintLabel);
|
||||
Controls.Add(maskHintLabel);
|
||||
Controls.Add(maskGrid);
|
||||
Controls.Add(okButton);
|
||||
Controls.Add(cancelButton);
|
||||
|
||||
AcceptButton = okButton;
|
||||
CancelButton = cancelButton;
|
||||
|
||||
Load += PayloadFileNameXmlTokenDlg_Load;
|
||||
xmlTreeView.AfterSelect += xmlTreeView_AfterSelect;
|
||||
xmlTreeView.NodeMouseDoubleClick += xmlTreeView_NodeMouseDoubleClick;
|
||||
transformationComboBox.SelectedIndexChanged += transformationComboBox_SelectedIndexChanged;
|
||||
okButton.Click += okButton_Click;
|
||||
}
|
||||
|
||||
public string SelectedDestinationPath { get; private set; }
|
||||
|
||||
public string Transformation
|
||||
{
|
||||
get
|
||||
{
|
||||
string selectedTransformation = transformationComboBox.SelectedItem as string;
|
||||
|
||||
if (string.Equals(selectedTransformation, TimeFormatTransformation, StringComparison.Ordinal))
|
||||
{
|
||||
string format = timeFormatComboBox.SelectedItem as string;
|
||||
return string.IsNullOrWhiteSpace(format)
|
||||
? string.Empty
|
||||
: "timeformat:" + format;
|
||||
}
|
||||
|
||||
if (string.Equals(selectedTransformation, MaskTransformation, StringComparison.Ordinal))
|
||||
{
|
||||
List<string> entries = new List<string>();
|
||||
|
||||
foreach (DataGridViewRow row in maskGrid.Rows)
|
||||
{
|
||||
if (row.IsNewRow)
|
||||
continue;
|
||||
|
||||
string sourceValue = Convert.ToString(row.Cells[0].Value).Trim();
|
||||
string targetValue = Convert.ToString(row.Cells[1].Value).Trim();
|
||||
|
||||
if (sourceValue.Length == 0 && targetValue.Length == 0)
|
||||
continue;
|
||||
|
||||
entries.Add(sourceValue + "=" + targetValue);
|
||||
}
|
||||
|
||||
return entries.Count == 0
|
||||
? string.Empty
|
||||
: "mask:" + string.Join(";", entries.ToArray());
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void PayloadFileNameXmlTokenDlg_Load(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeTransformation(currentTransformation);
|
||||
|
||||
PayloadTemplateInspectionResult inspection = new PayloadTemplateInspector().Inspect(templatePath);
|
||||
if (inspection.Root == null)
|
||||
return;
|
||||
|
||||
TreeNode root = CreateTreeNode(inspection.Root);
|
||||
xmlTreeView.Nodes.Add(root);
|
||||
root.Expand();
|
||||
|
||||
TreeNode currentNode = FindTreeNode(xmlTreeView.Nodes, currentDestinationPath);
|
||||
if (currentNode != null)
|
||||
{
|
||||
ExpandParents(currentNode);
|
||||
xmlTreeView.SelectedNode = currentNode;
|
||||
currentNode.EnsureVisible();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"Failed to analyze the XML reference." + Environment.NewLine + Environment.NewLine + ex.Message,
|
||||
"XML filename value",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeTransformation(string transformation)
|
||||
{
|
||||
string definition = (transformation ?? string.Empty).Trim();
|
||||
|
||||
if (definition.StartsWith("timeformat:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SelectTimeFormat(definition.Substring("timeformat:".Length).Trim());
|
||||
return;
|
||||
}
|
||||
|
||||
if (definition.StartsWith("DateTime:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SelectTimeFormat(definition.Substring("DateTime:".Length).Trim());
|
||||
return;
|
||||
}
|
||||
|
||||
if (definition.StartsWith("mask:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SelectMask(definition.Substring("mask:".Length).Trim());
|
||||
return;
|
||||
}
|
||||
|
||||
if (definition.IndexOf('=') >= 0)
|
||||
{
|
||||
SelectMask(definition);
|
||||
return;
|
||||
}
|
||||
|
||||
if (definition.Length > 0)
|
||||
{
|
||||
SelectTimeFormat(definition);
|
||||
return;
|
||||
}
|
||||
|
||||
transformationComboBox.SelectedItem = NoTransformation;
|
||||
}
|
||||
|
||||
private void SelectTimeFormat(string format)
|
||||
{
|
||||
transformationComboBox.SelectedItem = TimeFormatTransformation;
|
||||
|
||||
int formatIndex = timeFormatComboBox.FindStringExact(format);
|
||||
timeFormatComboBox.SelectedIndex = formatIndex >= 0 ? formatIndex : 0;
|
||||
}
|
||||
|
||||
private void SelectMask(string mapping)
|
||||
{
|
||||
transformationComboBox.SelectedItem = MaskTransformation;
|
||||
|
||||
string[] entries = mapping.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (string entry in entries)
|
||||
{
|
||||
int separator = entry.IndexOf('=');
|
||||
if (separator <= 0)
|
||||
continue;
|
||||
|
||||
maskGrid.Rows.Add(
|
||||
entry.Substring(0, separator).Trim(),
|
||||
entry.Substring(separator + 1).Trim());
|
||||
}
|
||||
}
|
||||
|
||||
private void transformationComboBox_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
string selectedTransformation = transformationComboBox.SelectedItem as string;
|
||||
bool showTimeFormat = string.Equals(
|
||||
selectedTransformation,
|
||||
TimeFormatTransformation,
|
||||
StringComparison.Ordinal);
|
||||
bool showMask = string.Equals(
|
||||
selectedTransformation,
|
||||
MaskTransformation,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
timeFormatComboBox.Visible = showTimeFormat;
|
||||
timeFormatHintLabel.Visible = showTimeFormat;
|
||||
maskHintLabel.Visible = showMask;
|
||||
maskGrid.Visible = showMask;
|
||||
|
||||
if (showTimeFormat && timeFormatComboBox.SelectedIndex < 0)
|
||||
timeFormatComboBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private TreeNode CreateTreeNode(PayloadTemplateNode payloadNode)
|
||||
{
|
||||
TreeNode treeNode = new TreeNode(payloadNode.DisplayText ?? payloadNode.Name ?? string.Empty)
|
||||
{
|
||||
Tag = payloadNode
|
||||
};
|
||||
|
||||
if (payloadNode.IsSelectable && payloadNode.DestinationKind == PayloadDestinationKind.RepeatingPrototype)
|
||||
{
|
||||
treeNode.ForeColor = SystemColors.GrayText;
|
||||
treeNode.ToolTipText = "Repeating XML values cannot be used in a single filename.";
|
||||
}
|
||||
|
||||
foreach (PayloadTemplateNode child in payloadNode.Children)
|
||||
treeNode.Nodes.Add(CreateTreeNode(child));
|
||||
|
||||
return treeNode;
|
||||
}
|
||||
|
||||
private void xmlTreeView_AfterSelect(object sender, TreeViewEventArgs e)
|
||||
{
|
||||
PayloadTemplateNode payloadNode = e.Node != null ? e.Node.Tag as PayloadTemplateNode : null;
|
||||
bool selectable = payloadNode != null &&
|
||||
payloadNode.IsSelectable &&
|
||||
payloadNode.DestinationKind == PayloadDestinationKind.SingleValue;
|
||||
|
||||
selectedPathTextBox.Text = selectable ? payloadNode.DestinationPath : string.Empty;
|
||||
okButton.Enabled = selectable;
|
||||
}
|
||||
|
||||
private void xmlTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
|
||||
{
|
||||
if (e.Node != null)
|
||||
xmlTreeView.SelectedNode = e.Node;
|
||||
}
|
||||
|
||||
private void okButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
AcceptSelection();
|
||||
}
|
||||
|
||||
private void AcceptSelection()
|
||||
{
|
||||
maskGrid.EndEdit();
|
||||
|
||||
PayloadTemplateNode payloadNode = xmlTreeView.SelectedNode != null
|
||||
? xmlTreeView.SelectedNode.Tag as PayloadTemplateNode
|
||||
: null;
|
||||
|
||||
if (payloadNode == null ||
|
||||
!payloadNode.IsSelectable ||
|
||||
payloadNode.DestinationKind != PayloadDestinationKind.SingleValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateSelectedTransformation())
|
||||
return;
|
||||
|
||||
SelectedDestinationPath = payloadNode.DestinationPath;
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
|
||||
private bool ValidateSelectedTransformation()
|
||||
{
|
||||
string selectedTransformation = transformationComboBox.SelectedItem as string;
|
||||
|
||||
if (!string.Equals(selectedTransformation, MaskTransformation, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
int validRowCount = 0;
|
||||
|
||||
foreach (DataGridViewRow row in maskGrid.Rows)
|
||||
{
|
||||
if (row.IsNewRow)
|
||||
continue;
|
||||
|
||||
string sourceValue = Convert.ToString(row.Cells[0].Value).Trim();
|
||||
string targetValue = Convert.ToString(row.Cells[1].Value).Trim();
|
||||
|
||||
if (sourceValue.Length == 0 && targetValue.Length == 0)
|
||||
continue;
|
||||
|
||||
if (sourceValue.Length == 0)
|
||||
{
|
||||
ShowMaskError("Enter the XML value to be replaced.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceValue.IndexOfAny(new[] { '=', ';' }) >= 0 || targetValue.IndexOf(';') >= 0)
|
||||
{
|
||||
ShowMaskError("Mask values cannot contain semicolons. The XML value also cannot contain '='.");
|
||||
return false;
|
||||
}
|
||||
|
||||
validRowCount++;
|
||||
}
|
||||
|
||||
if (validRowCount == 0)
|
||||
{
|
||||
ShowMaskError("Add at least one XML value and filename value pair.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ShowMaskError(string message)
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
message,
|
||||
"XML filename mask",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
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 child = FindTreeNode(node.Nodes, destinationPath);
|
||||
if (child != null)
|
||||
return child;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ExpandParents(TreeNode node)
|
||||
{
|
||||
TreeNode parent = node.Parent;
|
||||
while (parent != null)
|
||||
{
|
||||
parent.Expand();
|
||||
parent = parent.Parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using TBF.Rig.Generic;
|
||||
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters;
|
||||
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
|
||||
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers;
|
||||
using static TBF.Rig.Output.DataStorage.UniDataStorageWriter.Types;
|
||||
@@ -121,6 +122,7 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
|
||||
browsePayloadTemplateButton.Click += browsePayloadTemplateButton_Click;
|
||||
browsePayloadArchiveButton.Click += browsePayloadArchiveButton_Click;
|
||||
archivePayloadCheckBox.CheckedChanged += archivePayloadCheckBox_CheckedChanged;
|
||||
payloadFileNameTemplateTextBox.MouseClick += payloadFileNameTemplateTextBox_MouseClick;
|
||||
|
||||
//
|
||||
// Diagnostic XML payloads can be multiline. Keep the compact
|
||||
@@ -1858,6 +1860,202 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the XML destination chooser when the user clicks inside any
|
||||
/// filename token enclosed in braces.
|
||||
/// </summary>
|
||||
private void payloadFileNameTemplateTextBox_MouseClick(object sender, MouseEventArgs e)
|
||||
{
|
||||
string template = payloadFileNameTemplateTextBox.Text ?? string.Empty;
|
||||
int tokenStart;
|
||||
int tokenEnd;
|
||||
string tokenContent;
|
||||
|
||||
if (!TryGetFileNameTokenAtCaret(
|
||||
template,
|
||||
payloadFileNameTemplateTextBox.SelectionStart,
|
||||
out tokenStart,
|
||||
out tokenEnd,
|
||||
out tokenContent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isResultToken = string.Equals(tokenContent, "Result", StringComparison.OrdinalIgnoreCase);
|
||||
bool isXmlToken = tokenContent.StartsWith("Xml:", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string referencePath = (payloadTemplatePathTextBox.Text ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(referencePath))
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"Select the XML payload reference before configuring an XML filename token.",
|
||||
"XML filename value",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
string currentPath = string.Empty;
|
||||
string currentTransformation = isResultToken
|
||||
? "mask:Passed=P;Failed=F"
|
||||
: string.Empty;
|
||||
|
||||
if (isXmlToken)
|
||||
ParseXmlFileNameToken(tokenContent, out currentPath, out currentTransformation);
|
||||
|
||||
using (PayloadFileNameXmlTokenDlg dialog =
|
||||
new PayloadFileNameXmlTokenDlg(referencePath, currentPath, currentTransformation))
|
||||
{
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
|
||||
string transformationError;
|
||||
if (!TryValidateFileNameTransformation(dialog.Transformation, out transformationError))
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
transformationError,
|
||||
"XML filename value",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
string replacementToken = "{Xml:" + dialog.SelectedDestinationPath;
|
||||
if (!string.IsNullOrWhiteSpace(dialog.Transformation))
|
||||
replacementToken += "|" + dialog.Transformation;
|
||||
|
||||
replacementToken += "}";
|
||||
|
||||
payloadFileNameTemplateTextBox.Text =
|
||||
template.Remove(tokenStart, tokenEnd - tokenStart + 1).Insert(tokenStart, replacementToken);
|
||||
payloadFileNameTemplateTextBox.SelectionStart = tokenStart + replacementToken.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetFileNameTokenAtCaret(
|
||||
string text,
|
||||
int caretPosition,
|
||||
out int tokenStart,
|
||||
out int tokenEnd,
|
||||
out string tokenContent)
|
||||
{
|
||||
tokenStart = -1;
|
||||
tokenEnd = -1;
|
||||
tokenContent = string.Empty;
|
||||
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return false;
|
||||
|
||||
int searchPosition = Math.Min(Math.Max(caretPosition, 0), text.Length - 1);
|
||||
tokenStart = text.LastIndexOf('{', searchPosition);
|
||||
tokenEnd = text.IndexOf('}', searchPosition);
|
||||
|
||||
if (tokenStart < 0 || tokenEnd <= tokenStart || caretPosition <= tokenStart || caretPosition > tokenEnd)
|
||||
return false;
|
||||
|
||||
int nestedClosingBrace = text.LastIndexOf('}', Math.Min(caretPosition, text.Length - 1));
|
||||
int nestedOpeningBrace = text.IndexOf('{', tokenStart + 1, tokenEnd - tokenStart - 1);
|
||||
if (nestedClosingBrace > tokenStart || nestedOpeningBrace >= 0)
|
||||
return false;
|
||||
|
||||
tokenContent = text.Substring(tokenStart + 1, tokenEnd - tokenStart - 1).Trim();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ParseXmlFileNameToken(string tokenContent, out string destinationPath, out string transformation)
|
||||
{
|
||||
string definition = tokenContent.Substring("Xml:".Length);
|
||||
int transformationSeparator = definition.IndexOf('|');
|
||||
|
||||
if (transformationSeparator >= 0)
|
||||
{
|
||||
destinationPath = definition.Substring(0, transformationSeparator).Trim();
|
||||
transformation = definition.Substring(transformationSeparator + 1).Trim();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the earlier path:DateTimeFormat syntax and let the dialog
|
||||
// save it back as |timeformat:DateTimeFormat.
|
||||
int finalPathSeparator = definition.LastIndexOf('/');
|
||||
int formatSeparator = definition.IndexOf(':', finalPathSeparator + 1);
|
||||
|
||||
if (formatSeparator >= 0)
|
||||
{
|
||||
destinationPath = definition.Substring(0, formatSeparator).Trim();
|
||||
transformation = "timeformat:" + definition.Substring(formatSeparator + 1).Trim();
|
||||
return;
|
||||
}
|
||||
|
||||
destinationPath = definition.Trim();
|
||||
transformation = string.Empty;
|
||||
}
|
||||
|
||||
private bool TryValidateFileNameTransformation(string transformation, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(transformation))
|
||||
return true;
|
||||
|
||||
const string timeFormatPrefix = "timeformat:";
|
||||
if (transformation.StartsWith(timeFormatPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string format = transformation.Substring(timeFormatPrefix.Length).Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
{
|
||||
error = "Time format is empty. For example: timeformat:yyyyMMdd_HHmmss";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (format.IndexOfAny(new[] { '/', '\\', ':' }) >= 0)
|
||||
{
|
||||
error = "Time format contains a character that is not allowed in a filename: /, \\ or :.";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DateTime.Now.ToString(format, CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
error = "Invalid DateTime format. For example: yyyyMMdd_HHmmss";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const string maskPrefix = "mask:";
|
||||
if (!transformation.StartsWith(maskPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = "Unsupported transformation. Select None, Time format or Mask.";
|
||||
return false;
|
||||
}
|
||||
|
||||
string mapping = transformation.Substring(maskPrefix.Length).Trim();
|
||||
if (string.IsNullOrWhiteSpace(mapping))
|
||||
{
|
||||
error = "Mask is empty. Add at least one XML value and filename value pair.";
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] entries = mapping.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (string entry in entries)
|
||||
{
|
||||
int separator = entry.IndexOf('=');
|
||||
if (separator <= 0)
|
||||
{
|
||||
error = "Invalid mask. Use Source=Target pairs, for example mask:Passed=P;Failed=F.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a folder browser used to select the payload archive directory.
|
||||
/// </summary>
|
||||
|
||||
@@ -1247,6 +1247,12 @@
|
||||
<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\PayloadFileNameXmlTokenDlg.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\UI\PayloadFileNameXmlTokenDlg.Designer.cs">
|
||||
<DependentUpon>PayloadFileNameXmlTokenDlg.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\UI\XmlPayloadViewerDlg.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
|
||||
Reference in New Issue
Block a user