Skip to content

Commit

Permalink
Merge branch 'main' into xedkn/edi-start-request-process
Browse files Browse the repository at this point in the history
  • Loading branch information
ebbeknudsen authored Nov 19, 2024
2 parents d79f007 + 9cfa449 commit ba0c32c
Show file tree
Hide file tree
Showing 29 changed files with 1,281 additions and 143 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,14 @@ public static IServiceCollection AddIncomingMessagesModule(this IServiceCollecti
.AddSingleton<JsonSchemaProvider>();

services
.AddTransient<Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.MeteredDateForMeasurementPointParsers.EbixMessageParser>();
.AddTransient<Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.MeteredDateForMeasurementPointParsers.MeteredDateForMeasurementPointEbixMessageParser>();
services
.AddTransient<Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.MeteredDateForMeasurementPointParsers.MeteredDateForMeasurementPointXmlMessageParser>();

services.AddTransient<IDictionary<(IncomingDocumentType, DocumentFormat), IMessageParser>>(provider => new Dictionary<(IncomingDocumentType, DocumentFormat), IMessageParser>
{
{ (IncomingDocumentType.MeteredDataForMeasurementPoint, DocumentFormat.Ebix), provider.GetRequiredService<Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.MeteredDateForMeasurementPointParsers.EbixMessageParser>() },
{ (IncomingDocumentType.MeteredDataForMeasurementPoint, DocumentFormat.Ebix), provider.GetRequiredService<Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.MeteredDateForMeasurementPointParsers.MeteredDateForMeasurementPointEbixMessageParser>() },
{ (IncomingDocumentType.MeteredDataForMeasurementPoint, DocumentFormat.Xml), provider.GetRequiredService<Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.MeteredDateForMeasurementPointParsers.MeteredDateForMeasurementPointXmlMessageParser>() },
});

return services;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ await _delegateIncomingMessage
.ConfigureAwait(false);

var validationResult = await _validateIncomingMessage
.ValidateAsync(incomingMarketMessageParserResult.IncomingMessage, cancellationToken)
.ValidateAsync(incomingMarketMessageParserResult.IncomingMessage, incomingDocumentFormat, cancellationToken)
.ConfigureAwait(false);

if (!validationResult.Success)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models;
using Energinet.DataHub.EDI.IncomingMessages.Domain.Abstractions;
using Energinet.DataHub.EDI.IncomingMessages.Domain.Validation;
using Energinet.DataHub.EDI.IncomingMessages.Domain.Validation.ValidationErrors;
Expand Down Expand Up @@ -53,6 +54,7 @@ public ValidateIncomingMessage(

public async Task<Result> ValidateAsync(
IIncomingMessage incomingMessage,
DocumentFormat documentFormat,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(incomingMessage);
Expand All @@ -62,7 +64,7 @@ public async Task<Result> ValidateAsync(
VerifyReceiverAsync(incomingMessage),
CheckMessageIdAsync(incomingMessage, cancellationToken),
CheckMessageTypeAsync(incomingMessage, cancellationToken),
CheckBusinessReasonAsync(incomingMessage, cancellationToken),
CheckBusinessReasonAsync(incomingMessage, documentFormat, cancellationToken),
CheckBusinessTypeAsync(incomingMessage, cancellationToken))
.ConfigureAwait(false))
.SelectMany(errs => errs);
Expand Down Expand Up @@ -135,9 +137,10 @@ private async Task<IReadOnlyCollection<ValidationError>> CheckMessageTypeAsync(

private async Task<IReadOnlyCollection<ValidationError>> CheckBusinessReasonAsync(
IIncomingMessage message,
DocumentFormat documentFormat,
CancellationToken cancellationToken)
{
var result = await _processTypeValidator.ValidateAsync(message, cancellationToken)
var result = await _processTypeValidator.ValidateAsync(message, documentFormat, cancellationToken)
.ConfigureAwait(false);
return result.Errors;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models;
using Energinet.DataHub.EDI.IncomingMessages.Domain.Abstractions;

namespace Energinet.DataHub.EDI.IncomingMessages.Domain.Validation;
Expand All @@ -24,7 +25,5 @@ public interface IProcessTypeValidator
/// <summary>
/// Validates Process Type
/// </summary>
/// <param name="message"></param>
/// <param name="cancellationToken"></param>
public Task<Result> ValidateAsync(IIncomingMessage message, CancellationToken cancellationToken);
public Task<Result> ValidateAsync(IIncomingMessage message, DocumentFormat documentFormat, CancellationToken cancellationToken);
}
52 changes: 31 additions & 21 deletions source/IncomingMessages.Domain/Validation/ProcessTypeValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,35 @@ namespace Energinet.DataHub.EDI.IncomingMessages.Domain.Validation;
public class ProcessTypeValidator : IProcessTypeValidator
{
private static readonly IReadOnlyCollection<string> _aggregatedMeasureDataWhitelist =
new[]
{
BusinessReason.PreliminaryAggregation.Code,
BusinessReason.BalanceFixing.Code,
BusinessReason.WholesaleFixing.Code,
BusinessReason.Correction.Code,
};
[
BusinessReason.PreliminaryAggregation.Code,
BusinessReason.BalanceFixing.Code,
BusinessReason.WholesaleFixing.Code,
BusinessReason.Correction.Code,
];

private static readonly IReadOnlyCollection<string> _wholesaleServicesWhitelist =
new[]
{
BusinessReason.WholesaleFixing.Code,
BusinessReason.Correction.Code,
};
[
BusinessReason.WholesaleFixing.Code,
BusinessReason.Correction.Code,
];

private static readonly IReadOnlyCollection<string> _meteredDataForMeasurementPointEbixWhiteList =
[
BusinessReason.PeriodicMetering.Code,
BusinessReason.PeriodicFlexMetering
.Code, // Flex metering is only supported for Ebix and should be rejected when used for CIM
];

private static readonly IReadOnlyCollection<string> _meteredDataForMeasurementPointWhiteList =
new[]
{
BusinessReason.PeriodicMetering.Code,
BusinessReason.PeriodicFlexMetering.Code, // Flex metering is only supported for Ebix and should be rejected when used for CIM
};
[
BusinessReason.PeriodicMetering.Code,
];

public async Task<Result> ValidateAsync(IIncomingMessage message, CancellationToken cancellationToken)
public async Task<Result> ValidateAsync(
IIncomingMessage message,
DocumentFormat documentFormat,
CancellationToken cancellationToken)
{
return await Task.FromResult(
message switch
Expand All @@ -57,9 +63,13 @@ public async Task<Result> ValidateAsync(IIncomingMessage message, CancellationTo
? Result.Succeeded()
: Result.Failure(new NotSupportedProcessType(rwsm.BusinessReason)),
MeteredDataForMeasurementPointMessage mdfmpm =>
_meteredDataForMeasurementPointWhiteList.Contains(mdfmpm.BusinessReason)
? Result.Succeeded()
: Result.Failure(new NotSupportedProcessType(mdfmpm.BusinessReason)),
documentFormat == DocumentFormat.Ebix
? _meteredDataForMeasurementPointEbixWhiteList.Contains(mdfmpm.BusinessReason)
? Result.Succeeded()
: Result.Failure(new NotSupportedProcessType(mdfmpm.BusinessReason))
: _meteredDataForMeasurementPointWhiteList.Contains(mdfmpm.BusinessReason)
? Result.Succeeded()
: Result.Failure(new NotSupportedProcessType(mdfmpm.BusinessReason)),
_ => throw new InvalidOperationException($"The baw's on the slates! {message.GetType().Name}"),
})
.ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,10 @@
<EmbeddedResource Include="Schemas\Ebix\Schemas\document\DK_MeteredDataTimeSeries\ebIX_DK_MeteredDataTimeSeries-2.xsd">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="Schemas\Cim\Xml\Schemas\urn-ediel-org-measure-notifyvalidatedmeasuredata-0-1.xsd" />
<EmbeddedResource Include="Schemas\Cim\Xml\Schemas\urn-ediel-org-measure-notifyvalidatedmeasuredata-0-1.xsd">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 Energinet DataHub A/S
// Copyright 2020 Energinet DataHub A/S
//
// Licensed under the Apache License, Version 2.0 (the "License2");
// you may not use this file except in compliance with the License.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2020 Energinet DataHub A/S
//
// Licensed under the Apache License, Version 2.0 (the "License2");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Xml;
using System.Xml.Schema;
using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models;
using Energinet.DataHub.EDI.IncomingMessages.Domain.Validation.ValidationErrors;
using Energinet.DataHub.EDI.IncomingMessages.Infrastructure.Schemas.Cim.Xml;

namespace Energinet.DataHub.EDI.IncomingMessages.Infrastructure.MessageParsers.BaseParsers.Xml;

public static class SchemaExtractor
{
public static async Task<(XmlSchema? Schema, string? Namespace, IncomingMarketMessageParserResult? ParserResult)> GetXmlSchemaAsync(
IIncomingMarketMessageStream incomingMarketMessageStream,
string rootElement,
CimXmlSchemaProvider schemaProvider,
CancellationToken cancellationToken)
{
string? @namespace = null;
IncomingMarketMessageParserResult? parserResult = null;
XmlSchema? xmlSchema = null;
try
{
@namespace = GetNamespace(incomingMarketMessageStream, rootElement);
var version = GetVersion(@namespace);
var businessProcessType = BusinessProcessType(@namespace);
xmlSchema = await schemaProvider.GetSchemaAsync<XmlSchema>(businessProcessType, version, cancellationToken)
.ConfigureAwait(true);

if (xmlSchema is null)
{
parserResult = new IncomingMarketMessageParserResult(
new InvalidBusinessReasonOrVersion(businessProcessType, version));
}
}
catch (XmlException exception)
{
parserResult = InvalidXmlFailure(exception);
}
catch (ObjectDisposedException objectDisposedException)
{
parserResult = InvalidXmlFailure(objectDisposedException);
}
catch (IndexOutOfRangeException indexOutOfRangeException)
{
parserResult = InvalidXmlFailure(indexOutOfRangeException);
}

return (xmlSchema, @namespace, parserResult);
}

private static IncomingMarketMessageParserResult InvalidXmlFailure(
Exception exception)
{
return new IncomingMarketMessageParserResult(
InvalidMessageStructure.From(exception));
}

private static string GetNamespace(IIncomingMarketMessageStream marketMessage, string rootElement)
{
ArgumentNullException.ThrowIfNull(marketMessage);

var settings = new XmlReaderSettings
{
Async = true,
IgnoreWhitespace = true,
IgnoreComments = true,
};

using var reader = XmlReader.Create(marketMessage.Stream, settings);
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name.Contains(rootElement))
{
return reader.NamespaceURI;
}
}

throw new XmlException($"Namespace for element '{rootElement}' not found.");
}

private static string BusinessProcessType(string @namespace)
{
ArgumentNullException.ThrowIfNull(@namespace);
var split = SplitNamespace(@namespace);
if (split.Length < 6)
{
throw new XmlException($"Invalid namespace format");
}

return split[3];
}

private static string GetVersion(string @namespace)
{
ArgumentNullException.ThrowIfNull(@namespace);
var split = SplitNamespace(@namespace);
if (split.Length < 5)
{
throw new XmlException($"Invalid namespace format");
}

var version = split[4] + "." + split[5];
return version;
}

private static string[] SplitNamespace(string @namespace)
{
ArgumentNullException.ThrowIfNull(@namespace);
return @namespace.Split(':');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Collections.ObjectModel;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
Expand Down Expand Up @@ -39,65 +40,59 @@ public abstract class EbixMessageParserBase(EbixSchemaProvider schemaProvider) :

protected abstract string RootPayloadElementName { get; }

private Collection<ValidationError> ValidationErrors { get; } = [];

protected override async Task<IncomingMarketMessageParserResult> ParseMessageAsync(
IIncomingMarketMessageStream marketMessage,
XmlSchema schemaResult,
CancellationToken cancellationToken)
{
using var reader = XmlReader.Create(marketMessage.Stream, CreateXmlReaderSettings(schemaResult));
if (Errors.Count > 0)
var document = await XDocument.LoadAsync(reader, LoadOptions.None, cancellationToken).ConfigureAwait(false);
if (ValidationErrors.Count > 0)
{
return new IncomingMarketMessageParserResult(Errors.ToArray());
return new IncomingMarketMessageParserResult(ValidationErrors.ToArray());
}

var document = await XDocument.LoadAsync(reader, LoadOptions.None, cancellationToken).ConfigureAwait(false);
var @namespace = GetNamespace(marketMessage);
var ns = XNamespace.Get(@namespace);

var header = ParseHeader(document, ns);
var transactions = ParseTransactions(document, ns, header.SenderId);

if (Errors.Count != 0)
{
return new IncomingMarketMessageParserResult(Errors.ToArray());
}

return CreateResult(header, transactions);
}

protected override async Task<(XmlSchema? Schema, IncomingMarketMessageParserResult? Result)> GetSchemaAsync(IIncomingMarketMessageStream marketMessage, CancellationToken cancellationToken)
protected override async Task<(XmlSchema? Schema, ValidationError? ValidationError)> GetSchemaAsync(IIncomingMarketMessageStream marketMessage, CancellationToken cancellationToken)
{
string? @namespace = null;
IncomingMarketMessageParserResult? parserResult = null;
XmlSchema? xmlSchema = default;
try
{
@namespace = GetNamespace(marketMessage);
var @namespace = GetNamespace(marketMessage);
var version = GetVersion(@namespace);
var businessProcessType = BusinessProcessType(@namespace);
xmlSchema = await _schemaProvider.GetSchemaAsync<XmlSchema>(businessProcessType, version, cancellationToken)
.ConfigureAwait(true);

if (xmlSchema is null)
{
parserResult = new IncomingMarketMessageParserResult(
new InvalidBusinessReasonOrVersion(businessProcessType, version));
return (xmlSchema, new InvalidBusinessReasonOrVersion(businessProcessType, version));
}
}
catch (XmlException exception)
{
parserResult = Invalid(exception);
return (xmlSchema, InvalidMessageStructure.From(exception));
}
catch (ObjectDisposedException objectDisposedException)
{
parserResult = Invalid(objectDisposedException);
return (xmlSchema, InvalidMessageStructure.From(objectDisposedException));
}
catch (IndexOutOfRangeException indexOutOfRangeException)
{
parserResult = Invalid(indexOutOfRangeException);
return (xmlSchema, InvalidMessageStructure.From(indexOutOfRangeException));
}

return (xmlSchema, parserResult);
return (xmlSchema, null);
}

protected abstract IReadOnlyCollection<IIncomingMessageSeries> ParseTransactions(XDocument document, XNamespace ns, string senderNumber);
Expand Down Expand Up @@ -210,6 +205,6 @@ private void OnValidationError(object? sender, ValidationEventArgs arguments)
{
var message =
$"XML schema validation error at line {arguments.Exception.LineNumber}, position {arguments.Exception.LinePosition}: {arguments.Message}.";
Errors.Add(InvalidMessageStructure.From(message));
ValidationErrors.Add(InvalidMessageStructure.From(message));
}
}
Loading

0 comments on commit ba0c32c

Please sign in to comment.