From 80c70fc36451bb2e28a892cdb12bae8e0d20a27f Mon Sep 17 00:00:00 2001 From: siarhei-charniak Date: Wed, 15 Nov 2023 10:38:01 +0300 Subject: [PATCH] MODBULKOPS-144 - Holdings records - electronic access updates (#150) --- .../domain/bean/ElectronicAccessEntity.java | 7 + .../bulkops/domain/bean/HoldingsRecord.java | 2 +- .../org/folio/bulkops/domain/bean/Item.java | 2 +- .../ElectronicAccessUpdaterFactory.java | 125 ++++++++++ .../processor/HoldingsDataProcessor.java | 67 +++++- .../service/ElectronicAccessService.java | 11 + .../db/changelog/changelog-master.xml | 1 + ...23_add_electronic_access_options_types.sql | 5 + ...23_add_electronic_access_options_types.xml | 13 + .../schemas/update_option_type.json | 7 +- .../processor/HoldingsDataProcessorTest.java | 222 ++++++++++++++++-- 11 files changed, 432 insertions(+), 30 deletions(-) create mode 100644 src/main/java/org/folio/bulkops/domain/bean/ElectronicAccessEntity.java create mode 100644 src/main/java/org/folio/bulkops/processor/ElectronicAccessUpdaterFactory.java create mode 100644 src/main/resources/db/changelog/changes/08-11-2023_add_electronic_access_options_types.sql create mode 100644 src/main/resources/db/changelog/changes/08-11-2023_add_electronic_access_options_types.xml diff --git a/src/main/java/org/folio/bulkops/domain/bean/ElectronicAccessEntity.java b/src/main/java/org/folio/bulkops/domain/bean/ElectronicAccessEntity.java new file mode 100644 index 00000000..9df0ef22 --- /dev/null +++ b/src/main/java/org/folio/bulkops/domain/bean/ElectronicAccessEntity.java @@ -0,0 +1,7 @@ +package org.folio.bulkops.domain.bean; + +import java.util.List; + +public interface ElectronicAccessEntity { + List getElectronicAccess(); +} diff --git a/src/main/java/org/folio/bulkops/domain/bean/HoldingsRecord.java b/src/main/java/org/folio/bulkops/domain/bean/HoldingsRecord.java index 21d1d322..dd6be9a1 100644 --- a/src/main/java/org/folio/bulkops/domain/bean/HoldingsRecord.java +++ b/src/main/java/org/folio/bulkops/domain/bean/HoldingsRecord.java @@ -42,7 +42,7 @@ @AllArgsConstructor @JsonTypeName("holdingsRecord") @EqualsAndHashCode(exclude = {"metadata", "instanceId", "permanentLocation", "effectiveLocationId", "illPolicy", "instanceHrid", "itemBarcode"}) -public class HoldingsRecord implements BulkOperationsEntity { +public class HoldingsRecord implements BulkOperationsEntity, ElectronicAccessEntity { @JsonProperty("id") @CsvCustomBindByName(column = "Holdings record id", converter = StringConverter.class) diff --git a/src/main/java/org/folio/bulkops/domain/bean/Item.java b/src/main/java/org/folio/bulkops/domain/bean/Item.java index ca3e42df..28a00ad5 100644 --- a/src/main/java/org/folio/bulkops/domain/bean/Item.java +++ b/src/main/java/org/folio/bulkops/domain/bean/Item.java @@ -49,7 +49,7 @@ @AllArgsConstructor @JsonTypeName("item") @EqualsAndHashCode(exclude = {"metadata", "effectiveCallNumberComponents", "effectiveLocation", "boundWithTitles"}) -public class Item implements BulkOperationsEntity { +public class Item implements BulkOperationsEntity, ElectronicAccessEntity { @JsonProperty("id") @CsvCustomBindByName(column = "Item id", converter = StringConverter.class) @CsvCustomBindByPosition(position = 0, converter = StringConverter.class) diff --git a/src/main/java/org/folio/bulkops/processor/ElectronicAccessUpdaterFactory.java b/src/main/java/org/folio/bulkops/processor/ElectronicAccessUpdaterFactory.java new file mode 100644 index 00000000..1a562f19 --- /dev/null +++ b/src/main/java/org/folio/bulkops/processor/ElectronicAccessUpdaterFactory.java @@ -0,0 +1,125 @@ +package org.folio.bulkops.processor; + +import static java.lang.String.format; +import static java.util.Objects.isNull; +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.apache.commons.lang3.StringUtils.equalsIgnoreCase; + +import org.folio.bulkops.domain.bean.ElectronicAccessEntity; +import org.folio.bulkops.domain.dto.Action; +import org.folio.bulkops.domain.dto.UpdateOptionType; +import org.folio.bulkops.exception.BulkOperationException; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +@Component +public class ElectronicAccessUpdaterFactory { + public Updater updater(UpdateOptionType option, Action action) { + return switch (option) { + case ELECTRONIC_ACCESS_URL_RELATIONSHIP -> updateUrlRelationship(option, action); + case ELECTRONIC_ACCESS_URI -> updateUri(option, action); + case ELECTRONIC_ACCESS_LINK_TEXT -> updateLinkText(option, action); + case ELECTRONIC_ACCESS_MATERIALS_SPECIFIED -> updateMaterialsSpecified(option, action); + case ELECTRONIC_ACCESS_URL_PUBLIC_NOTE -> updatePublicNote(option, action); + default -> notSupported(option, action); + }; + } + + private Updater updateUrlRelationship(UpdateOptionType option, Action action) { + return switch (action.getType()) { + case CLEAR_FIELD -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.forEach(electronicAccess -> electronicAccess.setRelationshipId(null))); + case FIND_AND_REMOVE_THESE -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.stream() + .filter(electronicAccess -> equalsIgnoreCase(electronicAccess.getRelationshipId(), action.getInitial())) + .forEach(electronicAccess -> electronicAccess.setRelationshipId(null))); + case FIND_AND_REPLACE -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.stream() + .filter(electronicAccess -> equalsIgnoreCase(electronicAccess.getRelationshipId(), action.getInitial())) + .forEach(electronicAccess -> electronicAccess.setRelationshipId(action.getUpdated()))); + case REPLACE_WITH -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.forEach(electronicAccess -> electronicAccess.setRelationshipId(action.getUpdated()))); + default -> notSupported(option, action); + }; + } + + private Updater updateUri(UpdateOptionType option, Action action) { + return switch (action.getType()) { + case CLEAR_FIELD -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.forEach(electronicAccess -> electronicAccess.setUri(EMPTY))); + case FIND_AND_REMOVE_THESE -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.stream() + .filter(electronicAccess -> Objects.equals(electronicAccess.getUri(), action.getInitial())) + .forEach(electronicAccess -> electronicAccess.setUri(EMPTY))); + case FIND_AND_REPLACE -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.stream() + .filter(electronicAccess -> Objects.equals(electronicAccess.getUri(), action.getInitial())) + .forEach(electronicAccess -> electronicAccess.setUri(isNull(action.getUpdated()) ? EMPTY : action.getUpdated()))); + case REPLACE_WITH -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.forEach(electronicAccess -> electronicAccess.setUri(isNull(action.getUpdated()) ? EMPTY : action.getUpdated()))); + default -> notSupported(option, action); + }; + } + + private Updater updateLinkText(UpdateOptionType option, Action action) { + return switch (action.getType()) { + case CLEAR_FIELD -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.forEach(electronicAccess -> electronicAccess.setLinkText(null))); + case FIND_AND_REMOVE_THESE -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.stream() + .filter(electronicAccess -> Objects.equals(electronicAccess.getLinkText(), action.getInitial())) + .forEach(electronicAccess -> electronicAccess.setLinkText(null))); + case FIND_AND_REPLACE -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.stream() + .filter(electronicAccess -> Objects.equals(electronicAccess.getLinkText(), action.getInitial())) + .forEach(electronicAccess -> electronicAccess.setLinkText(action.getUpdated()))); + case REPLACE_WITH -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.forEach(electronicAccess -> electronicAccess.setLinkText(action.getUpdated()))); + default -> notSupported(option, action); + }; + } + + private Updater updateMaterialsSpecified(UpdateOptionType option, Action action) { + return switch (action.getType()) { + case CLEAR_FIELD -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.forEach(electronicAccess -> electronicAccess.setMaterialsSpecification(null))); + case FIND_AND_REMOVE_THESE -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.stream() + .filter(electronicAccess -> Objects.equals(electronicAccess.getMaterialsSpecification(), action.getInitial())) + .forEach(electronicAccess -> electronicAccess.setMaterialsSpecification(null))); + case FIND_AND_REPLACE -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.stream() + .filter(electronicAccess -> Objects.equals(electronicAccess.getMaterialsSpecification(), action.getInitial())) + .forEach(electronicAccess -> electronicAccess.setMaterialsSpecification(action.getUpdated()))); + case REPLACE_WITH -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.forEach(electronicAccess -> electronicAccess.setMaterialsSpecification(action.getUpdated()))); + default -> notSupported(option, action); + }; + } + + private Updater updatePublicNote(UpdateOptionType option, Action action) { + return switch (action.getType()) { + case CLEAR_FIELD -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.forEach(electronicAccess -> electronicAccess.setPublicNote(null))); + case FIND_AND_REMOVE_THESE -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.stream() + .filter(electronicAccess -> Objects.equals(electronicAccess.getPublicNote(), action.getInitial())) + .forEach(electronicAccess -> electronicAccess.setPublicNote(null))); + case FIND_AND_REPLACE -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.stream() + .filter(electronicAccess -> Objects.equals(electronicAccess.getPublicNote(), action.getInitial())) + .forEach(electronicAccess -> electronicAccess.setPublicNote(action.getUpdated()))); + case REPLACE_WITH -> electronicAccessEntity -> ofNullable(electronicAccessEntity.getElectronicAccess()) + .ifPresent(list -> list.forEach(electronicAccess -> electronicAccess.setPublicNote(action.getUpdated()))); + default -> notSupported(option, action); + }; + } + + private Updater notSupported(UpdateOptionType option, Action action) { + return electronicAccessEntity -> { + throw new BulkOperationException(format("Combination %s and %s isn't supported yet", option, action.getType())); + }; + } +} diff --git a/src/main/java/org/folio/bulkops/processor/HoldingsDataProcessor.java b/src/main/java/org/folio/bulkops/processor/HoldingsDataProcessor.java index 62427f0c..21ad6430 100644 --- a/src/main/java/org/folio/bulkops/processor/HoldingsDataProcessor.java +++ b/src/main/java/org/folio/bulkops/processor/HoldingsDataProcessor.java @@ -8,11 +8,18 @@ import static org.folio.bulkops.domain.dto.UpdateActionType.SET_TO_FALSE_INCLUDING_ITEMS; import static org.folio.bulkops.domain.dto.UpdateActionType.SET_TO_TRUE; import static org.folio.bulkops.domain.dto.UpdateActionType.SET_TO_TRUE_INCLUDING_ITEMS; +import static org.folio.bulkops.domain.dto.UpdateOptionType.ELECTRONIC_ACCESS_LINK_TEXT; +import static org.folio.bulkops.domain.dto.UpdateOptionType.ELECTRONIC_ACCESS_MATERIALS_SPECIFIED; +import static org.folio.bulkops.domain.dto.UpdateOptionType.ELECTRONIC_ACCESS_URI; +import static org.folio.bulkops.domain.dto.UpdateOptionType.ELECTRONIC_ACCESS_URL_PUBLIC_NOTE; +import static org.folio.bulkops.domain.dto.UpdateOptionType.ELECTRONIC_ACCESS_URL_RELATIONSHIP; import static org.folio.bulkops.domain.dto.UpdateOptionType.PERMANENT_LOCATION; import static org.folio.bulkops.domain.dto.UpdateOptionType.SUPPRESS_FROM_DISCOVERY; +import static org.folio.bulkops.domain.dto.UpdateOptionType.TEMPORARY_LOCATION; import java.util.ArrayList; import java.util.Objects; +import java.util.Set; import java.util.regex.Pattern; import org.folio.bulkops.domain.bean.HoldingsRecord; @@ -22,6 +29,7 @@ import org.folio.bulkops.exception.BulkOperationException; import org.folio.bulkops.exception.NotFoundException; import org.folio.bulkops.exception.RuleValidationException; +import org.folio.bulkops.service.ElectronicAccessService; import org.folio.bulkops.service.HoldingsReferenceService; import org.folio.bulkops.service.ItemReferenceService; import org.springframework.stereotype.Component; @@ -38,6 +46,8 @@ public class HoldingsDataProcessor extends AbstractDataProcessor private final ItemReferenceService itemReferenceService; private final HoldingsReferenceService holdingsReferenceService; private final HoldingsNotesUpdater holdingsNotesUpdater; + private final ElectronicAccessUpdaterFactory electronicAccessUpdaterFactory; + private final ElectronicAccessService electronicAccessService; private static final Pattern UUID_REGEX = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); @@ -53,18 +63,7 @@ public Validator validator(HoldingsRecord entity) { log.error("Holdings source was not found by id={}", entity.getSourceId()); } if (REPLACE_WITH == action.getType()) { - var locationId = action.getUpdated(); - if (isEmpty(locationId)) { - throw new RuleValidationException("Location id cannot be empty"); - } - if (!UUID_REGEX.matcher(locationId).matches()) { - throw new RuleValidationException("Location id has invalid format: %s" + locationId); - } - try { - itemReferenceService.getLocationById(locationId); - } catch (Exception e) { - throw new RuleValidationException(format("Location %s doesn't exist", locationId)); - } + validateReplacement(option, action); } if (PERMANENT_LOCATION == option && CLEAR_FIELD == action.getType()) { throw new RuleValidationException("Permanent location cannot be cleared"); @@ -73,7 +72,9 @@ public Validator validator(HoldingsRecord entity) { } public Updater updater(UpdateOptionType option, Action action) { - if (REPLACE_WITH == action.getType()) { + if (isElectronicAccessUpdate(option)) { + return (Updater) electronicAccessUpdaterFactory.updater(option, action); + } else if (REPLACE_WITH == action.getType()) { return holding -> { var locationId = action.getUpdated(); if (PERMANENT_LOCATION == option) { @@ -102,6 +103,46 @@ public Updater updater(UpdateOptionType option, Action action) { }; } + private void validateReplacement(UpdateOptionType option, Action action) throws RuleValidationException { + if (isIdValue(option)) { + var newId = action.getUpdated(); + if (isEmpty(newId)) { + throw new RuleValidationException("Id value cannot be empty"); + } + if (!UUID_REGEX.matcher(action.getUpdated()).matches()) { + throw new RuleValidationException("UUID has invalid format: %s" + newId); + } + + if (Set.of(PERMANENT_LOCATION, TEMPORARY_LOCATION).contains(option)) { + try { + itemReferenceService.getLocationById(newId); + } catch (Exception e) { + throw new RuleValidationException(format("Location %s doesn't exist", newId)); + } + } else if (ELECTRONIC_ACCESS_URL_RELATIONSHIP.equals(option)) { + try { + electronicAccessService.getRelationshipNameAndIdById(newId); + } catch (Exception e) { + throw new RuleValidationException(format("URL relationship %s doesn't exist", newId)); + } + } + } + } + + private boolean isIdValue(UpdateOptionType option) { + return Set.of(PERMANENT_LOCATION, + TEMPORARY_LOCATION, + ELECTRONIC_ACCESS_URL_RELATIONSHIP).contains(option); + } + + private boolean isElectronicAccessUpdate(UpdateOptionType option) { + return Set.of(ELECTRONIC_ACCESS_URL_RELATIONSHIP, + ELECTRONIC_ACCESS_URI, + ELECTRONIC_ACCESS_LINK_TEXT, + ELECTRONIC_ACCESS_MATERIALS_SPECIFIED, + ELECTRONIC_ACCESS_URL_PUBLIC_NOTE).contains(option); + } + private boolean isSetDiscoverySuppressTrue(UpdateActionType actionType, UpdateOptionType optionType) { return (actionType == SET_TO_TRUE || actionType == SET_TO_TRUE_INCLUDING_ITEMS) && optionType == SUPPRESS_FROM_DISCOVERY; } diff --git a/src/main/java/org/folio/bulkops/service/ElectronicAccessService.java b/src/main/java/org/folio/bulkops/service/ElectronicAccessService.java index a5f1cbc4..f83bef08 100644 --- a/src/main/java/org/folio/bulkops/service/ElectronicAccessService.java +++ b/src/main/java/org/folio/bulkops/service/ElectronicAccessService.java @@ -1,5 +1,6 @@ package org.folio.bulkops.service; +import static java.lang.String.format; import static java.util.Objects.isNull; import static org.apache.commons.lang3.ObjectUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.EMPTY; @@ -10,6 +11,7 @@ import org.folio.bulkops.client.ElectronicAccessRelationshipClient; import org.folio.bulkops.domain.bean.ElectronicAccess; +import org.folio.bulkops.domain.bean.ElectronicAccessRelationship; import org.folio.bulkops.exception.EntityFormatException; import org.folio.bulkops.exception.NotFoundException; import org.springframework.cache.annotation.Cacheable; @@ -50,6 +52,15 @@ public String getRelationshipNameAndIdById(String id) { } } + @Cacheable(cacheNames = "electronicAccessRelationships") + public ElectronicAccessRelationship getRelationshipById(String id) { + try { + return relationshipClient.getById(id); + } catch (Exception e) { + throw new NotFoundException(format("URL relationship was not found by id=%s", id)); + } + } + public ElectronicAccess restoreElectronicAccessItem(String s) { if (isNotEmpty(s)) { var tokens = s.split(ARRAY_DELIMITER, -1); diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml index 21856053..2c584a12 100644 --- a/src/main/resources/db/changelog/changelog-master.xml +++ b/src/main/resources/db/changelog/changelog-master.xml @@ -19,4 +19,5 @@ + diff --git a/src/main/resources/db/changelog/changes/08-11-2023_add_electronic_access_options_types.sql b/src/main/resources/db/changelog/changes/08-11-2023_add_electronic_access_options_types.sql new file mode 100644 index 00000000..6cafeb91 --- /dev/null +++ b/src/main/resources/db/changelog/changes/08-11-2023_add_electronic_access_options_types.sql @@ -0,0 +1,5 @@ +ALTER TYPE UpdateOptionType ADD VALUE IF NOT EXISTS 'ELECTRONIC_ACCESS_URL_RELATIONSHIP'; +ALTER TYPE UpdateOptionType ADD VALUE IF NOT EXISTS 'ELECTRONIC_ACCESS_URI'; +ALTER TYPE UpdateOptionType ADD VALUE IF NOT EXISTS 'ELECTRONIC_ACCESS_LINK_TEXT'; +ALTER TYPE UpdateOptionType ADD VALUE IF NOT EXISTS 'ELECTRONIC_ACCESS_MATERIALS_SPECIFIED'; +ALTER TYPE UpdateOptionType ADD VALUE IF NOT EXISTS 'ELECTRONIC_ACCESS_URL_PUBLIC_NOTE'; diff --git a/src/main/resources/db/changelog/changes/08-11-2023_add_electronic_access_options_types.xml b/src/main/resources/db/changelog/changes/08-11-2023_add_electronic_access_options_types.xml new file mode 100644 index 00000000..f3e78eb3 --- /dev/null +++ b/src/main/resources/db/changelog/changes/08-11-2023_add_electronic_access_options_types.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/main/resources/swagger.api/schemas/update_option_type.json b/src/main/resources/swagger.api/schemas/update_option_type.json index 3ad8f273..ee2f8f96 100644 --- a/src/main/resources/swagger.api/schemas/update_option_type.json +++ b/src/main/resources/swagger.api/schemas/update_option_type.json @@ -17,7 +17,12 @@ "ADMINISTRATIVE_NOTE", "CHECK_IN_NOTE", "CHECK_OUT_NOTE", - "HOLDINGS_NOTE" + "HOLDINGS_NOTE", + "ELECTRONIC_ACCESS_URL_RELATIONSHIP", + "ELECTRONIC_ACCESS_URI", + "ELECTRONIC_ACCESS_LINK_TEXT", + "ELECTRONIC_ACCESS_MATERIALS_SPECIFIED", + "ELECTRONIC_ACCESS_URL_PUBLIC_NOTE" ] } } diff --git a/src/test/java/org/folio/bulkops/processor/HoldingsDataProcessorTest.java b/src/test/java/org/folio/bulkops/processor/HoldingsDataProcessorTest.java index 3504ad6b..4cf4495d 100644 --- a/src/test/java/org/folio/bulkops/processor/HoldingsDataProcessorTest.java +++ b/src/test/java/org/folio/bulkops/processor/HoldingsDataProcessorTest.java @@ -1,6 +1,7 @@ package org.folio.bulkops.processor; import static java.util.Objects.isNull; +import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.folio.bulkops.domain.dto.UpdateActionType.ADD_TO_EXISTING; import static org.folio.bulkops.domain.dto.UpdateActionType.CHANGE_TYPE; import static org.folio.bulkops.domain.dto.UpdateActionType.CLEAR_FIELD; @@ -15,6 +16,7 @@ import static org.folio.bulkops.domain.dto.UpdateActionType.SET_TO_TRUE; import static org.folio.bulkops.domain.dto.UpdateActionType.SET_TO_TRUE_INCLUDING_ITEMS; import static org.folio.bulkops.domain.dto.UpdateOptionType.ADMINISTRATIVE_NOTE; +import static org.folio.bulkops.domain.dto.UpdateOptionType.ELECTRONIC_ACCESS_URL_RELATIONSHIP; import static org.folio.bulkops.domain.dto.UpdateOptionType.EMAIL_ADDRESS; import static org.folio.bulkops.domain.dto.UpdateOptionType.HOLDINGS_NOTE; import static org.folio.bulkops.domain.dto.UpdateOptionType.PERMANENT_LOCATION; @@ -25,7 +27,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; @@ -34,17 +39,26 @@ import lombok.SneakyThrows; import org.folio.bulkops.BaseTest; +import org.folio.bulkops.domain.bean.ElectronicAccess; import org.folio.bulkops.domain.bean.HoldingsNote; import org.folio.bulkops.domain.bean.HoldingsRecord; import org.folio.bulkops.domain.bean.HoldingsRecordsSource; import org.folio.bulkops.domain.bean.ItemLocation; import org.folio.bulkops.domain.dto.Action; import org.folio.bulkops.domain.dto.Parameter; +import org.folio.bulkops.domain.dto.UpdateOptionType; import org.folio.bulkops.exception.NotFoundException; +import org.folio.bulkops.exception.RuleValidationException; import org.folio.bulkops.repository.BulkOperationExecutionContentRepository; +import org.folio.bulkops.service.ElectronicAccessService; import org.folio.bulkops.service.ErrorService; +import org.folio.bulkops.service.HoldingsReferenceService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; @@ -59,6 +73,8 @@ class HoldingsDataProcessorTest extends BaseTest { DataProcessorFactory factory; @MockBean ErrorService errorService; + @MockBean + ElectronicAccessService electronicAccessService; private DataProcessor processor; @@ -258,7 +274,7 @@ void testUpdaterForSuppressFromDiscoveryOption() { .withId(UUID.randomUUID().toString()) .withDiscoverySuppress(false); - var processor = new HoldingsDataProcessor(null, null, null); + var processor = new HoldingsDataProcessor(null, null, null, null, null); processor.updater(SUPPRESS_FROM_DISCOVERY, new Action().type(SET_TO_TRUE)).apply(holdingsRecord); assertTrue(holdingsRecord.getDiscoverySuppress()); @@ -278,7 +294,7 @@ void testUpdateMarkAsStaffOnlyForHoldingsNotes() { var parameter = new Parameter(); parameter.setKey(HOLDINGS_NOTE_TYPE_ID_KEY); parameter.setValue("typeId"); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(HOLDINGS_NOTE, new Action().type(MARK_AS_STAFF_ONLY).parameters(List.of(parameter))).apply(holding); @@ -293,7 +309,7 @@ void testUpdateRemoveMarkAsStaffOnlyForHoldingsNotes() { var parameter = new Parameter(); parameter.setKey(HOLDINGS_NOTE_TYPE_ID_KEY); parameter.setValue("typeId"); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(HOLDINGS_NOTE, new Action().type(REMOVE_MARK_AS_STAFF_ONLY).parameters(List.of(parameter))).apply(holding); @@ -305,7 +321,7 @@ void testUpdateRemoveMarkAsStaffOnlyForHoldingsNotes() { void testRemoveAdministrativeNotes() { var administrativeNote = "administrative note"; var holding = new HoldingsRecord().withAdministrativeNotes(List.of(administrativeNote)); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(ADMINISTRATIVE_NOTE, new Action().type(REMOVE_ALL)).apply(holding); assertTrue(holding.getAdministrativeNotes().isEmpty()); @@ -320,7 +336,7 @@ void testRemoveHoldingsNotes() { var parameter = new Parameter(); parameter.setKey(HOLDINGS_NOTE_TYPE_ID_KEY); parameter.setValue("typeId1"); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(HOLDINGS_NOTE, new Action().type(REMOVE_ALL).parameters(List.of(parameter))).apply(holding); assertEquals(1, holding.getNotes().size()); @@ -333,7 +349,7 @@ void testAddAdministrativeNotes() { var administrativeNote1 = "administrative note"; var administrativeNote2 = "administrative note 2"; var holding = new HoldingsRecord(); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(ADMINISTRATIVE_NOTE, new Action().type(ADD_TO_EXISTING).updated(administrativeNote1)).apply(holding); assertEquals(1, holding.getAdministrativeNotes().size()); @@ -353,7 +369,7 @@ void testAddHoldingsNotes() { parameter.setKey(HOLDINGS_NOTE_TYPE_ID_KEY); parameter.setValue("typeId1"); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(HOLDINGS_NOTE, new Action().type(ADD_TO_EXISTING).parameters(List.of(parameter)).updated(note1)).apply(holding); @@ -375,7 +391,7 @@ void testFindAndRemoveForAdministrativeNotes() { var administrativeNote1 = "administrative note 1"; var administrativeNote2 = "administrative note 2"; var holding = new HoldingsRecord().withAdministrativeNotes(new ArrayList<>(List.of(administrativeNote1, administrativeNote2))); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("administrative note")).apply(holding); assertEquals(2, holding.getAdministrativeNotes().size()); @@ -395,7 +411,7 @@ void testFindAndRemoveHoldingsNotes() { parameter.setKey(HOLDINGS_NOTE_TYPE_ID_KEY); parameter.setValue("typeId1"); var holding = new HoldingsRecord().withNotes(List.of(note1, note2, note3)); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(HOLDINGS_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("note") .parameters(List.of(parameter))).apply(holding); @@ -415,7 +431,7 @@ void testFindAndReplaceForAdministrativeNotes() { var administrativeNote2 = "administrative note 2"; var administrativeNote3 = "administrative note 3"; var holding = new HoldingsRecord().withAdministrativeNotes(new ArrayList<>(List.of(administrativeNote1, administrativeNote2))); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REPLACE) .initial(administrativeNote1).updated(administrativeNote3)).apply(holding); @@ -433,7 +449,7 @@ void testFindAndReplaceForHoldingNotes() { parameter.setKey(HOLDINGS_NOTE_TYPE_ID_KEY); parameter.setValue("typeId1"); var holding = new HoldingsRecord().withNotes(List.of(note1, note2)); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(HOLDINGS_NOTE, new Action().type(FIND_AND_REPLACE).parameters(List.of(parameter)) .initial("note1").updated("note3")).apply(holding); @@ -447,7 +463,7 @@ void testFindAndReplaceForHoldingNotes() { void testChangeTypeForAdministrativeNotes() { var administrativeNote = "note"; var holding = new HoldingsRecord().withAdministrativeNotes(new ArrayList<>(List.of(administrativeNote))); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(ADMINISTRATIVE_NOTE, new Action().type(CHANGE_TYPE) .updated("typeId")).apply(holding); @@ -465,7 +481,7 @@ void testChangeNoteTypeForHoldingsNotes() { parameter.setKey(HOLDINGS_NOTE_TYPE_ID_KEY); parameter.setValue("typeId1"); var holding = new HoldingsRecord().withNotes(List.of(note1, note2)); - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); processor.updater(HOLDINGS_NOTE, new Action().type(CHANGE_TYPE).updated(ADMINISTRATIVE_NOTE.getValue()).parameters(List.of(parameter))).apply(holding); @@ -488,7 +504,7 @@ void testChangeNoteTypeForHoldingsNotes() { @Test @SneakyThrows void testClone() { - var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater())); + var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null); var administrativeNotes = new ArrayList(); administrativeNotes.add("note1"); var holding1 = new HoldingsRecord().withId("id") @@ -516,4 +532,182 @@ void testClone() { assertFalse(processor.compare(holding1, holding2)); } + + @ParameterizedTest + @NullSource + @ValueSource(strings = { "invalid-uuid", "aa76d5d9-4c0f-4895-84f4-56d3266941d1" }) + void shouldNotUpdateInvalidElectronicAccessUrlRelationship(String id) { + doThrow(new NotFoundException("not found")).when(electronicAccessService).getRelationshipById("aa76d5d9-4c0f-4895-84f4-56d3266941d1"); + var holdingsReferenceService = mock(HoldingsReferenceService.class); + when(holdingsReferenceService.getSourceById(MARC_SOURCE_ID)).thenReturn(new HoldingsRecordsSource().withName("MARC")); + var processor = new HoldingsDataProcessor(null, holdingsReferenceService, null, null, electronicAccessService); + + var holdingsRecord = new HoldingsRecord().withSourceId(MARC_SOURCE_ID); + var action = new Action().type(REPLACE_WITH).updated(id); + + assertThrows(RuleValidationException.class, () -> processor.validator(holdingsRecord).validate(ELECTRONIC_ACCESS_URL_RELATIONSHIP, action)); + } + + @ParameterizedTest + @ValueSource(strings = { + "ELECTRONIC_ACCESS_URL_RELATIONSHIP", + "ELECTRONIC_ACCESS_URI", + "ELECTRONIC_ACCESS_LINK_TEXT", + "ELECTRONIC_ACCESS_MATERIALS_SPECIFIED", + "ELECTRONIC_ACCESS_URL_PUBLIC_NOTE" + }) + @SneakyThrows + void shouldClearElectronicAccessFields(UpdateOptionType option) { + var holdingsRecord = buildHoldingsWithElectronicAccess(); + + var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null); + var action = new Action().type(CLEAR_FIELD); + + processor.updater(option, action).apply(holdingsRecord); + + var electronicAccess = holdingsRecord.getElectronicAccess().get(0); + switch (option) { + case ELECTRONIC_ACCESS_URL_RELATIONSHIP -> assertNull(electronicAccess.getRelationshipId()); + case ELECTRONIC_ACCESS_URI -> assertEquals(EMPTY, electronicAccess.getUri()); + case ELECTRONIC_ACCESS_LINK_TEXT -> assertNull(electronicAccess.getLinkText()); + case ELECTRONIC_ACCESS_MATERIALS_SPECIFIED -> assertNull(electronicAccess.getMaterialsSpecification()); + case ELECTRONIC_ACCESS_URL_PUBLIC_NOTE -> assertNull(electronicAccess.getPublicNote()); + } + } + + @ParameterizedTest + @CsvSource(textBlock = """ + ELECTRONIC_ACCESS_URL_RELATIONSHIP | 2510A1D1-A61C-4378-8886-B831004F018E + ELECTRONIC_ACCESS_URI | http://example.org + ELECTRONIC_ACCESS_LINK_TEXT | link text + ELECTRONIC_ACCESS_MATERIALS_SPECIFIED | materials + ELECTRONIC_ACCESS_URL_PUBLIC_NOTE | public note + """, delimiter = '|') + @SneakyThrows + void shouldFindAndClearExactlyMatchedElectronicAccessFields(UpdateOptionType option, String value) { + var holdingsRecord = buildHoldingsWithElectronicAccess(); + + var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null); + var action = new Action().type(FIND_AND_REMOVE_THESE).initial(value); + + processor.updater(option, action).apply(holdingsRecord); + + var modified = holdingsRecord.getElectronicAccess().get(0); + var unmodified = holdingsRecord.getElectronicAccess().get(1); + var initialElectronicAccess = buildHoldingsWithElectronicAccess().getElectronicAccess().get(1); + + switch (option) { + case ELECTRONIC_ACCESS_URL_RELATIONSHIP -> { + assertNull(modified.getRelationshipId()); + assertEquals(initialElectronicAccess.getRelationshipId(), unmodified.getRelationshipId()); + } + case ELECTRONIC_ACCESS_URI -> { + assertEquals(EMPTY, modified.getUri()); + assertEquals(initialElectronicAccess.getUri(), unmodified.getUri()); + } + case ELECTRONIC_ACCESS_LINK_TEXT -> { + assertNull(modified.getLinkText()); + assertEquals(initialElectronicAccess.getLinkText(), unmodified.getLinkText()); + } + case ELECTRONIC_ACCESS_MATERIALS_SPECIFIED -> { + assertNull(modified.getMaterialsSpecification()); + assertEquals(initialElectronicAccess.getMaterialsSpecification(), unmodified.getMaterialsSpecification()); + } + case ELECTRONIC_ACCESS_URL_PUBLIC_NOTE -> { + assertNull(modified.getPublicNote()); + assertEquals(initialElectronicAccess.getPublicNote(), unmodified.getPublicNote()); + } + } + } + + @ParameterizedTest + @CsvSource(textBlock = """ + ELECTRONIC_ACCESS_URL_RELATIONSHIP | fc34ddc0-0cfc-40b3-8e1a-bade16b33e5b + ELECTRONIC_ACCESS_URI | http://replaced.org + ELECTRONIC_ACCESS_LINK_TEXT | new link text + ELECTRONIC_ACCESS_MATERIALS_SPECIFIED | new materials + ELECTRONIC_ACCESS_URL_PUBLIC_NOTE | new note + """, delimiter = '|') + @SneakyThrows + void shouldReplaceElectronicAccessFields(UpdateOptionType option, String newValue) { + var holdingsRecord = buildHoldingsWithElectronicAccess(); + + var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null); + var action = new Action().type(REPLACE_WITH).updated(newValue); + + processor.updater(option, action).apply(holdingsRecord); + + var electronicAccess = holdingsRecord.getElectronicAccess().get(0); + switch (option) { + case ELECTRONIC_ACCESS_URL_RELATIONSHIP -> assertEquals(newValue, electronicAccess.getRelationshipId()); + case ELECTRONIC_ACCESS_URI -> assertEquals(newValue, electronicAccess.getUri()); + case ELECTRONIC_ACCESS_LINK_TEXT -> assertEquals(newValue, electronicAccess.getLinkText()); + case ELECTRONIC_ACCESS_MATERIALS_SPECIFIED -> assertEquals(newValue, electronicAccess.getMaterialsSpecification()); + case ELECTRONIC_ACCESS_URL_PUBLIC_NOTE -> assertEquals(newValue, electronicAccess.getPublicNote()); + } + } + + @ParameterizedTest + @CsvSource(textBlock = """ + ELECTRONIC_ACCESS_URL_RELATIONSHIP | 2510a1d1-a61c-4378-8886-b831004f018e | a6398566-f6cb-4916-96a9-5d3353e06d58 + ELECTRONIC_ACCESS_URI | http://example.org | http://modified.org + ELECTRONIC_ACCESS_LINK_TEXT | link text | new link text + ELECTRONIC_ACCESS_MATERIALS_SPECIFIED | materials | new materials + ELECTRONIC_ACCESS_URL_PUBLIC_NOTE | public note | new public note + """, delimiter = '|') + @SneakyThrows + void shouldFindAndReplaceExactlyMatchedElectronicAccessFields(UpdateOptionType option, String initial, String updated) { + var holdingsRecord = buildHoldingsWithElectronicAccess(); + + var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null); + var action = new Action().type(FIND_AND_REPLACE).initial(initial).updated(updated); + + processor.updater(option, action).apply(holdingsRecord); + + var modified = holdingsRecord.getElectronicAccess().get(0); + var unmodified = holdingsRecord.getElectronicAccess().get(1); + var initialElectronicAccess = buildHoldingsWithElectronicAccess().getElectronicAccess().get(1); + + switch (option) { + case ELECTRONIC_ACCESS_URL_RELATIONSHIP -> { + assertEquals(updated, modified.getRelationshipId()); + assertEquals(initialElectronicAccess.getRelationshipId(), unmodified.getRelationshipId()); + } + case ELECTRONIC_ACCESS_URI -> { + assertEquals(updated, modified.getUri()); + assertEquals(initialElectronicAccess.getUri(), unmodified.getUri()); + } + case ELECTRONIC_ACCESS_LINK_TEXT -> { + assertEquals(updated, modified.getLinkText()); + assertEquals(initialElectronicAccess.getLinkText(), unmodified.getLinkText()); + } + case ELECTRONIC_ACCESS_MATERIALS_SPECIFIED -> { + assertEquals(updated, modified.getMaterialsSpecification()); + assertEquals(initialElectronicAccess.getMaterialsSpecification(), unmodified.getMaterialsSpecification()); + } + case ELECTRONIC_ACCESS_URL_PUBLIC_NOTE -> { + assertEquals(updated, modified.getPublicNote()); + assertEquals(initialElectronicAccess.getPublicNote(), unmodified.getPublicNote()); + } + } + } + + private HoldingsRecord buildHoldingsWithElectronicAccess() { + return HoldingsRecord.builder() + .electronicAccess(List.of( + ElectronicAccess.builder() + .relationshipId("2510a1d1-a61c-4378-8886-b831004f018e") + .uri("http://example.org") + .linkText("link text") + .materialsSpecification("materials") + .publicNote("public note").build(), + ElectronicAccess.builder() + .relationshipId("3510a1d1-a61c-4378-8886-b831004f018e") + .uri("http://example2.org") + .linkText("Link text") + .materialsSpecification("Materials") + .publicNote("note").build() + )).build(); + } } +