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

MRSPECS-59: implement LCCN subfield validation #79

Merged
merged 13 commits into from
Oct 10, 2024
Merged
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
* implement Missing Subfield validation([MRSPECS-47](https://folio-org.atlassian.net/browse/MRSPECS-47))
* implement Undefined Subfield validation ([MRSPECS-49](https://folio-org.atlassian.net/browse/MRSPECS-49))
* implement Non-Repeatable Subfield validation ([MRSPECS-48](https://folio-org.atlassian.net/browse/MRSPECS-48))
* implement Invalid LCCN Subfield validation ([MRSPECS-59](https://folio-org.atlassian.net/browse/MRSPECS-59))

#### General
* Implement build dependants GitHub workflow on PR creation ([MRSPECS-9](https://folio-org.atlassian.net//browse/MRSPECS-9))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,30 @@
</insert>
</changeSet>

<changeSet id="MRSPECS-59@@default-data:rule-invalidLccnSubfieldValue" labels="default-data"
author="mukhiddin_yusupov">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(id)
FROM rule
WHERE id = '0a769f6a-fe60-4ef1-bfa6-bcdd88908d04';
</sqlCheck>
</preConditions>

<comment>Populate default data for invalidLccnSubfieldValue rule</comment>

<insert tableName="rule">
<column name="id" value="0a769f6a-fe60-4ef1-bfa6-bcdd88908d04"/>
<column name="name" value="Invalid LCCN Subfield Value"/>
<column name="code" value="invalidLccnSubfieldValue"/>
<column name="description" value="Subfield value is not valid based on LCCN structure"/>
<column name="created_by_user_id" value="00000000-0000-0000-0000-000000000000"/>
<column name="created_date" valueComputed="now()"/>
<column name="updated_by_user_id" value="00000000-0000-0000-0000-000000000000"/>
<column name="updated_date" valueComputed="now()"/>
</insert>
</changeSet>

<!-- Authority specification rules -->

<changeSet id="MRSPECS-4@@default-data:specification-rule-authority-undefinedField" labels="default-data"
Expand Down Expand Up @@ -687,6 +711,27 @@
</insert>
</changeSet>

<changeSet id="MRSPECS-59@@default-data:specification-rule-authority-invalidLccnSubfieldValue"
labels="default-data"
author="mukhiddin_yusupov">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*)
FROM specification_rule
WHERE specification_id = 'a37d01ba-75c7-4a0c-997e-309823f1df3f'
AND rule_id = '0a769f6a-fe60-4ef1-bfa6-bcdd88908d04';
</sqlCheck>
</preConditions>

<comment>Populate default data for authority-invalidLccnSubfieldValue rule</comment>

<insert tableName="specification_rule">
<column name="specification_id" value="a37d01ba-75c7-4a0c-997e-309823f1df3f"/>
<column name="rule_id" value="0a769f6a-fe60-4ef1-bfa6-bcdd88908d04"/>
<column name="enabled" value="false"/>
</insert>
</changeSet>

<!-- Bibliographic specification rules -->

<changeSet id="MRSPECS-4@@default-data:specification-rule-bibliographic-undefinedField" labels="default-data"
Expand Down Expand Up @@ -969,6 +1014,27 @@
</insert>
</changeSet>

<changeSet id="MRSPECS-59@@default-data:specification-rule-bibliographic-invalidLccnSubfieldValue"
labels="default-data"
author="mukhiddin_yusupov">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*)
FROM specification_rule
WHERE specification_id = '6eefa4c6-bbf7-4845-ad82-de7fc4abd0e3'
AND rule_id = '0a769f6a-fe60-4ef1-bfa6-bcdd88908d04';
</sqlCheck>
</preConditions>

<comment>Populate default data for bibliographic-invalidLccnSubfieldValue rule</comment>

<insert tableName="specification_rule">
<column name="specification_id" value="6eefa4c6-bbf7-4845-ad82-de7fc4abd0e3"/>
<column name="rule_id" value="0a769f6a-fe60-4ef1-bfa6-bcdd88908d04"/>
<column name="enabled" value="false"/>
</insert>
</changeSet>

<changeSet id="MRSPECS-12@@default-data:specification-metadata-fields-v1" author="pavlo_smahin" labels="default-data">
<loadUpdateData tableName="specification_metadata" file="data/specification_metadata.csv"
relativeToChangelogFile="true" primaryKey="id"/>
Expand All @@ -979,4 +1045,4 @@
relativeToChangelogFile="true" primaryKey="id"/>
</changeSet>

</databaseChangeLog>
</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ void getSpecification_shouldReturn200AndSpecificationWithRequiredFields() throws
@Test
void getSpecificationRules_shouldReturn200AndCollectionOfRules() throws Exception {
doGet(specificationRulesPath(BIBLIOGRAPHIC_SPECIFICATION_ID))
.andExpect(jsonPath("totalRecords", is(14)))
.andExpect(jsonPath("rules.size()", is(14)))
.andExpect(jsonPath("totalRecords", is(15)))
.andExpect(jsonPath("rules.size()", is(15)))
.andExpect(jsonPath("rules[0].id", notNullValue()))
.andExpect(jsonPath("rules[0].name", notNullValue()))
.andExpect(jsonPath("rules[0].description", notNullValue()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ void findBySpecificationId() {

assertThat(specificationRuleList)
.isNotEmpty()
.hasSize(14);
.hasSize(15);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.folio.rspec.validation.validator.marc.impl;

import java.util.List;
import java.util.regex.Pattern;
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.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;


public class InvalidLccnSubfieldRuleValidator
implements SpecificationRuleValidator<List<MarcSubfield>, SpecificationFieldDto> {

private static final String CODE_KEY = "code";
private static final String TAG_010 = "010";
private static final String LCCN_SUBFIELD = "a";
private static final Pattern LCCN_STRUCTURE_A_PATTERN = Pattern.compile("( {3}|[a-z][|a-z]{2})\\d{8} ");
private static final Pattern LCCN_STRUCTURE_B_PATTERN = Pattern.compile("( {2}|[a-z][|a-z])\\d{10}");

private final TranslationProvider translationProvider;

InvalidLccnSubfieldRuleValidator(TranslationProvider translationProvider) {
this.translationProvider = translationProvider;
}

@Override
public List<ValidationError> validate(List<MarcSubfield> subfields, SpecificationFieldDto specificationFieldDto) {
if (!TAG_010.equals(specificationFieldDto.getTag())) {
return List.of();
}

var lccn = subfields.stream().filter(subfield -> subfield.code() == 'a').findFirst();

if (lccn.isEmpty()) {
return List.of();
}

if (LCCN_STRUCTURE_A_PATTERN.matcher(lccn.get().value()).matches()
|| LCCN_STRUCTURE_B_PATTERN.matcher(lccn.get().value()).matches()) {
return List.of();
}

var lccnSpecificationDto = specificationFieldDto.getSubfields().stream()
.filter(subfieldDto -> LCCN_SUBFIELD.equals(subfieldDto.getCode()))
.findFirst()
.orElse(null);

if (lccnSpecificationDto == null) {
return List.of();
}

return List.of(buildError(TAG_010, lccnSpecificationDto));
}

@Override
public SpecificationRuleCode supportedRule() {
return MarcRuleCode.INVALID_LCCN_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, definition.getCode());
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ public MarcRecordRuleValidator(TranslationProvider translationProvider) {
this.missingSubfieldValidator = new MissingSubfieldRuleValidator(translationProvider);
this.subfieldValidators = List.of(
new UndefinedSubfieldRuleValidator(translationProvider),
new NonRepeatableSubfieldRuleValidator(translationProvider));
new NonRepeatableSubfieldRuleValidator(translationProvider),
new InvalidLccnSubfieldRuleValidator(translationProvider));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public enum MarcRuleCode implements SpecificationRuleCode {
INVALID_INDICATOR("invalidIndicator"),
MISSING_SUBFIELD("missingSubfield"),
UNDEFINED_SUBFIELD("undefinedSubfield"),
NON_REPEATABLE_SUBFIELD("nonRepeatableSubfield");
NON_REPEATABLE_SUBFIELD("nonRepeatableSubfield"),
INVALID_LCCN_SUBFIELD("invalidLccnSubfieldValue");

private final String code;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.folio.rspec.validation.validator.marc.impl;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

import java.util.List;
import java.util.stream.Stream;
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.validation.validator.marc.model.MarcSubfield;
import org.folio.rspec.validation.validator.marc.model.Reference;
import org.folio.spring.testing.type.UnitTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.util.CollectionUtils;

@UnitTest
@ExtendWith(MockitoExtension.class)
public class InvalidLccnSubfieldRuleValidatorTest {

@Mock
private TranslationProvider translationProvider;
@InjectMocks
private InvalidLccnSubfieldRuleValidator validator;

@Test
void validate_whenNon010Field_shouldReturnEmptyList() {
var errors = validator.validate(List.of(), new SpecificationFieldDto().tag("tag"));

assertTrue(CollectionUtils.isEmpty(errors));
}

@Test
void validate_whenLccnSubfieldNotExist_shouldReturnEmptyList() {
var subfield = new MarcSubfield(Reference.forSubfield(Reference.forTag("010"), 'z'), "12345");

var errors = validator.validate(List.of(subfield), new SpecificationFieldDto().tag("010"));

assertTrue(CollectionUtils.isEmpty(errors));
}

@ParameterizedTest
@MethodSource("validLccn")
void validate_whenValidlccn_shouldReturnEmptyList(String lccn) {
var subfield = new MarcSubfield(Reference.forSubfield(Reference.forTag("010"), 'a'), lccn);

var errors = validator.validate(List.of(subfield), new SpecificationFieldDto().tag("010"));

assertTrue(CollectionUtils.isEmpty(errors));
}

@ParameterizedTest
@MethodSource("invalidLccn")
void validate_whenInvalidlccn_shouldReturnValidationError(String lccn) {
when(translationProvider.format(anyString(), anyString(), anyString())).thenReturn("message");
var subfield = new MarcSubfield(Reference.forSubfield(Reference.forTag("010"), 'a'), lccn);
var specification = new SpecificationFieldDto().tag("010")
.subfields(List.of(new SubfieldDto().code("a")));

var errors = validator.validate(List.of(subfield), specification);

assertEquals(1, errors.size());
ValidationError error = errors.get(0);
assertEquals(validator.definitionType(), error.getDefinitionType());
assertEquals(validator.severity(), error.getSeverity());
assertEquals(validator.supportedRule().getCode(), error.getRuleCode());
assertEquals("message", error.getMessage());
}

public static Stream<Arguments> validLccn() {
return Stream.of(
// structure A
arguments(" 12345678 "),
arguments("n 12345678 "),
arguments("nn 12345678 "),
arguments("nnn12345678 "),

// structure B
arguments(" 0123456789"),
arguments("n 0123456781"),
arguments("nn0123456789"));
}

public static Stream<Arguments> invalidLccn() {
return Stream.of(
// invalid structure A
arguments(" 12345678 "),
arguments(" 12345678 "),
arguments(" n 12345678 "),
arguments(" n12345678 "),
arguments(" 1234567 "),
arguments(" 12345678"),
arguments(" nnn123456789 "),
arguments("nnn123456789 "),
arguments("nnnn12345678 "),

// structure B
arguments(" 0123456789"),
arguments(" m123456789"),
arguments(" m123456789 "),
arguments(" mm123456789 "),
arguments("mm123456789 "),
arguments("mmm0123456789"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"ind2": " ",
"subfields": [
{
"a": "Both valid"
"a": "nn0123456789"
}
]
}
Expand Down Expand Up @@ -121,4 +121,4 @@
}
],
"leader": "01750ccm a2200421 4500"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"ind2": " ",
"subfields": [
{
"a": "First and Second must be #"
"a": "nnn12345678 "
}
]
}
Expand Down Expand Up @@ -132,4 +132,4 @@
}
],
"leader": "01750ccm a2200421 4500"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,4 @@
}
],
"leader": "01750ccm a2200421 4500"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,4 @@
}
],
"leader": "01750ccm a2200421 4500"
}
}
Loading
Loading