Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

min validator bug fix follow up #3065

Merged
merged 32 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
979c95f
nit- extra line
esmadau Sep 26, 2023
79f4446
nit: rename to HasCoreTagError and fix formatting
esmadau Sep 26, 2023
7de48fb
nit: docs
esmadau Sep 26, 2023
489f330
validate method static so does not need leniency bool
esmadau Sep 26, 2023
faadf55
start logging when we fail or warn
esmadau Sep 26, 2023
1223c59
fix nit
esmadau Sep 26, 2023
872ce53
nit: enumerate values to avoid lookup
esmadau Sep 26, 2023
65b95d4
use enum to set validation style
esmadau Sep 27, 2023
85dc7fd
use leniency by default
esmadau Sep 27, 2023
f147a46
check for null before trimming
esmadau Sep 27, 2023
8497e45
Update src/Microsoft.Health.Dicom.Core/Features/Store/StoreService.cs
esmadau Sep 28, 2023
4cb7cb1
make RequiredCoreTags private, expose checks via static method
esmadau Sep 29, 2023
24d19f7
nit: rm commas
esmadau Sep 29, 2023
b21f14c
Use common string validator to sanitize
esmadau Sep 29, 2023
b0ce322
rename class
esmadau Sep 29, 2023
539eedb
move validationStyle to its own class
esmadau Sep 29, 2023
bbfd397
rm newline
esmadau Sep 29, 2023
f2bcdb4
rename from styles to levels
esmadau Sep 29, 2023
d0f00fd
Merge branch 'users/esmadau/vs-minvalidator-followup' of https://gith…
esmadau Sep 29, 2023
08b3df0
fix nits
esmadau Oct 2, 2023
e5d445d
use base class for validation, but force each validator to tell it ho…
esmadau Oct 2, 2023
bb4e36c
Merge branch 'main' into users/esmadau/vs-minvalidator-followup
esmadau Oct 5, 2023
a802f83
use virtual getValue on string validator and add string validator tes…
esmadau Oct 6, 2023
e6c2be4
by default allow null and empty on string validation, but only after …
esmadau Oct 6, 2023
4a67944
fix nit: swap vs and value args
esmadau Oct 6, 2023
d6f7545
getValue to GetValueOrDefault with no return
esmadau Oct 6, 2023
ba5747e
use property AllowNullOrEmpty
esmadau Oct 6, 2023
fb840ca
rm out
esmadau Oct 6, 2023
21e28f1
uid validation also allows null or empty, added tests
esmadau Oct 6, 2023
df3ca69
added tests, add back in ability to override check for null or empty …
esmadau Oct 6, 2023
e0e8527
added tests
esmadau Oct 6, 2023
00ae3cc
add null/empty check back in to validate long string as it is used el…
esmadau Oct 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ private static IStoreDatasetValidator CreateStoreDatasetValidatorWithDropDataEna
new ElementMinimumValidator(),
queryTagService,
storeMeter,
contextAccessor);
contextAccessor,
NullLogger<StoreDatasetValidator>.Instance);
return validator;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading;
using System.Threading.Tasks;
using FellowOakDicom;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Health.Dicom.Core.Configs;
using Microsoft.Health.Dicom.Core.Features.Context;
Expand Down Expand Up @@ -44,7 +45,7 @@ public StoreDatasetValidatorTestsV1()
_queryTags = new List<QueryTag>(QueryTagService.CoreQueryTags);
_queryTagService.GetQueryTagsAsync(Arg.Any<CancellationToken>()).Returns(_queryTags);
_storeMeter = new StoreMeter();
_dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, minValidator, _queryTagService, _storeMeter, _dicomRequestContextAccessor);
_dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, minValidator, _queryTagService, _storeMeter, _dicomRequestContextAccessor, NullLogger<StoreDatasetValidator>.Instance);
}

[Fact]
Expand All @@ -67,7 +68,8 @@ public async Task GivenFullValidation_WhenPatientIDInvalid_ExpectErrorProduced()
minimumValidator,
_queryTagService,
_storeMeter,
_dicomRequestContextAccessor);
_dicomRequestContextAccessor,
NullLogger<StoreDatasetValidator>.Instance);

var result = await dicomDatasetValidator.ValidateAsync(
dicomDataset,
Expand Down Expand Up @@ -99,7 +101,8 @@ public async Task GivenPartialValidation_WhenPatientIDInvalid_ExpectTagValidated
minimumValidator,
_queryTagService,
_storeMeter,
_dicomRequestContextAccessor);
_dicomRequestContextAccessor,
NullLogger<StoreDatasetValidator>.Instance);

DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(
validateItems: false,
Expand Down Expand Up @@ -128,7 +131,7 @@ public async Task GivenDicomTagWithDifferentVR_WhenValidated_ThenShouldReturnInv
_queryTags.Clear();
_queryTags.Add(new QueryTag(tag.BuildExtendedQueryTagStoreEntry()));
IElementMinimumValidator validator = Substitute.For<IElementMinimumValidator>();
_dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, validator, _queryTagService, _storeMeter, _dicomRequestContextAccessor);
_dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, validator, _queryTagService, _storeMeter, _dicomRequestContextAccessor, NullLogger<StoreDatasetValidator>.Instance);

var result = await _dicomDatasetValidator.ValidateAsync(_dicomDataset, requiredStudyInstanceUid: null);

Expand Down Expand Up @@ -308,7 +311,7 @@ public async Task GivenDatasetWithInvalidVrValue_WhenValidatingWithFullValidatio
});
var minValidator = new ElementMinimumValidator();

_dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, minValidator, _queryTagService, _storeMeter, _dicomRequestContextAccessor);
_dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, minValidator, _queryTagService, _storeMeter, _dicomRequestContextAccessor, NullLogger<StoreDatasetValidator>.Instance);

// LO VR, invalid characters
_dicomDataset.Add(DicomTag.SeriesDescription, "CT1 abdomen\u0000");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Threading;
using System.Threading.Tasks;
using FellowOakDicom;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Health.Dicom.Core.Configs;
using Microsoft.Health.Dicom.Core.Features.Context;
Expand Down Expand Up @@ -43,7 +44,7 @@ public StoreDatasetValidatorTestsV2()
_storeMeter = new StoreMeter();
_dicomRequestContextV2.Version.Returns(2);
_dicomRequestContextAccessorV2.RequestContext.Returns(_dicomRequestContextV2);
_dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, _minimumValidator, _queryTagService, _storeMeter, _dicomRequestContextAccessorV2);
_dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, _minimumValidator, _queryTagService, _storeMeter, _dicomRequestContextAccessorV2, NullLogger<StoreDatasetValidator>.Instance);
}

[Fact]
Expand All @@ -62,7 +63,8 @@ public async Task GivenFullValidation_WhenPatientIDInvalid_ExpectErrorProduced()
_minimumValidator,
_queryTagService,
_storeMeter,
_dicomRequestContextAccessorV2);
_dicomRequestContextAccessorV2,
NullLogger<StoreDatasetValidator>.Instance);

var result = await validator.ValidateAsync(
dicomDataset,
Expand All @@ -73,7 +75,7 @@ public async Task GivenFullValidation_WhenPatientIDInvalid_ExpectErrorProduced()
"""does not validate VR LO: value contains invalid character""",
result.InvalidTagErrors[DicomTag.PatientID].Error);

_minimumValidator.DidNotReceive().Validate(Arg.Any<DicomElement>(), true);
_minimumValidator.DidNotReceive().Validate(Arg.Any<DicomElement>(), ValidationLevel.Default);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public ElementMinimumValidatorTests()
[MemberData(nameof(SupportedDicomElements))]
public void GivenSupportedVR_WhenValidating_ThenShouldPass(DicomElement dicomElement)
{
_validator.Validate(dicomElement, false);
_validator.Validate(dicomElement);
}

public static IEnumerable<object[]> SupportedDicomElements()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,8 @@ public static ValidationWarnings ValidateQueryTag(this DicomDataset dataset, Que
/// <param name="dataset">The dicom dataset.</param>
/// <param name="dicomTag">The dicom tag being validated.</param>
/// <param name="minimumValidator">The minimum validator.</param>
/// <param name="withLeniency">Whether or not to validate with additional leniency</param>
public static ValidationWarnings ValidateDicomTag(this DicomDataset dataset, DicomTag dicomTag, IElementMinimumValidator minimumValidator, bool withLeniency = false)
/// <param name="validationLevel">Style of validation to enforce on running rules</param>
public static ValidationWarnings ValidateDicomTag(this DicomDataset dataset, DicomTag dicomTag, IElementMinimumValidator minimumValidator, ValidationLevel validationLevel = ValidationLevel.Strict)
{
EnsureArg.IsNotNull(dataset, nameof(dataset));
EnsureArg.IsNotNull(dicomTag, nameof(dicomTag));
Expand All @@ -432,7 +432,7 @@ public static ValidationWarnings ValidateDicomTag(this DicomDataset dataset, Dic
warning |= ValidationWarnings.IndexedDicomTagHasMultipleValues;
}

minimumValidator.Validate(dicomElement, withLeniency);
minimumValidator.Validate(dicomElement, validationLevel);
}
return warning;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Threading.Tasks;
using EnsureThat;
using FellowOakDicom;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Health.Dicom.Core.Configs;
using Microsoft.Health.Dicom.Core.Exceptions;
Expand All @@ -32,9 +33,9 @@ public class StoreDatasetValidator : IStoreDatasetValidator
private readonly IQueryTagService _queryTagService;
private readonly StoreMeter _storeMeter;
private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor;
private readonly ILogger _logger;


public static readonly HashSet<DicomTag> RequiredCoreTags = new HashSet<DicomTag>()
private static readonly IReadOnlySet<DicomTag> RequiredCoreTags = new HashSet<DicomTag>()
{
DicomTag.StudyInstanceUID,
DicomTag.SeriesInstanceUID,
Expand All @@ -48,7 +49,8 @@ public StoreDatasetValidator(
IElementMinimumValidator minimumValidator,
IQueryTagService queryTagService,
StoreMeter storeMeter,
IDicomRequestContextAccessor dicomRequestContextAccessor)
IDicomRequestContextAccessor dicomRequestContextAccessor,
ILogger<StoreDatasetValidator> logger)
{
EnsureArg.IsNotNull(featureConfiguration?.Value, nameof(featureConfiguration));
EnsureArg.IsNotNull(minimumValidator, nameof(minimumValidator));
Expand All @@ -60,6 +62,7 @@ public StoreDatasetValidator(
_minimumValidator = minimumValidator;
_queryTagService = queryTagService;
_storeMeter = EnsureArg.IsNotNull(storeMeter, nameof(storeMeter));
_logger = EnsureArg.IsNotNull(logger, nameof(logger));
}

/// <inheritdoc/>
Expand Down Expand Up @@ -234,7 +237,7 @@ private async Task ValidateAllItemsWithLeniencyAsync(
/// </remarks>
/// <param name="dicomDataset"></param>
/// <param name="validationResultBuilder"></param>
/// <param name="queryTags"></param>
/// <param name="queryTags">Queryable dicom tags</param>
private void GenerateErrors(DicomDataset dicomDataset, StoreValidationResultBuilder validationResultBuilder,
IReadOnlyCollection<QueryTag> queryTags)
{
Expand All @@ -244,13 +247,14 @@ private void GenerateErrors(DicomDataset dicomDataset, StoreValidationResultBuil
{
// validate with additional leniency
var validationWarning =
dicomDataset.ValidateDicomTag(requiredCoreTag, _minimumValidator, withLeniency: true);
dicomDataset.ValidateDicomTag(requiredCoreTag, _minimumValidator, validationLevel: ValidationLevel.Default);

validationResultBuilder.Add(validationWarning, requiredCoreTag);
}
catch (ElementValidationException ex)
{
validationResultBuilder.Add(ex, requiredCoreTag, isCoreTag: true);
_logger.LogInformation("Dicom instance validation failed with error on required core tag {Tag} with id {Id}", requiredCoreTag.DictionaryEntry.Keyword, requiredCoreTag);
esmadau marked this conversation as resolved.
Show resolved Hide resolved
_storeMeter.V2ValidationError.Add(1,
TelemetryDimension(requiredCoreTag, IsIndexableTag(queryTags, requiredCoreTag)));
}
Expand Down Expand Up @@ -305,6 +309,14 @@ private void GenerateValidationWarnings(DicomDataset dicomDataset, StoreValidati
catch (DicomValidationException ex)
{
validationResultBuilder.Add(ex, item.Tag, isCoreTag: false);
if (item.Tag.IsPrivate)
{
_logger.LogInformation("Dicom instance validation succeeded but with warning on a private tag");
}
else
{
_logger.LogInformation("Dicom instance validation succeeded but with warning on non-private tag {Tag} with id {Id}, where tag is indexable: {Indexable} and VR is {VR}", item.Tag.DictionaryEntry.Keyword, item.Tag, IsIndexableTag(queryTags, item), item.ValueRepresentation);
}
_storeMeter.V2ValidationError.Add(1, TelemetryDimension(item, IsIndexableTag(queryTags, item)));
}
}
Expand All @@ -326,14 +338,24 @@ private void ValidateStringItemWithLeniency(string value, DicomElement de, IRead

private static bool IsIndexableTag(IReadOnlyCollection<QueryTag> queryTags, DicomItem de)
{
return queryTags.Any(x => x.Tag == de.Tag);
return IsIndexableTag(queryTags, de.Tag);
}

private static bool IsIndexableTag(IReadOnlyCollection<QueryTag> queryTags, DicomTag tag)
{
return queryTags.Any(x => x.Tag == tag);
}

/// <summary>
/// Check if a tag is a required core tag
/// </summary>
/// <param name="tag">tag to check if it is required</param>
/// <returns>whether or not tag is required</returns>
public static bool IsCoreTag(DicomTag tag)
{
return RequiredCoreTags.Contains(tag);
}

private void ValidateWithoutNullPadding(string value, DicomElement de, IReadOnlyCollection<QueryTag> queryTags)
{
de.ValueRepresentation.ValidateString(value.TrimEnd('\0'));
Expand All @@ -349,7 +371,7 @@ private static KeyValuePair<string, object>[] TelemetryDimension(DicomTag tag, b
new[]
{
new KeyValuePair<string, object>("TagKeyword", tag.DictionaryEntry.Keyword),
new KeyValuePair<string, object>("VR", tag.GetDefaultVR()),
new KeyValuePair<string, object>("VR", tag.GetDefaultVR().ToString()),
new KeyValuePair<string, object>("Tag", tag.ToString()),
new KeyValuePair<string, object>("IsIndexable", isIndexableTag.ToString())
};
Expand Down
10 changes: 5 additions & 5 deletions src/Microsoft.Health.Dicom.Core/Features/Store/StoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public async Task<StoreResponse> ProcessAsync(
if (dropMetadata)
{
// if any core tag errors occured, log as failure and return. otherwise we drop the invalid tag
if (storeValidatorResult.InvalidCoreTagErrorsPresent)
if (storeValidatorResult.HasCoreTagError)
{
LogFailure(index, dicomDataset, storeValidatorResult);
return null;
Expand Down Expand Up @@ -262,14 +262,14 @@ private void LogFailure(int index, DicomDataset dicomDataset, StoreValidationRes
private void DropInvalidMetadata(StoreValidationResult storeValidatorResult, DicomDataset dicomDataset, Partition partition)
{
var identifier = dicomDataset.ToInstanceIdentifier(partition);
foreach (DicomTag tag in storeValidatorResult.InvalidTagErrors.Keys)
foreach ((DicomTag tag, StoreErrorResult result) in storeValidatorResult.InvalidTagErrors)
{
if (!StoreDatasetValidator.RequiredCoreTags.Contains(tag))
if (!StoreDatasetValidator.IsCoreTag(error.Key))
{
// drop invalid metadata if not a core tag
dicomDataset.Remove(tag);
dicomDataset.Remove(error.Key);

string message = storeValidatorResult.InvalidTagErrors[tag].Error;
string message = error.Value.Error;
_telemetryClient.ForwardLogTrace(
$"{message}. This attribute will not be present when retrieving study, series, or instance metadata resources, nor can it be used in searches." +
" However, it will still be present when retrieving study, series, or instance resources.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,5 @@ public StoreValidationResult(

public IReadOnlyDictionary<DicomTag, StoreErrorResult> InvalidTagErrors { get; }

public bool InvalidCoreTagErrorsPresent
{
get => InvalidTagErrors.Any(invalidTagError => invalidTagError.Value.IsRequiredCoreTag);
}
public bool HasCoreTagError => InvalidTagErrors.Any(invalidTagError => invalidTagError.Value.IsRequiredCoreTag);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

namespace Microsoft.Health.Dicom.Core.Features.Validation;

/// <summary>
/// Use for base string sanitization shared across all string related validators
/// </summary>
public static class BaseStringSanitizer
{
/// <summary>
/// Sanitizes string value to prep for validation
/// </summary>
public static string Sanitize(string value, ValidationLevel validationLevel)
{
return validationLevel == ValidationLevel.Default
? string.IsNullOrEmpty(value) ? value : RemoveNullPadding(value)
: value;
}

private static string RemoveNullPadding(string value)
{
return value.TrimEnd('\0');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,10 @@ internal class DateValidation : IElementValidation
{
private const string DateFormatDA = "yyyyMMdd";

public void Validate(DicomElement dicomElement, bool withLeniency = false)
public void Validate(DicomElement dicomElement, ValidationLevel validationLevel = ValidationLevel.Strict)
{
string value = dicomElement.GetFirstValueOrDefault<string>();
string name = dicomElement.Tag.GetFriendlyName();
if (withLeniency)
{
value = value.TrimEnd('\0');
}
string value = BaseStringSanitizer.Sanitize(dicomElement.GetFirstValueOrDefault<string>(), validationLevel);
if (string.IsNullOrEmpty(value))
{
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,9 @@ public ElementMaxLengthValidation(int maxLength)

public int MaxLength { get; }

public void Validate(DicomElement dicomElement, bool withLeniency = false)
public void Validate(DicomElement dicomElement, ValidationLevel validationLevel = ValidationLevel.Strict)
{
string value = dicomElement.GetFirstValueOrDefault<string>();
if (withLeniency)
{
value = value.TrimEnd('\0');
}
string value = BaseStringSanitizer.Sanitize(dicomElement.GetFirstValueOrDefault<string>(), validationLevel);
Validate(value, MaxLength, dicomElement.Tag.GetFriendlyName(), dicomElement.ValueRepresentation);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class ElementMinimumValidator : IElementMinimumValidator
{ DicomVR.US, new ElementRequiredLengthValidation(2) },
};

public void Validate(DicomElement dicomElement, bool withLeniency = false)
public void Validate(DicomElement dicomElement, ValidationLevel validationLevel = ValidationLevel.Strict)
{
EnsureArg.IsNotNull(dicomElement, nameof(dicomElement));
DicomVR vr = dicomElement.ValueRepresentation;
Expand All @@ -43,7 +43,7 @@ public void Validate(DicomElement dicomElement, bool withLeniency = false)
}
if (Validations.TryGetValue(vr, out IElementValidation validationRule))
{
validationRule.Validate(dicomElement, withLeniency);
validationRule.Validate(dicomElement, validationLevel);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,12 @@ public ElementRequiredLengthValidation(int expectedLength)
ExpectedLength = expectedLength;
}

public void Validate(DicomElement dicomElement, bool withLeniency = false)
public void Validate(DicomElement dicomElement, ValidationLevel validationLevel = ValidationLevel.Strict)
{
DicomVR vr = dicomElement.ValueRepresentation;
if (TryGetAsString(dicomElement, out string value))
{
if (withLeniency)
{
value = value.TrimEnd('\0');
}
value = BaseStringSanitizer.Sanitize(value, validationLevel);
ValidateStringLength(vr, dicomElement.Tag.GetFriendlyName(), value);
}
else
Expand Down
Loading