From 40c27773fe920491b6d359732c695122df2d7edc Mon Sep 17 00:00:00 2001 From: Ruslan Lavrov Date: Thu, 13 Aug 2026 22:54:39 +0300 Subject: [PATCH 1/2] Added support for DELETE action of mapping profile with UPDATE marc mapping option --- .../mapper/writer/marc/MarcRecordModifier.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifier.java b/src/main/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifier.java index 7d709afb..fbdc6a89 100644 --- a/src/main/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifier.java +++ b/src/main/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifier.java @@ -131,6 +131,10 @@ public void processUpdateMappingOption(List marcMappingRules) var notUpdatedDataFields = newLinkedList(incomingMarcRecord.getDataFields()); for (MarcMappingDetail detail : marcMappingRules) { + if (detail.getAction() == MarcMappingDetail.Action.DELETE) { + processDeleteAction(detail, true); + continue; + } String fieldTag = detail.getField().getField(); if (Verifier.isControlField(fieldTag)) { incomingMarcRecord.getControlFields().stream() @@ -451,6 +455,10 @@ private boolean isNumericalSortableField(VariableField field) { } private void processDeleteAction(MarcMappingDetail detail) { + processDeleteAction(detail, false); + } + + private void processDeleteAction(MarcMappingDetail detail, boolean applyProtection) { String fieldTag = detail.getField().getField(); char ind1 = isNotEmpty(detail.getField().getIndicator1()) ? detail.getField().getIndicator1().charAt(0) : BLANK_SUBFIELD_CODE; @@ -458,18 +466,21 @@ private void processDeleteAction(MarcMappingDetail detail) { isNotEmpty(detail.getField().getIndicator2()) ? detail.getField().getIndicator2().charAt(0) : BLANK_SUBFIELD_CODE; if (Verifier.isControlField(fieldTag)) { - for (VariableField field : marcRecordToChange.getVariableFields(fieldTag)) { - marcRecordToChange.removeVariableField(field); - } + marcRecordToChange.getVariableFields(fieldTag).stream() + .filter(field -> !applyProtection || isNotProtected((ControlField) field)) + .toList() + .forEach(marcRecordToChange::removeVariableField); } else if (detail.getField().getSubfields().getFirst().getSubfield().charAt(0) == ANY_CHAR) { marcRecordToChange.getDataFields().stream() .filter(field -> fieldMatches(field, fieldTag, ind1, ind2)) + .filter(field -> !applyProtection || isNotProtected(field)) .toList() .forEach(fieldToDelete -> marcRecordToChange.removeVariableField(fieldToDelete)); } else { char subfieldCode = detail.getField().getSubfields().getFirst().getSubfield().charAt(0); marcRecordToChange.getDataFields().stream() .filter(field -> fieldMatches(field, fieldTag, ind1, ind2)) + .filter(field -> !applyProtection || isNotProtected(field)) .map(targetField -> { targetField.removeSubfield(targetField.getSubfield(subfieldCode)); return targetField; From 66659f8938b996d3d11d50d9212c7b7f891b72d2 Mon Sep 17 00:00:00 2001 From: Ruslan Lavrov Date: Thu, 13 Aug 2026 23:03:53 +0300 Subject: [PATCH 2/2] Added tests --- .../writer/marc/MarcRecordModifier.java | 3 +- .../writer/marc/MarcRecordModifierTest.java | 227 ++++++++++++++++++ 2 files changed, 229 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifier.java b/src/main/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifier.java index fbdc6a89..48da3579 100644 --- a/src/main/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifier.java +++ b/src/main/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifier.java @@ -11,6 +11,7 @@ import static org.apache.commons.lang3.StringUtils.isNotEmpty; import static org.folio.processing.mapping.defaultmapper.processor.Processor.LDR_TAG; import static org.folio.rest.jaxrs.model.MappingDetail.MarcMappingOption.MODIFY; +import static org.folio.rest.jaxrs.model.MarcMappingDetail.Action.DELETE; import com.fasterxml.jackson.databind.ObjectMapper; import io.vertx.core.json.Json; @@ -131,7 +132,7 @@ public void processUpdateMappingOption(List marcMappingRules) var notUpdatedDataFields = newLinkedList(incomingMarcRecord.getDataFields()); for (MarcMappingDetail detail : marcMappingRules) { - if (detail.getAction() == MarcMappingDetail.Action.DELETE) { + if (detail.getAction() == DELETE) { processDeleteAction(detail, true); continue; } diff --git a/src/test/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifierTest.java b/src/test/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifierTest.java index 889ac0c0..f62a0723 100644 --- a/src/test/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifierTest.java +++ b/src/test/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifierTest.java @@ -2855,6 +2855,233 @@ void shouldRetainExistingRepeatableFieldWhenExistingIsProtectedAndHasNoIncomingF testUpdateRecord(incomingParsedContent, existingParsedContent, existingParsedContent, mappingParameters); } + // DELETE action during UPDATE mapping option tests + + @Test + void shouldDeleteAllFieldOccurrencesWhenSubfieldIsWildcardDuringUpdate() throws IOException { + String incomingParsedContent = + "{\"leader\":\"01314nam 22003851a 4500\",\"fields\":[{\"001\":\"ybp7406411\"}]}"; + String existingParsedContent = + "{\"leader\":\"01314nam 22003851a 4500\",\"fields\":[{\"001\":\"ybp7406411\"},{\"650\":{\"subfields\":" + + "[{\"a\":\"Biology\"},{\"x\":\"Study and teaching\"}],\"ind1\":\" \",\"ind2\":\"0\"}},{\"650\":" + + "{\"subfields\":[{\"a\":\"Zoology\"},{\"2\":\"fast\"}],\"ind1\":\" \",\"ind2\":\"7\"}}]}"; + + MarcMappingDetail deleteRule = new MarcMappingDetail() + .withOrder(0) + .withAction(MarcMappingDetail.Action.DELETE) + .withField(new MarcField() + .withField("650") + .withIndicator1("*") + .withIndicator2("*") + .withSubfields(List.of(new MarcSubfield().withSubfield("*")))); + + MappingProfile mappingProfile = new MappingProfile() + .withMappingDetails(new MappingDetail() + .withMarcMappingOption(UPDATE) + .withMarcMappingDetails(List.of(deleteRule))); + + String expectedParsedContent = + "{\"leader\":\"00049nam 22000371a 4500\",\"fields\":[{\"001\":\"ybp7406411\"}]}"; + testMarcUpdating(incomingParsedContent, existingParsedContent, expectedParsedContent, mappingProfile); + } + + @Test + void shouldDeleteOnlySubfieldFromFieldIfSameSubfieldSpecifiedInMappingDetailDuringUpdate() throws IOException { + String incomingParsedContent = + "{\"leader\":\"01314nam 22003851a 4500\",\"fields\":[{\"001\":\"ybp7406411\"}]}"; + String existingParsedContent = + "{\"leader\":\"01314nam 22003851a 4500\",\"fields\":[" + + "{\"001\":\"ybp7406411\"}," + + "{\"650\":{\"subfields\":[{\"a\":\"Biology\"},{\"x\":\"Study\"}],\"ind1\":\" \",\"ind2\":\"0\"}}," + + "{\"650\":{\"subfields\":[{\"a\":\"Zoology\"},{\"2\":\"fast\"}],\"ind1\":\" \",\"ind2\":\"7\"}}]}"; + + MarcMappingDetail deleteRule = new MarcMappingDetail() + .withOrder(0) + .withAction(MarcMappingDetail.Action.DELETE) + .withField(new MarcField() + .withField("650") + .withIndicator1(" ") + .withIndicator2("7") + .withSubfields(List.of(new MarcSubfield().withSubfield("2")))); + + MappingProfile mappingProfile = new MappingProfile() + .withMappingDetails(new MappingDetail() + .withMarcMappingOption(UPDATE) + .withMarcMappingDetails(List.of(deleteRule))); + + String expectedParsedContent = + "{\"leader\":\"00104nam 22000611a 4500\",\"fields\":[" + + "{\"001\":\"ybp7406411\"}," + + "{\"650\":{\"subfields\":[{\"a\":\"Biology\"},{\"x\":\"Study\"}],\"ind1\":\" \",\"ind2\":\"0\"}}," + + "{\"650\":{\"subfields\":[{\"a\":\"Zoology\"}],\"ind1\":\" \",\"ind2\":\"7\"}}]}"; + testMarcUpdating(incomingParsedContent, existingParsedContent, expectedParsedContent, mappingProfile); + } + + @Test + void shouldDeleteSpecifiedSubfieldFromAllFieldsIfSpecifiedTagIsWildcardDuringUpdate() throws IOException { + String incomingParsedContent = + "{\"leader\":\"01314nam 22003851a 4500\",\"fields\":[{\"001\":\"ybp7406411\"}]}"; + String existingParsedContent = + "{\"leader\":\"01314nam 22003851a 4500\",\"fields\":[" + + "{\"001\":\"ybp7406411\"}," + + "{\"650\":{\"subfields\":[{\"a\":\"Zoology\"},{\"2\":\"fast\"}],\"ind1\":\" \",\"ind2\":\"7\"}}," + + "{\"651\":{\"subfields\":[{\"a\":\"Marine biology\"},{\"2\":\"fast\"}],\"ind1\":\" \",\"ind2\":\"0\"}}]}"; + + MarcMappingDetail deleteRule = new MarcMappingDetail() + .withOrder(0) + .withAction(MarcMappingDetail.Action.DELETE) + .withField(new MarcField() + .withField("*") + .withIndicator1("*") + .withIndicator2("*") + .withSubfields(List.of(new MarcSubfield().withSubfield("2")))); + + MappingProfile mappingProfile = new MappingProfile() + .withMappingDetails(new MappingDetail() + .withMarcMappingOption(UPDATE) + .withMarcMappingDetails(List.of(deleteRule))); + + String expectedParsedContent = + "{\"leader\":\"00104nam 22000611a 4500\",\"fields\":[" + + "{\"001\":\"ybp7406411\"}," + + "{\"650\":{\"subfields\":[{\"a\":\"Zoology\"}],\"ind1\":\" \",\"ind2\":\"7\"}}," + + "{\"651\":{\"subfields\":[{\"a\":\"Marine biology\"}],\"ind1\":\" \",\"ind2\":\"0\"}}]}"; + testMarcUpdating(incomingParsedContent, existingParsedContent, expectedParsedContent, mappingProfile); + } + + @Test + void shouldNotDeleteProtectedFieldsIfSpecifiedSubfieldIsWildcardDuringUpdate() throws IOException { + String incomingParsedContent = + "{\"leader\":\"01314nam 22003851a 4500\",\"fields\":[{\"001\":\"ybp7406411\"}]}"; + String existingParsedContent = + "{\"leader\":\"00110nam 22000611a 4500\",\"fields\":[" + + "{\"001\":\"ybp7406411\"}," + + "{\"650\":{\"subfields\":[{\"a\":\"Biology\"},{\"x\":\"Study\"}],\"ind1\":\" \",\"ind2\":\"0\"}}," + + "{\"650\":{\"subfields\":[{\"a\":\"Zoology\"},{\"2\":\"fast\"}],\"ind1\":\" \",\"ind2\":\"7\"}}]}"; + + MarcMappingDetail deleteRule = new MarcMappingDetail() + .withOrder(0) + .withAction(MarcMappingDetail.Action.DELETE) + .withField(new MarcField() + .withField("650") + .withIndicator1("*") + .withIndicator2("*") + .withSubfields(List.of(new MarcSubfield().withSubfield("*")))); + + MappingProfile mappingProfile = new MappingProfile() + .withMappingDetails(new MappingDetail() + .withMarcMappingOption(UPDATE) + .withMarcMappingDetails(List.of(deleteRule))); + + List protectionSettings = List.of( + new MarcFieldProtectionSetting() + .withField("650") + .withIndicator1("*") + .withIndicator2("*") + .withSubfield("a") + .withData("*")); + + MappingParameters mappingParameters = new MappingParameters() + .withMarcFieldProtectionSettings(protectionSettings); + + testMarcUpdating(incomingParsedContent, existingParsedContent, existingParsedContent, mappingParameters, + mappingProfile); + } + + @Test + @SuppressWarnings("checkstyle:LineLength") + void shouldNotDeleteSubfieldFromProtectedFieldIfSameSubfieldSpecifiedInMappingDetailDuringUpdate() throws IOException { + String incomingParsedContent = + "{\"leader\":\"01314nam 22003851a 4500\",\"fields\":[{\"001\":\"ybp7406411\"}]}"; + String existingParsedContent = + "{\"leader\":\"00110nam 22000611a 4500\",\"fields\":[" + + "{\"001\":\"ybp7406411\"}," + + "{\"650\":{\"subfields\":[{\"a\":\"Biology\"},{\"x\":\"Study\"}],\"ind1\":\" \",\"ind2\":\"0\"}}," + + "{\"650\":{\"subfields\":[{\"a\":\"Zoology\"},{\"2\":\"fast\"}],\"ind1\":\" \",\"ind2\":\"7\"}}]}"; + + MarcMappingDetail deleteRule = new MarcMappingDetail() + .withOrder(0) + .withAction(MarcMappingDetail.Action.DELETE) + .withField(new MarcField() + .withField("650") + .withIndicator1(" ") + .withIndicator2("7") + .withSubfields(List.of(new MarcSubfield().withSubfield("2")))); + + MappingProfile mappingProfile = new MappingProfile() + .withMappingDetails(new MappingDetail() + .withMarcMappingOption(UPDATE) + .withMarcMappingDetails(List.of(deleteRule))); + + List protectionSettings = List.of( + new MarcFieldProtectionSetting() + .withField("650") + .withIndicator1("*") + .withIndicator2("7") + .withSubfield("*") + .withData("*")); + + MappingParameters mappingParameters = new MappingParameters() + .withMarcFieldProtectionSettings(protectionSettings); + + testMarcUpdating(incomingParsedContent, existingParsedContent, existingParsedContent, mappingParameters, + mappingProfile); + } + + @Test + void shouldDeleteSpecifiedControlFieldDuringUpdate() throws IOException { + String incomingParsedContent = + "{\"leader\":\"01314nam 22003851a 4500\",\"fields\":[{\"001\":\"ybp7406411\"}]}"; + String existingParsedContent = + "{\"leader\":\"00068nam 22000491a 4500\",\"fields\":[{\"001\":\"ybp7406411\"},{\"007\":\"vz|cza\"}]}"; + + MarcMappingDetail deleteRule = new MarcMappingDetail() + .withOrder(0) + .withAction(MarcMappingDetail.Action.DELETE) + .withField(new MarcField().withField("007")); + + MappingProfile mappingProfile = new MappingProfile() + .withMappingDetails(new MappingDetail() + .withMarcMappingOption(UPDATE) + .withMarcMappingDetails(List.of(deleteRule))); + + String expectedParsedContent = + "{\"leader\":\"00049nam 22000371a 4500\",\"fields\":[{\"001\":\"ybp7406411\"}]}"; + testMarcUpdating(incomingParsedContent, existingParsedContent, expectedParsedContent, mappingProfile); + } + + @Test + void shouldNotDeleteProtectedControlFieldDuringUpdate() throws IOException { + String incomingParsedContent = + "{\"leader\":\"01314nam 22003851a 4500\",\"fields\":[{\"001\":\"ybp7406411\"}]}"; + String existingParsedContent = + "{\"leader\":\"00068nam 22000491a 4500\",\"fields\":[{\"001\":\"ybp7406411\"},{\"007\":\"vz|cza\"}]}"; + + MarcMappingDetail deleteRule = new MarcMappingDetail() + .withOrder(0) + .withAction(MarcMappingDetail.Action.DELETE) + .withField(new MarcField().withField("007")); + + MappingProfile mappingProfile = new MappingProfile() + .withMappingDetails(new MappingDetail() + .withMarcMappingOption(UPDATE) + .withMarcMappingDetails(List.of(deleteRule))); + + List protectionSettings = List.of( + new MarcFieldProtectionSetting() + .withField("007") + .withIndicator1("") + .withIndicator2("") + .withSubfield("") + .withData("*")); + + MappingParameters mappingParameters = new MappingParameters() + .withMarcFieldProtectionSettings(protectionSettings); + + testMarcUpdating(incomingParsedContent, existingParsedContent, existingParsedContent, mappingParameters, + mappingProfile); + } + @Test void shouldReturnTrueWhenFieldTagAnyOf1xxFields() { for (int i = 100; i < 200; i++) {