From 694c3b1d8b1b9ee28aa400b100547e840bcbf31d Mon Sep 17 00:00:00 2001 From: SvitlanaKovalova1 Date: Tue, 3 Sep 2024 13:29:24 +0300 Subject: [PATCH] feat(specification-subfields-validation) implement "Missing Subfield" Rule Validation --- NEWS.md | 1 + .../folio/rspec/utils/SpecificationUtils.java | 8 ++ .../marc/impl/MarcRecordRuleValidator.java | 8 ++ .../impl/MissingSubfieldRuleValidator.java | 76 ++++++++++++++++++ .../validator/marc/model/MarcRuleCode.java | 3 +- .../rspec/utils/SpecificationUtilsTest.java | 18 +++++ .../MarcSpecificationGuidedValidatorTest.java | 17 ++++ .../org/folio/support/TestDataProvider.java | 33 ++++++++ .../testdata/marc-subfield-record.json | 77 +++++++++++++++++++ .../mod-record-specifications/en.json | 6 +- 10 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/impl/MissingSubfieldRuleValidator.java create mode 100644 mod-record-specifications-validator/src/test/resources/testdata/marc-subfield-record.json diff --git a/NEWS.md b/NEWS.md index 0020d01c..80ad9967 100644 --- a/NEWS.md +++ b/NEWS.md @@ -26,6 +26,7 @@ * implement Non-Repeatable Required 1XX Fields validator ([MRSPECS-42](https://folio-org.atlassian.net/browse/MRSPECS-42)) * implement Invalid Field Tag validator ([MRSPECS-44](https://folio-org.atlassian.net/browse/MRSPECS-44)) * implement Invalid Indicator validation ([MRSPECS-45](https://folio-org.atlassian.net/browse/MRSPECS-45)) +* implement Missing Subfield validation([MRSPECS-47](https://folio-org.atlassian.net/browse/MRSPECS-47)) #### General * Implement build dependants GitHub workflow on PR creation ([MRSPECS-9](https://folio-org.atlassian.net//browse/MRSPECS-9)) diff --git a/mod-record-specifications-validator/src/main/java/org/folio/rspec/utils/SpecificationUtils.java b/mod-record-specifications-validator/src/main/java/org/folio/rspec/utils/SpecificationUtils.java index e09a3dca..051ae657 100644 --- a/mod-record-specifications-validator/src/main/java/org/folio/rspec/utils/SpecificationUtils.java +++ b/mod-record-specifications-validator/src/main/java/org/folio/rspec/utils/SpecificationUtils.java @@ -9,6 +9,7 @@ import org.folio.rspec.domain.dto.SpecificationDto; import org.folio.rspec.domain.dto.SpecificationFieldDto; import org.folio.rspec.domain.dto.SpecificationRuleDto; +import org.folio.rspec.domain.dto.SubfieldDto; @UtilityClass public class SpecificationUtils { @@ -35,4 +36,11 @@ public static Optional findField(SpecificationDto specifi .filter(fieldDto -> tag.equals(fieldDto.getTag())) .findFirst(); } + + public static Map requiredSubfields(List subfieldDto) { + return subfieldDto == null ? Map.of() : subfieldDto + .stream() + .filter(subfield -> Boolean.TRUE.equals(subfield.getRequired())) + .collect(Collectors.toMap(subfield -> subfield.getCode().charAt(0), Function.identity())); + } } diff --git a/mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/impl/MarcRecordRuleValidator.java b/mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/impl/MarcRecordRuleValidator.java index 6eca4657..a9f50cbf 100644 --- a/mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/impl/MarcRecordRuleValidator.java +++ b/mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/impl/MarcRecordRuleValidator.java @@ -17,12 +17,14 @@ import org.folio.rspec.validation.validator.marc.model.MarcField; import org.folio.rspec.validation.validator.marc.model.MarcIndicator; import org.folio.rspec.validation.validator.marc.model.MarcRecord; +import org.folio.rspec.validation.validator.marc.model.MarcSubfield; public class MarcRecordRuleValidator implements SpecificationRuleValidator { private final List>, SpecificationDto>> fieldSetValidators; private final List> fieldValidators; private final List, SpecificationFieldDto>> indicatorValidators; + private final List, SpecificationFieldDto>> subfieldValidators; public MarcRecordRuleValidator(TranslationProvider translationProvider) { this.fieldSetValidators = List.of( @@ -38,6 +40,7 @@ public MarcRecordRuleValidator(TranslationProvider translationProvider) { this.indicatorValidators = List.of( new InvalidIndicatorRuleValidator(translationProvider) ); + this.subfieldValidators = List.of(new MissingSubfieldRuleValidator(translationProvider)); } @Override @@ -64,6 +67,11 @@ public List validate(MarcRecord marcRecord, SpecificationDto sp validationErrors.addAll(validator.validate(field.indicators(), fieldDefinition)); } } + for (var validator : subfieldValidators) { + if (ruleIsEnabled(validator.ruleCode(), specification) && marcField instanceof MarcDataField field) { + validationErrors.addAll(validator.validate(field.subfields(), fieldDefinition)); + } + } }); } return validationErrors; diff --git a/mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/impl/MissingSubfieldRuleValidator.java b/mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/impl/MissingSubfieldRuleValidator.java new file mode 100644 index 00000000..22bde0a2 --- /dev/null +++ b/mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/impl/MissingSubfieldRuleValidator.java @@ -0,0 +1,76 @@ +package org.folio.rspec.validation.validator.marc.impl; + +import java.util.List; +import org.folio.rspec.domain.dto.DefinitionType; +import org.folio.rspec.domain.dto.SeverityType; +import org.folio.rspec.domain.dto.SpecificationFieldDto; +import org.folio.rspec.domain.dto.SubfieldDto; +import org.folio.rspec.domain.dto.ValidationError; +import org.folio.rspec.i18n.TranslationProvider; +import org.folio.rspec.utils.SpecificationUtils; +import org.folio.rspec.validation.validator.SpecificationRuleCode; +import org.folio.rspec.validation.validator.SpecificationRuleValidator; +import org.folio.rspec.validation.validator.marc.model.MarcRuleCode; +import org.folio.rspec.validation.validator.marc.model.MarcSubfield; +import org.folio.rspec.validation.validator.marc.model.Reference; +import org.springframework.util.CollectionUtils; + +public class MissingSubfieldRuleValidator + implements SpecificationRuleValidator, SpecificationFieldDto> { + + private static final String CODE_KEY = "code"; + + private final TranslationProvider translationProvider; + + MissingSubfieldRuleValidator(TranslationProvider translationProvider) { + this.translationProvider = translationProvider; + } + + @Override + public List validate(List subfields, SpecificationFieldDto specification) { + var requiredSubFields = SpecificationUtils.requiredSubfields(specification.getSubfields()); + + return requiredSubFields.keySet().stream() + .filter(subFieldCode -> isMissing(subfields, subFieldCode)) + .map(subFieldCode -> buildError(specification.getTag(), requiredSubFields.get(subFieldCode))) + .toList(); + } + + @Override + public SpecificationRuleCode supportedRule() { + return MarcRuleCode.MISSING_SUBFIELD; + } + + @Override + public DefinitionType definitionType() { + return DefinitionType.SUBFIELD; + } + + @Override + public SeverityType severity() { + return SeverityType.ERROR; + } + + private ValidationError buildError(String tag, SubfieldDto definition) { + var message = translationProvider.format(ruleCode(), CODE_KEY); + return ValidationError.builder() + .path(Reference.forSubfield(Reference.forTag(tag), definition.getCode().charAt(0)).toString()) + .definitionType(definitionType()) + .definitionId(definition.getId()) + .severity(severity()) + .ruleCode(ruleCode()) + .message(message) + .build(); + } + + private boolean isMissing(List marcSubfields, Character subFieldCode) { + return CollectionUtils.isEmpty(marcSubfields) || marcSubfields.stream() + .noneMatch(subfield -> isSubfieldEquals(subfield, subFieldCode)); + } + + private boolean isSubfieldEquals(MarcSubfield subfield, Character subFieldCode) { + return subfield.reference() != null + && subfield.reference().getSubfield() != null + && subfield.reference().getSubfield().equals(subFieldCode); + } +} diff --git a/mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/model/MarcRuleCode.java b/mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/model/MarcRuleCode.java index 9de61672..fb0b14a6 100644 --- a/mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/model/MarcRuleCode.java +++ b/mod-record-specifications-validator/src/main/java/org/folio/rspec/validation/validator/marc/model/MarcRuleCode.java @@ -10,7 +10,8 @@ public enum MarcRuleCode implements SpecificationRuleCode { NON_REPEATABLE_1XX_FIELD("nonRepeatable1XXField"), NON_REPEATABLE_REQUIRED_1XX_FIELD("nonRepeatableRequired1XXField"), NON_REPEATABLE_FIELD("nonRepeatableField"), - INVALID_INDICATOR("invalidIndicator"); + INVALID_INDICATOR("invalidIndicator"), + MISSING_SUBFIELD("missingSubfield"); private final String code; diff --git a/mod-record-specifications-validator/src/test/java/org/folio/rspec/utils/SpecificationUtilsTest.java b/mod-record-specifications-validator/src/test/java/org/folio/rspec/utils/SpecificationUtilsTest.java index e678e95a..ce1d33ef 100644 --- a/mod-record-specifications-validator/src/test/java/org/folio/rspec/utils/SpecificationUtilsTest.java +++ b/mod-record-specifications-validator/src/test/java/org/folio/rspec/utils/SpecificationUtilsTest.java @@ -9,6 +9,7 @@ import org.folio.rspec.domain.dto.SpecificationDto; import org.folio.rspec.domain.dto.SpecificationFieldDto; import org.folio.rspec.domain.dto.SpecificationRuleDto; +import org.folio.rspec.domain.dto.SubfieldDto; import org.folio.spring.testing.type.UnitTest; import org.junit.jupiter.api.Test; @@ -78,4 +79,21 @@ void findField_returnsField() { assertTrue(result.isPresent()); assertEquals("tag1", result.get().getTag()); } + + @Test + void requiredSubfields_returnsRequiredSubfields() { + Map result = SpecificationUtils.requiredSubfields( + List.of( + getSubfieldDto('a', true), + getSubfieldDto('b', false), + getSubfieldDto('c', true))); + + assertEquals(2, result.size()); + assertTrue(result.containsKey('a')); + assertTrue(result.containsKey('c')); + } + + private SubfieldDto getSubfieldDto(Character code, boolean isRequired) { + return new SubfieldDto().code(code.toString()).required(isRequired); + } } diff --git a/mod-record-specifications-validator/src/test/java/org/folio/rspec/validation/MarcSpecificationGuidedValidatorTest.java b/mod-record-specifications-validator/src/test/java/org/folio/rspec/validation/MarcSpecificationGuidedValidatorTest.java index 91748ab9..64ebdfd4 100644 --- a/mod-record-specifications-validator/src/test/java/org/folio/rspec/validation/MarcSpecificationGuidedValidatorTest.java +++ b/mod-record-specifications-validator/src/test/java/org/folio/rspec/validation/MarcSpecificationGuidedValidatorTest.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.tuple; import static org.folio.support.TestDataProvider.getSpecification; import static org.folio.support.TestDataProvider.getSpecificationWithIndicators; +import static org.folio.support.TestDataProvider.getSpecificationWithSubfields; import static org.folio.support.TestDataProvider.getSpecificationWithTags; import java.util.stream.Stream; @@ -91,6 +92,22 @@ void testInvalidIndicatorValidation() { ); } + @Test + void testMarcRecordSubfieldValidation() { + var marc4jRecord = TestRecordProvider.getMarc4jRecord("testdata/marc-subfield-record.json"); + var validationErrors = validator.validate(marc4jRecord, getSpecificationWithSubfields()); + assertThat(validationErrors) + .hasSize(5) + .extracting(ValidationError::getPath, ValidationError::getRuleCode) + .containsExactlyInAnyOrder( + tuple("650[0]$d[0]", MarcRuleCode.MISSING_SUBFIELD.getCode()), + tuple("035[0]$d[0]", MarcRuleCode.MISSING_SUBFIELD.getCode()), + tuple("047[0]$d[0]", MarcRuleCode.MISSING_SUBFIELD.getCode()), + tuple("245[0]$d[0]", MarcRuleCode.MISSING_SUBFIELD.getCode()), + tuple("010[0]$d[0]", MarcRuleCode.MISSING_SUBFIELD.getCode()) + ); + } + private static Stream provide1xxArguments() { return Stream.of( // Multiple 1xx fields with same undefined tag diff --git a/mod-record-specifications-validator/src/test/java/org/folio/support/TestDataProvider.java b/mod-record-specifications-validator/src/test/java/org/folio/support/TestDataProvider.java index 808bf285..3cae5e51 100644 --- a/mod-record-specifications-validator/src/test/java/org/folio/support/TestDataProvider.java +++ b/mod-record-specifications-validator/src/test/java/org/folio/support/TestDataProvider.java @@ -11,6 +11,7 @@ import org.folio.rspec.domain.dto.SpecificationDto; import org.folio.rspec.domain.dto.SpecificationFieldDto; import org.folio.rspec.domain.dto.SpecificationRuleDto; +import org.folio.rspec.domain.dto.SubfieldDto; import org.folio.rspec.validation.validator.marc.model.MarcRuleCode; public class TestDataProvider { @@ -42,6 +43,15 @@ public static SpecificationDto getSpecificationWithIndicators() { .fields(indicatorsFieldDefinitions()); } + public static SpecificationDto getSpecificationWithSubfields() { + return new SpecificationDto() + .id(UUID.randomUUID()) + .family(Family.MARC) + .profile(FamilyProfile.BIBLIOGRAPHIC) + .rules(allEnabledRules()) + .fields(subfieldDefinitions()); + } + private static List commonFieldDefinitions() { List fields = new ArrayList<>(); fields.add(requiredNonRepeatableField("000")); @@ -66,6 +76,16 @@ private static List indicatorsFieldDefinitions() { return fields; } + private static List subfieldDefinitions() { + return List.of(requiredNonRepeatableField("000"), + defaultFieldWithSubfields("010"), + defaultFieldWithSubfields("035"), + defaultFieldWithSubfields("047"), + defaultFieldWithSubfields("100"), + defaultFieldWithSubfields("245"), + defaultFieldWithSubfields("650")); + } + private static List fieldDefinitions() { List fields = commonFieldDefinitions(); fields.add(defaultField("035")); @@ -102,6 +122,10 @@ private static SpecificationFieldDto defaultFieldWithIndicator(String tag) { return defaultField(tag).indicators(List.of(getIndicator(1), getIndicator(2))); } + private static SpecificationFieldDto defaultFieldWithSubfields(String tag) { + return defaultField(tag).subfields(List.of(getSubfield("a"), getSubfield("d"))); + } + private static SpecificationFieldDto fieldDefinition(String tag, boolean required, boolean deprecated, boolean repeatable) { return new SpecificationFieldDto() @@ -126,4 +150,13 @@ private static FieldIndicatorDto getIndicator(int order) { .order(order) .addCodesItem(new IndicatorCodeDto().code("code")); } + + private static SubfieldDto getSubfield(String code) { + return new SubfieldDto() + .id(UUID.randomUUID()) + .required(true) + .deprecated(false) + .repeatable(true) + .code(code); + } } diff --git a/mod-record-specifications-validator/src/test/resources/testdata/marc-subfield-record.json b/mod-record-specifications-validator/src/test/resources/testdata/marc-subfield-record.json new file mode 100644 index 00000000..c988f90f --- /dev/null +++ b/mod-record-specifications-validator/src/test/resources/testdata/marc-subfield-record.json @@ -0,0 +1,77 @@ +{ + "fields": [ + { + "010": { + "ind1": "#", + "ind2": " ", + "subfields": [ + { + "a": "LC control number" + } + ] + } + }, + { + "035": { + "ind1": "#", + "ind2": " ", + "subfields": [ + { + "a": "System control number" + } + ] + } + }, + { + "047": { + "ind1": "#", + "ind2": " ", + "subfields": [ + { + "a": "Form of musical composition code" + } + ] + } + }, + { + "100": { + "ind1": "#", + "ind2": " ", + "subfields": [ + { + "a": "Personal name" + }, + { + "d": "1756-1791." + }, + { + "0": "12345" + } + ] + } + }, + { + "245": { + "ind1": "#", + "ind2": " ", + "subfields": [ + { + "a": "Title" + } + ] + } + }, + { + "650": { + "ind1": "#", + "ind2": " ", + "subfields": [ + { + "a": "Topical term" + } + ] + } + } + ], + "leader": "01750ccm a2200421 4500" +} diff --git a/translations/mod-record-specifications/en.json b/translations/mod-record-specifications/en.json index ff41e653..7f1a4dbd 100644 --- a/translations/mod-record-specifications/en.json +++ b/translations/mod-record-specifications/en.json @@ -29,6 +29,6 @@ "validation.nonRepeatableRequired1XXField": "Field 1XX is non-repeatable and required.", "validation.nonRepeatableField": "Field is non-repeatable.", "validation.undefinedField": "Field is undefined.", - "validation.invalidIndicator": "Indicator must contain one character and can only accept numbers 0-9, letters a-z or a '#'." - -} \ No newline at end of file + "validation.invalidIndicator": "Indicator must contain one character and can only accept numbers 0-9, letters a-z or a '#'.", + "validation.missingSubfield": "Subfield {code} is required." +}