diff --git a/TBF/Properties/AssemblyInfo.cs b/TBF/Properties/AssemblyInfo.cs index 07c0ea1cf..c357216c3 100644 --- a/TBF/Properties/AssemblyInfo.cs +++ b/TBF/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // Build Number // Revision // -[assembly: AssemblyVersion("3.9.3145.100")] -[assembly: AssemblyFileVersion("3.9.3145.100")] +[assembly: AssemblyVersion("3.9.3145.101")] +[assembly: AssemblyFileVersion("3.9.3145.101")] diff --git a/TBF/Rig/Output/DB/ResultsWriter/ResultsWriter.cs b/TBF/Rig/Output/DB/ResultsWriter/ResultsWriter.cs index dc6452826..47ac047a8 100644 --- a/TBF/Rig/Output/DB/ResultsWriter/ResultsWriter.cs +++ b/TBF/Rig/Output/DB/ResultsWriter/ResultsWriter.cs @@ -242,6 +242,13 @@ namespace TBF.Rig.Output.DB.ResultsWriter /// /// Writes all enabled water meters in a batch. /// + /// + /// XML payload targets are generated once per batch. Every enabled water + /// meter contributes one logical repeat record to the same XML document. + /// + /// Legacy Insert output keeps its existing behavior: every published + /// regular meter test is written with its concrete test ID context. + /// public void WriteBatchResults( Batch batch) { @@ -256,11 +263,25 @@ namespace TBF.Rig.Output.DB.ResultsWriter WriterCfg storageCfg = GetDataStorageWriterConfiguration(); - XmlPayloadRequestBuilder payloadBuilder = - storageCfg.UsesXmlPayload() - ? new XmlPayloadRequestBuilder( - storageCfg) - : null; + if (storageCfg.UsesXmlPayload()) + { + WriteXmlBatchResults( + batch, + storageCfg); + + return; + } + + if (storageCfg.WriteMode != + WriteMode.Insert) + { + throw new NotSupportedException( + string.Format( + "ResultsWriter does not support UniDataStorageWriter target '{0}' / '{1}' / '{2}'.", + storageCfg.DataStorageType, + storageCfg.TechnologyType, + storageCfg.WriteMode)); + } foreach (WaterMeter wm in batch.WaterMeters) @@ -271,104 +292,141 @@ namespace TBF.Rig.Output.DB.ResultsWriter continue; } - DataWriteRequest targetRequest; - - if (storageCfg.IsStoredProcedurePayloadTarget()) - { - PayloadGenerationRequest generationRequest = - BuildPayloadGenerationRequest( - wm); - - XmlPayloadBuildResult buildResult = - payloadBuilder.Build( - generationRequest, - Convert.ToString( - wm.SerialNr)); - - targetRequest = - buildResult.Request; - - if (!string.IsNullOrWhiteSpace( - buildResult.ArchiveFilePath)) - { - log.InfoFormat( - "XML payload archived to '{0}'.", - buildResult.ArchiveFilePath); - } - } - else if (storageCfg.IsXmlFileTarget()) - { - PayloadGenerationRequest generationRequest = - BuildPayloadGenerationRequest( - wm); - - XmlPayloadBuildResult buildResult = - payloadBuilder.BuildFile( - generationRequest, - Convert.ToString( - wm.SerialNr)); - - targetRequest = - buildResult.Request; - - log.InfoFormat( - "XML file payload generated for WM position {0}: {1}", - wm.WMPosition, - buildResult.OutputFileName); - } - else if (storageCfg.WriteMode == - WriteMode.Insert) - { - targetRequest = - BuildInsertRequest( - wm); - - if (targetRequest.InsertItems.Count == 0) - { - log.WarnFormat( - "No values to write for WM position {0}", - wm.WMPosition); - - continue; - } - } - else - { - throw new NotSupportedException( - string.Format( - "ResultsWriter does not support UniDataStorageWriter target '{0}' / '{1}' / '{2}'.", - storageCfg.DataStorageType, - storageCfg.TechnologyType, - storageCfg.WriteMode)); - } - - WriterDiagnosticResult result = - dataStorageWriter.SetData( - targetRequest); - - if (!result.Success) - { - throw new Exception( - result.Message); - } - - log.WarnFormat( - "ResultsWriter wrote WM position {0}. Target={1}, Technology={2}, Mode={3}.", - wm.WMPosition, - storageCfg.DataStorageType, - storageCfg.TechnologyType, - storageCfg.WriteMode); + // + // Insert mode needs the same test context as the legacy TBF + // result writers. One row is generated for every published + // regular meter test and all selected items are evaluated + // with the actual mtr.Name() as test ID. + // + WriteInsertResults( + wm, + storageCfg); } } + /// + /// Generates and writes one XML payload for the complete batch. + /// + private void WriteXmlBatchResults( + Batch batch, + WriterCfg storageCfg) + { + IList enabledMeters = + batch.WaterMeters + .Where( + wm => + wm != null && + !wm.Disabled) + .ToList(); + + if (enabledMeters.Count == 0) + return; + + XmlPayloadRequestBuilder payloadBuilder = + new XmlPayloadRequestBuilder( + storageCfg); + + PayloadGenerationRequest generationRequest = + BuildBatchPayloadGenerationRequest( + enabledMeters); + + string payloadIdentifier = + BuildBatchPayloadIdentifier( + enabledMeters); + + XmlPayloadBuildResult buildResult; + + if (storageCfg.IsStoredProcedurePayloadTarget()) + { + buildResult = + payloadBuilder.Build( + generationRequest, + payloadIdentifier); + } + else if (storageCfg.IsXmlFileTarget()) + { + buildResult = + payloadBuilder.BuildFile( + generationRequest, + payloadIdentifier); + } + else + { + throw new NotSupportedException( + string.Format( + "Unsupported XML payload target '{0}' / '{1}' / '{2}'.", + storageCfg.DataStorageType, + storageCfg.TechnologyType, + storageCfg.WriteMode)); + } + + if (!string.IsNullOrWhiteSpace( + buildResult.ArchiveFilePath)) + { + log.InfoFormat( + "XML payload archived to '{0}'.", + buildResult.ArchiveFilePath); + } + + if (!string.IsNullOrWhiteSpace( + buildResult.OutputFileName)) + { + log.InfoFormat( + "XML batch payload generated: {0}", + buildResult.OutputFileName); + } + + WriterDiagnosticResult result = + dataStorageWriter.SetData( + buildResult.Request); + + if (!result.Success) + { + throw new Exception( + result.Message); + } + + log.WarnFormat( + "ResultsWriter wrote one XML payload for {0} enabled water meter(s). Target={1}, Technology={2}, Mode={3}.", + enabledMeters.Count, + storageCfg.DataStorageType, + storageCfg.TechnologyType, + storageCfg.WriteMode); + } + + /// + /// Creates a stable identifier used by the XML request builder for one batch. + /// + private string BuildBatchPayloadIdentifier( + IList enabledMeters) + { + if (enabledMeters == null || + enabledMeters.Count == 0) + { + return "BATCH"; + } + + string firstSerial = + Convert.ToString( + enabledMeters[0].SerialNr); + + if (string.IsNullOrWhiteSpace( + firstSerial)) + { + return "BATCH"; + } + + return firstSerial; + } + /// /// Generates a dry-run XML payload without calling the database. /// /// - /// Batch used to evaluate one-time TBF mappings. + /// Batch used to evaluate one-time TBF mappings and provide preview context. /// /// - /// Number of repeated GROUP/TEST instances generated for the preview. + /// Number of logical repeated result records generated for the preview. /// public XmlPayloadBuildResult GeneratePreviewPayload( Batch simulationBatch, @@ -427,10 +485,70 @@ namespace TBF.Rig.Output.DB.ResultsWriter } /// - /// Creates the legacy insert request. + /// Writes Insert rows for one water meter using the actual TBF test context. + /// + /// + /// One row is written for every published regular meter test. + /// The actual test ID is passed to WMeterRsltItemSpec.Print(). + /// + private void WriteInsertResults( + WaterMeter wm, + WriterCfg storageCfg) + { + IList publishedTests = + wm.RegularMeterTestRslts() + .Where( + mtr => + mtr != null && + mtr.Publish() == + Publish.Always) + .ToList(); + + // + // Preserve meter/batch data even if there is no published test. + // + if (publishedTests.Count == 0) + { + DataWriteRequest fallbackRequest = + BuildInsertRequest( + wm, + string.Empty); + + WriteInsertRequest( + wm, + string.Empty, + fallbackRequest, + storageCfg); + + return; + } + + foreach (MeterTestRslt mtr + in publishedTests) + { + string testId = + mtr.Name() ?? + string.Empty; + + DataWriteRequest request = + BuildInsertRequest( + wm, + testId); + + WriteInsertRequest( + wm, + testId, + request, + storageCfg); + } + } + + /// + /// Builds one Insert request for one water meter and one concrete test. /// private DataWriteRequest BuildInsertRequest( - WaterMeter wm) + WaterMeter wm, + string testId) { DataWriteRequest request = new DataWriteRequest @@ -441,7 +559,7 @@ namespace TBF.Rig.Output.DB.ResultsWriter if (resultsWriterCfg.SelectedItems == null) return request; - foreach (var item + foreach (Results.WMeterRsltItemSpec item in resultsWriterCfg.SelectedItems) { if (item == null || @@ -458,7 +576,9 @@ namespace TBF.Rig.Output.DB.ResultsWriter item.Caption, Value = - item.Print(wm) + item.Print( + wm, + testId) }); } @@ -466,74 +586,290 @@ namespace TBF.Rig.Output.DB.ResultsWriter } /// - /// Creates the runtime XML mapping request for one meter. + /// Executes one prepared Insert request. /// - private PayloadGenerationRequest BuildPayloadGenerationRequest( - WaterMeter wm) + private void WriteInsertRequest( + WaterMeter wm, + string testId, + DataWriteRequest request, + WriterCfg storageCfg) + { + if (request == null || + request.InsertItems.Count == 0) + { + log.WarnFormat( + "No values to write for WM position {0}", + wm.WMPosition); + + return; + } + + WriterDiagnosticResult result = + dataStorageWriter.SetData( + request); + + if (!result.Success) + { + throw new Exception( + result.Message); + } + + log.WarnFormat( + "ResultsWriter wrote WM position {0}, TestID='{1}'. Target={2}, Technology={3}, Mode={4}.", + wm.WMPosition, + testId, + storageCfg.DataStorageType, + storageCfg.TechnologyType, + storageCfg.WriteMode); + } + + /// + /// Creates one runtime XML mapping request for the complete batch. + /// + /// + /// Enabled water meters that must be represented in the generated payload. + /// + /// + /// + /// Every enabled water meter creates one logical repeat record. All + /// destinations located inside the repeating XML prototype are populated + /// into that same record. + /// + /// + /// + /// Result evaluation first uses the meter-level context. If that does not + /// produce a value, the same result item is evaluated against the actual + /// published measurement-test IDs in procedure order. This preserves + /// meter-level values while also allowing test-result variables to be + /// populated in the XML record. + /// + /// + private PayloadGenerationRequest BuildBatchPayloadGenerationRequest( + IList enabledMeters) { PayloadGenerationRequest request = new PayloadGenerationRequest(); - if (resultsWriterCfg.SelectedItems == null) - return request; - - int sourceIndex = 0; - - foreach (var item - in resultsWriterCfg.SelectedItems) + if (enabledMeters == null || + enabledMeters.Count == 0 || + resultsWriterCfg.SelectedItems == null) { - if (item == null || - string.IsNullOrWhiteSpace( - item.Caption)) - { - continue; - } + return request; + } + + List singleItems = + resultsWriterCfg.SelectedItems + .Where( + item => + item != null && + !string.IsNullOrWhiteSpace( + item.Caption) && + !IsRepeatDestination( + item.Caption)) + .ToList(); + + List repeatItems = + resultsWriterCfg.SelectedItems + .Where( + item => + item != null && + !string.IsNullOrWhiteSpace( + item.Caption) && + IsRepeatDestination( + item.Caption)) + .ToList(); + + // + // One-time XML destinations exist only once in the document. + // Evaluate them from the first enabled water meter. This preserves + // the existing one-time mapping model while repeat destinations are + // created once for every enabled water meter. + // + WaterMeter firstMeter = + enabledMeters[0]; + + for (int singleIndex = 0; + singleIndex < singleItems.Count; + singleIndex++) + { + Results.WMeterRsltItemSpec item = + singleItems[ + singleIndex]; string sourceKey = - "Result." + - sourceIndex.ToString( - CultureInfo.InvariantCulture); + string.Format( + CultureInfo.InvariantCulture, + "Batch.Single.{0}", + singleIndex); - sourceIndex++; + request.SingleMappings.Add( + new PayloadMapping + { + SourceKey = + sourceKey, - if (IsRepeatDestination( - item.Caption)) + DestinationPath = + item.Caption + }); + + request.SingleValues.SetValue( + sourceKey, + EvaluateXmlMappedValue( + firstMeter, + item)); + } + + // + // Every enabled water meter becomes one logical repeat record. + // + for (int meterIndex = 0; + meterIndex < enabledMeters.Count; + meterIndex++) + { + WaterMeter wm = + enabledMeters[ + meterIndex]; + + PayloadRepeatRecord record = + new PayloadRepeatRecord(); + + for (int repeatIndex = 0; + repeatIndex < repeatItems.Count; + repeatIndex++) { - PayloadRepeatRecord record = - new PayloadRepeatRecord(); + Results.WMeterRsltItemSpec item = + repeatItems[ + repeatIndex]; + + string sourceKey = + string.Format( + CultureInfo.InvariantCulture, + "Batch.Repeat.{0}.{1}", + meterIndex, + repeatIndex); AddRecordMapping( record, sourceKey, item.Caption, - item.Print(wm)); + EvaluateXmlMappedValue( + wm, + item)); + } + if (record.Mappings.Count > 0) + { request.RepeatRecords.Add( record); } - else - { - request.SingleMappings.Add( - new PayloadMapping - { - SourceKey = - sourceKey, - - DestinationPath = - item.Caption - }); - - request.SingleValues.SetValue( - sourceKey, - item.Print(wm)); - } } return request; } /// - /// Creates a preview request with an exact number of simulated tests. + /// Evaluates one configured result item for XML output. + /// + /// + /// Meter/batch variables normally return a value directly from + /// Print(wm). Test-result variables require a concrete test ID. For those + /// variables the method evaluates the published regular meter tests in + /// measurement-procedure order and uses the first non-empty value. + /// + /// This fallback is intentionally applied only when the meter-level + /// evaluation is empty. + /// + private string EvaluateXmlMappedValue( + WaterMeter wm, + Results.WMeterRsltItemSpec item) + { + if (wm == null || + item == null) + { + return string.Empty; + } + + string meterValue = + item.Print( + wm); + + if (!string.IsNullOrWhiteSpace( + meterValue)) + { + return meterValue; + } + + IList publishedTests = + wm.RegularMeterTestRslts() + .Where( + mtr => + mtr != null && + mtr.Publish() == + Publish.Always) + .ToList(); + + string firstResolvedValue = + string.Empty; + + string firstResolvedTestId = + string.Empty; + + foreach (MeterTestRslt mtr + in publishedTests) + { + string testId = + mtr.Name() ?? + string.Empty; + + string testValue = + item.Print( + wm, + testId); + + if (string.IsNullOrWhiteSpace( + testValue)) + { + continue; + } + + if (string.IsNullOrWhiteSpace( + firstResolvedValue)) + { + firstResolvedValue = + testValue; + + firstResolvedTestId = + testId; + + continue; + } + + // + // A single XML destination can hold only one scalar value. + // If more than one test produces a different value for the same + // mapped result, preserve deterministic procedure order and keep + // the first value, but make the ambiguity visible in the log. + // + if (!string.Equals( + firstResolvedValue, + testValue, + StringComparison.Ordinal)) + { + log.WarnFormat( + "XML mapping '{0}' produced values in more than one test context for WM position {1}. Using first test '{2}'.", + item.Caption, + wm.WMPosition, + firstResolvedTestId); + + break; + } + } + + return firstResolvedValue; + } + + /// + /// Creates a preview request with an exact number of simulated repeat records. /// private PayloadGenerationRequest BuildPreviewGenerationRequest( WaterMeter wm, @@ -563,10 +899,9 @@ namespace TBF.Rig.Output.DB.ResultsWriter item.Caption)) { // - // Repeated XML attributes have no predefined semantics. - // Keep the selected TBF result exactly as configured: - // its evaluated value is written only to its selected - // repeating destination. + // A repeating mapping defines a destination inside the + // repeat prototype. The mapping itself must not create a + // separate repeat record. // repeatItems.Add( item); @@ -584,11 +919,9 @@ namespace TBF.Rig.Output.DB.ResultsWriter wm); // - // A minimal simulation batch does not contain every - // possible TBF result source. Never leave a configured - // preview destination empty only because its runtime - // simulation source is unavailable. Use a valid, - // type-appropriate and clearly synthetic value instead. + // A minimal simulation batch does not contain every possible + // TBF result source. Use a clearly synthetic type-appropriate + // value when the evaluated preview value is unavailable. // if (string.IsNullOrWhiteSpace( previewValue)) @@ -617,25 +950,38 @@ namespace TBF.Rig.Output.DB.ResultsWriter } } - if (repeatItems.Count > 0) + // + // Create the requested number of logical repeat records. Every record + // receives all configured repeating mappings, so one simulated record + // is structurally equivalent to one generated runtime record. + // + for (int recordIndex = 0; + recordIndex < simulatedTestCount && + repeatItems.Count > 0; + recordIndex++) { - for (int testIndex = 0; - testIndex < simulatedTestCount; - testIndex++) + PayloadRepeatRecord record = + new PayloadRepeatRecord(); + + for (int repeatIndex = 0; + repeatIndex < repeatItems.Count; + repeatIndex++) { Results.WMeterRsltItemSpec item = repeatItems[ - testIndex % - repeatItems.Count]; - - PayloadRepeatRecord record = - new PayloadRepeatRecord(); + repeatIndex]; string sourceKey = string.Format( CultureInfo.InvariantCulture, - "Preview.Repeat.{0}", - testIndex); + "Preview.Repeat.{0}.{1}", + recordIndex, + repeatIndex); + + int simulationValueIndex = + recordIndex * + repeatItems.Count + + repeatIndex; AddRecordMapping( record, @@ -643,11 +989,11 @@ namespace TBF.Rig.Output.DB.ResultsWriter item.Caption, CreateUniversalSimulationValue( item, - testIndex)); - - request.RepeatRecords.Add( - record); + simulationValueIndex)); } + + request.RepeatRecords.Add( + record); } return request;