<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
|
// Build Number
|
||||||
// Revision
|
// Revision
|
||||||
//
|
//
|
||||||
[assembly: AssemblyVersion("3.9.3145.102")]
|
[assembly: AssemblyVersion("3.9.3145.103")]
|
||||||
[assembly: AssemblyFileVersion("3.9.3145.102")]
|
[assembly: AssemblyFileVersion("3.9.3145.103")]
|
||||||
|
|||||||
@@ -995,89 +995,21 @@ namespace TBF.Rig.Output.DB.ResultsWriter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The file name is metadata and is not derived by parsing a
|
/// This keeps the legacy {Result} token independent of test-dependent
|
||||||
/// customer-specific XML path. Existing configurations normally expose
|
/// mappings such as Test passed(). New configurations may instead use an
|
||||||
/// the standard result as "Test passed()". Unknown or unavailable values
|
/// {Xml:...} token to read and optionally format a value directly from the
|
||||||
/// are represented by U so that the payload is never mislabeled as a
|
/// generated payload.
|
||||||
/// pass or failure.
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private string ResolveMeterResultCode(
|
private string ResolveMeterResultCode(
|
||||||
WaterMeter wm)
|
WaterMeter wm)
|
||||||
{
|
{
|
||||||
if (wm == null ||
|
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)
|
|
||||||
return "U";
|
return "U";
|
||||||
|
|
||||||
string resultValue =
|
return wm.PassedFromAllTests() ? "P" : "F";
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+291
-4
@@ -7,6 +7,7 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Xml.Linq;
|
||||||
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
|
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
|
||||||
|
|
||||||
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
||||||
@@ -100,7 +101,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
|||||||
string outputFileName =
|
string outputFileName =
|
||||||
CreatePayloadFileName(
|
CreatePayloadFileName(
|
||||||
fileNameContext,
|
fileNameContext,
|
||||||
false);
|
false,
|
||||||
|
payload);
|
||||||
|
|
||||||
if (cfg.ArchivePayload)
|
if (cfg.ArchivePayload)
|
||||||
{
|
{
|
||||||
@@ -188,7 +190,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
|||||||
string outputFileName =
|
string outputFileName =
|
||||||
CreatePayloadFileName(
|
CreatePayloadFileName(
|
||||||
fileNameContext,
|
fileNameContext,
|
||||||
false);
|
false,
|
||||||
|
payload);
|
||||||
|
|
||||||
DataWriteRequest request =
|
DataWriteRequest request =
|
||||||
new DataWriteRequest
|
new DataWriteRequest
|
||||||
@@ -257,7 +260,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
|||||||
string outputFileName =
|
string outputFileName =
|
||||||
CreatePayloadFileName(
|
CreatePayloadFileName(
|
||||||
fileNameContext,
|
fileNameContext,
|
||||||
true);
|
true,
|
||||||
|
payload);
|
||||||
|
|
||||||
string archiveFilePath =
|
string archiveFilePath =
|
||||||
ArchivePayload(
|
ArchivePayload(
|
||||||
@@ -456,7 +460,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private string CreatePayloadFileName(
|
private string CreatePayloadFileName(
|
||||||
PayloadFileNameContext context,
|
PayloadFileNameContext context,
|
||||||
bool preview)
|
bool preview,
|
||||||
|
string payload)
|
||||||
{
|
{
|
||||||
context =
|
context =
|
||||||
context ??
|
context ??
|
||||||
@@ -503,6 +508,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
|||||||
"PayloadName",
|
"PayloadName",
|
||||||
GetPayloadBaseName());
|
GetPayloadBaseName());
|
||||||
|
|
||||||
|
fileName = ReplaceXmlFileNameTokens(fileName, payload);
|
||||||
|
|
||||||
if (fileName.IndexOf('{') >= 0 ||
|
if (fileName.IndexOf('{') >= 0 ||
|
||||||
fileName.IndexOf('}') >= 0)
|
fileName.IndexOf('}') >= 0)
|
||||||
{
|
{
|
||||||
@@ -545,6 +552,286 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters
|
|||||||
return fileName;
|
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>
|
/// <summary>
|
||||||
/// Replaces the timestamp token and supports an optional .NET format string.
|
/// Replaces the timestamp token and supports an optional .NET format string.
|
||||||
/// </summary>
|
/// </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.Text;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using TBF.Rig.Generic;
|
using TBF.Rig.Generic;
|
||||||
|
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters;
|
||||||
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
|
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
|
||||||
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers;
|
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers;
|
||||||
using static TBF.Rig.Output.DataStorage.UniDataStorageWriter.Types;
|
using static TBF.Rig.Output.DataStorage.UniDataStorageWriter.Types;
|
||||||
@@ -121,6 +122,7 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
|
|||||||
browsePayloadTemplateButton.Click += browsePayloadTemplateButton_Click;
|
browsePayloadTemplateButton.Click += browsePayloadTemplateButton_Click;
|
||||||
browsePayloadArchiveButton.Click += browsePayloadArchiveButton_Click;
|
browsePayloadArchiveButton.Click += browsePayloadArchiveButton_Click;
|
||||||
archivePayloadCheckBox.CheckedChanged += archivePayloadCheckBox_CheckedChanged;
|
archivePayloadCheckBox.CheckedChanged += archivePayloadCheckBox_CheckedChanged;
|
||||||
|
payloadFileNameTemplateTextBox.MouseClick += payloadFileNameTemplateTextBox_MouseClick;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Diagnostic XML payloads can be multiline. Keep the compact
|
// 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>
|
/// <summary>
|
||||||
/// Opens a folder browser used to select the payload archive directory.
|
/// Opens a folder browser used to select the payload archive directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1247,6 +1247,12 @@
|
|||||||
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Interfaces\DataWriteRequest.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\Interfaces\IDataStorageWriter.cs" />
|
||||||
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\Types.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">
|
<Compile Include="Rig\Output\DataStorage\UniDataStorageWriter\UI\XmlPayloadViewerDlg.cs">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
|||||||
Reference in New Issue
Block a user