From e7717b3672737eda16018443f86290aa2a2f0eff Mon Sep 17 00:00:00 2001 From: askhat-abishev <150008941+askhat-abishev@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:23:56 +0500 Subject: [PATCH] MODLD-535: Disable automatic transformation of MARC bibliographic records (#7) --- .../data/controller/ResourceController.java | 4 +- .../SourceRecordDomainEventListener.java | 13 +- .../SourceRecordDomainEventHandler.java | 54 ++---- .../ResourceMarcAuthorityService.java | 9 + ...rvice.java => ResourceMarcBibService.java} | 5 +- .../{ => impl}/ResourceGraphServiceImpl.java | 3 +- .../ResourceMarcAuthorityServiceImpl.java | 115 +++++++++++ .../ResourceMarcBibServiceImpl.java} | 182 +++++------------- .../{ => impl}/ResourceServiceImpl.java | 4 +- .../controller/ResourceControllerTest.java | 4 +- .../e2e/AuthorityUpdateAndReadWorkIT.java | 4 +- .../SourceRecordDomainEventListenerIT.java | 33 ++++ .../SourceRecordDomainEventHandlerIT.java | 9 +- .../SourceRecordDomainEventHandlerTest.java | 70 +------ .../ResourceGraphServiceImplTest.java} | 4 +- .../ResourceMarcAuthorityServiceImplTest.java | 132 +++++++++++++ .../ResourceMarcBibServiceImplTest.java} | 170 +--------------- .../ResourceServiceImplTest.java} | 5 +- 18 files changed, 386 insertions(+), 434 deletions(-) create mode 100644 src/main/java/org/folio/linked/data/service/resource/ResourceMarcAuthorityService.java rename src/main/java/org/folio/linked/data/service/resource/{ResourceMarcService.java => ResourceMarcBibService.java} (77%) rename src/main/java/org/folio/linked/data/service/resource/{ => impl}/ResourceGraphServiceImpl.java (97%) create mode 100644 src/main/java/org/folio/linked/data/service/resource/impl/ResourceMarcAuthorityServiceImpl.java rename src/main/java/org/folio/linked/data/service/resource/{ResourceMarcServiceImpl.java => impl/ResourceMarcBibServiceImpl.java} (65%) rename src/main/java/org/folio/linked/data/service/resource/{ => impl}/ResourceServiceImpl.java (95%) rename src/test/java/org/folio/linked/data/service/resource/{ResourceGraphServiceTest.java => impl/ResourceGraphServiceImplTest.java} (96%) create mode 100644 src/test/java/org/folio/linked/data/service/resource/impl/ResourceMarcAuthorityServiceImplTest.java rename src/test/java/org/folio/linked/data/service/resource/{ResourceMarcServiceTest.java => impl/ResourceMarcBibServiceImplTest.java} (63%) rename src/test/java/org/folio/linked/data/service/resource/{ResourceServiceTest.java => impl/ResourceServiceImplTest.java} (98%) diff --git a/src/main/java/org/folio/linked/data/controller/ResourceController.java b/src/main/java/org/folio/linked/data/controller/ResourceController.java index 964dd491..7a6c26db 100644 --- a/src/main/java/org/folio/linked/data/controller/ResourceController.java +++ b/src/main/java/org/folio/linked/data/controller/ResourceController.java @@ -9,7 +9,7 @@ import org.folio.linked.data.domain.dto.ResourceRequestDto; import org.folio.linked.data.domain.dto.ResourceResponseDto; import org.folio.linked.data.rest.resource.ResourceApi; -import org.folio.linked.data.service.resource.ResourceMarcService; +import org.folio.linked.data.service.resource.ResourceMarcBibService; import org.folio.linked.data.service.resource.ResourceService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -19,7 +19,7 @@ public class ResourceController implements ResourceApi { private final ResourceService resourceService; - private final ResourceMarcService resourceMarcService; + private final ResourceMarcBibService resourceMarcService; @Override public ResponseEntity createResource(String okapiTenant, @Valid ResourceRequestDto resourceDto) { diff --git a/src/main/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListener.java b/src/main/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListener.java index 56628234..1e23c7a5 100644 --- a/src/main/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListener.java +++ b/src/main/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListener.java @@ -1,6 +1,7 @@ package org.folio.linked.data.integration.kafka.listener; import static java.util.Optional.ofNullable; +import static org.folio.linked.data.domain.dto.SourceRecordType.MARC_AUTHORITY; import static org.folio.linked.data.util.Constants.FOLIO_PROFILE; import static org.folio.linked.data.util.KafkaUtils.getHeaderValueByName; import static org.folio.spring.integration.XOkapiHeaders.TENANT; @@ -56,11 +57,13 @@ private void processRecord(ConsumerRecord consu .map(SourceRecordType::fromValue) .orElseThrow(); - tenantScopedExecutionService.executeAsyncWithRetry( - consumerRecord.headers(), - retryContext -> runRetryableJob(event, sourceRecordType, retryContext), - ex -> logFailedEvent(event, sourceRecordType, ex, false) - ); + if (sourceRecordType == MARC_AUTHORITY) { + tenantScopedExecutionService.executeAsyncWithRetry( + consumerRecord.headers(), + retryContext -> runRetryableJob(event, sourceRecordType, retryContext), + ex -> logFailedEvent(event, sourceRecordType, ex, false) + ); + } } private void runRetryableJob(SourceRecordDomainEvent event, SourceRecordType type, RetryContext context) { diff --git a/src/main/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandler.java b/src/main/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandler.java index fad2e956..d4206ee5 100644 --- a/src/main/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandler.java +++ b/src/main/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandler.java @@ -1,14 +1,11 @@ package org.folio.linked.data.integration.kafka.listener.handler; import static java.util.Objects.isNull; -import static java.util.Optional.ofNullable; import static org.apache.commons.lang3.ObjectUtils.isEmpty; -import static org.folio.ld.dictionary.ResourceTypeDictionary.INSTANCE; import static org.folio.linked.data.domain.dto.SourceRecordDomainEvent.EventTypeEnum.CREATED; import static org.folio.linked.data.domain.dto.SourceRecordDomainEvent.EventTypeEnum.UPDATED; import static org.folio.linked.data.domain.dto.SourceRecordType.MARC_AUTHORITY; import static org.folio.linked.data.domain.dto.SourceRecordType.MARC_BIB; -import static org.folio.linked.data.model.entity.ResourceSource.LINKED_DATA; import static org.folio.linked.data.util.Constants.FOLIO_PROFILE; import java.util.Set; @@ -17,11 +14,8 @@ import org.folio.ld.dictionary.model.Resource; import org.folio.linked.data.domain.dto.SourceRecordDomainEvent; import org.folio.linked.data.domain.dto.SourceRecordType; -import org.folio.linked.data.model.entity.FolioMetadata; -import org.folio.linked.data.repo.FolioMetadataRepository; -import org.folio.linked.data.service.resource.ResourceMarcService; +import org.folio.linked.data.service.resource.ResourceMarcAuthorityService; import org.folio.marc4ld.service.marc2ld.authority.MarcAuthority2ldMapper; -import org.folio.marc4ld.service.marc2ld.bib.MarcBib2ldMapper; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @@ -35,23 +29,23 @@ public class SourceRecordDomainEventHandler { private static final String EMPTY_RESOURCE_MAPPED = "Empty resource(s) mapped from SourceRecordDomainEvent [id {}]"; private static final String NO_MARC_EVENT = "SourceRecordDomainEvent [id {}] has no Marc record inside"; private static final String UNSUPPORTED_TYPE = "Ignoring unsupported {} type [{}] in SourceRecordDomainEvent [id {}]"; - private static final String IGNORED_LINKED_DATA_SOURCE = "Instance [id {}] has source = LINKED_DATA, " - + "thus skipping it's update out of SourceRecordDomainEvent [id {}]"; private static final Set SUPPORTED_RECORD_TYPES = Set.of(MARC_BIB, MARC_AUTHORITY); private static final Set SUPPORTED_EVENT_TYPES = Set.of(CREATED, UPDATED); - private final MarcBib2ldMapper marcBib2ldMapper; private final MarcAuthority2ldMapper marcAuthority2ldMapper; - private final ResourceMarcService resourceMarcService; - private final FolioMetadataRepository folioMetadataRepository; + private final ResourceMarcAuthorityService resourceMarcService; + @SuppressWarnings("java:S125") public void handle(SourceRecordDomainEvent event, SourceRecordType recordType) { if (notProcessableEvent(event, recordType)) { return; } - if (recordType == MARC_BIB) { - saveBib(event); - } else if (recordType == MARC_AUTHORITY) { + if (recordType == MARC_AUTHORITY) { saveAuthorities(event); + // } else if (recordType == MARC_BIB) { + // For now, you can discard the event if it is a MARC_BIB record + // In coming sprints, we will have stories to update the hrid (extracted out of MARC field 001) in DB for marc + // bib records + // this.updateHrId(event); } } @@ -73,43 +67,19 @@ private boolean notProcessableEvent(SourceRecordDomainEvent event, SourceRecordT return false; } - private void saveBib(SourceRecordDomainEvent event) { - marcBib2ldMapper.fromMarcJson(event.getEventPayload().getParsedRecord().getContent()) - .ifPresentOrElse(resource -> { - if (isInstanceWithLinkedDataSource(resource)) { - log.info(IGNORED_LINKED_DATA_SOURCE, resource.getId(), event.getId()); - } else { - saveResource(resource, event, MARC_BIB); - } - }, - () -> logEmptyResource(event.getId()) - ); - } - - private boolean isInstanceWithLinkedDataSource(Resource resource) { - return resource.getTypes().equals(Set.of(INSTANCE)) - && ofNullable(resource.getFolioMetadata()) - .map(org.folio.ld.dictionary.model.FolioMetadata::getSrsId) - .flatMap(folioMetadataRepository::findBySrsId) - .or(() -> folioMetadataRepository.findById(resource.getId())) - .map(FolioMetadata::getSource) - .map(LINKED_DATA::equals) - .orElse(false); - } - private void saveAuthorities(SourceRecordDomainEvent event) { var mapped = marcAuthority2ldMapper.fromMarcJson(event.getEventPayload().getParsedRecord().getContent()); if (mapped.isEmpty()) { logEmptyResource(event.getId()); } else { - mapped.forEach(resource -> saveResource(resource, event, MARC_AUTHORITY)); + mapped.forEach(resource -> saveAuthority(resource, event)); } } - private void saveResource(Resource resource, SourceRecordDomainEvent event, SourceRecordType recordType) { + private void saveAuthority(Resource resource, SourceRecordDomainEvent event) { if (CREATED == event.getEventType() || UPDATED == event.getEventType()) { var id = resourceMarcService.saveMarcResource(resource); - log.info(EVENT_SAVED, event.getId(), recordType.getValue(), id); + log.info(EVENT_SAVED, event.getId(), MARC_AUTHORITY, id); } } diff --git a/src/main/java/org/folio/linked/data/service/resource/ResourceMarcAuthorityService.java b/src/main/java/org/folio/linked/data/service/resource/ResourceMarcAuthorityService.java new file mode 100644 index 00000000..da3396e8 --- /dev/null +++ b/src/main/java/org/folio/linked/data/service/resource/ResourceMarcAuthorityService.java @@ -0,0 +1,9 @@ +package org.folio.linked.data.service.resource; + +import org.folio.ld.dictionary.model.Resource; + +public interface ResourceMarcAuthorityService { + + Long saveMarcResource(Resource modelResource); + +} diff --git a/src/main/java/org/folio/linked/data/service/resource/ResourceMarcService.java b/src/main/java/org/folio/linked/data/service/resource/ResourceMarcBibService.java similarity index 77% rename from src/main/java/org/folio/linked/data/service/resource/ResourceMarcService.java rename to src/main/java/org/folio/linked/data/service/resource/ResourceMarcBibService.java index 98276be8..bd6558ea 100644 --- a/src/main/java/org/folio/linked/data/service/resource/ResourceMarcService.java +++ b/src/main/java/org/folio/linked/data/service/resource/ResourceMarcBibService.java @@ -1,13 +1,10 @@ package org.folio.linked.data.service.resource; -import org.folio.ld.dictionary.model.Resource; import org.folio.linked.data.domain.dto.ResourceIdDto; import org.folio.linked.data.domain.dto.ResourceMarcViewDto; import org.folio.linked.data.domain.dto.ResourceResponseDto; -public interface ResourceMarcService { - - Long saveMarcResource(Resource modelResource); +public interface ResourceMarcBibService { ResourceMarcViewDto getResourceMarcView(Long id); diff --git a/src/main/java/org/folio/linked/data/service/resource/ResourceGraphServiceImpl.java b/src/main/java/org/folio/linked/data/service/resource/impl/ResourceGraphServiceImpl.java similarity index 97% rename from src/main/java/org/folio/linked/data/service/resource/ResourceGraphServiceImpl.java rename to src/main/java/org/folio/linked/data/service/resource/impl/ResourceGraphServiceImpl.java index a4f7e5c2..53cc1e93 100644 --- a/src/main/java/org/folio/linked/data/service/resource/ResourceGraphServiceImpl.java +++ b/src/main/java/org/folio/linked/data/service/resource/impl/ResourceGraphServiceImpl.java @@ -1,4 +1,4 @@ -package org.folio.linked.data.service.resource; +package org.folio.linked.data.service.resource.impl; import static java.util.Optional.ofNullable; import static org.apache.commons.lang3.ObjectUtils.notEqual; @@ -17,6 +17,7 @@ import org.folio.linked.data.model.entity.ResourceEdge; import org.folio.linked.data.repo.ResourceEdgeRepository; import org.folio.linked.data.repo.ResourceRepository; +import org.folio.linked.data.service.resource.ResourceGraphService; import org.folio.linked.data.util.JsonUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/org/folio/linked/data/service/resource/impl/ResourceMarcAuthorityServiceImpl.java b/src/main/java/org/folio/linked/data/service/resource/impl/ResourceMarcAuthorityServiceImpl.java new file mode 100644 index 00000000..98f9e445 --- /dev/null +++ b/src/main/java/org/folio/linked/data/service/resource/impl/ResourceMarcAuthorityServiceImpl.java @@ -0,0 +1,115 @@ +package org.folio.linked.data.service.resource.impl; + +import static java.util.Objects.isNull; +import static org.folio.ld.dictionary.PredicateDictionary.REPLACED_BY; +import static org.folio.ld.dictionary.PropertyDictionary.RESOURCE_PREFERRED; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.function.Function; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.linked.data.exception.NotFoundException; +import org.folio.linked.data.mapper.ResourceModelMapper; +import org.folio.linked.data.model.entity.Resource; +import org.folio.linked.data.model.entity.ResourceEdge; +import org.folio.linked.data.model.entity.event.ResourceCreatedEvent; +import org.folio.linked.data.model.entity.event.ResourceEvent; +import org.folio.linked.data.model.entity.event.ResourceReplacedEvent; +import org.folio.linked.data.model.entity.event.ResourceUpdatedEvent; +import org.folio.linked.data.repo.FolioMetadataRepository; +import org.folio.linked.data.repo.ResourceRepository; +import org.folio.linked.data.service.resource.ResourceGraphService; +import org.folio.linked.data.service.resource.ResourceMarcAuthorityService; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Log4j2 +@Service +@Transactional +@RequiredArgsConstructor +public class ResourceMarcAuthorityServiceImpl implements ResourceMarcAuthorityService { + + private final ObjectMapper objectMapper; + private final ResourceRepository resourceRepo; + private final ResourceModelMapper resourceModelMapper; + private final ResourceGraphService resourceGraphService; + private final FolioMetadataRepository folioMetadataRepository; + private final ApplicationEventPublisher applicationEventPublisher; + + @Override + public Long saveMarcResource(org.folio.ld.dictionary.model.Resource modelResource) { + var mapped = resourceModelMapper.toEntity(modelResource); + if (!mapped.isAuthority()) { + var message = "Resource is not an authority"; + log.error(message); + throw new IllegalArgumentException(message); + } + if (resourceRepo.existsById(modelResource.getId())) { + return updateAuthority(mapped); + } + if (folioMetadataRepository.existsBySrsId(modelResource.getFolioMetadata().getSrsId())) { + return replaceAuthority(mapped); + } + return createAuthority(mapped); + } + + private Long updateAuthority(Resource resource) { + var id = resource.getId(); + var srsId = resource.getFolioMetadata().getSrsId(); + logMarcAction(resource, "found by id [" + id + "] with srsId [" + srsId + "]", "be updated"); + return saveAndPublishEvent(resource, ResourceUpdatedEvent::new); + } + + private Long replaceAuthority(Resource resource) { + var srsId = resource.getFolioMetadata().getSrsId(); + return resourceRepo.findByFolioMetadataSrsId(srsId) + .map(previous -> { + var previousObsolete = markObsolete(previous); + setPreferred(resource, true); + var re = new ResourceEdge(previousObsolete, resource, REPLACED_BY); + previousObsolete.addOutgoingEdge(re); + resource.addIncomingEdge(re); + logMarcAction(resource, "not found by id, but found by srsId [" + srsId + "]", + "be saved as a new version of previously existed resource [id " + previous.getId() + "]"); + return saveAndPublishEvent(resource, saved -> new ResourceReplacedEvent(previousObsolete, saved)); + }) + .orElseThrow(() -> new NotFoundException("Resource not found by srsId: " + srsId)); + } + + private Long createAuthority(Resource resource) { + logMarcAction(resource, "not found by id and srsId", "be created"); + return saveAndPublishEvent(resource, ResourceCreatedEvent::new); + } + + private void logMarcAction(Resource resource, String existence, String action) { + log.info("Incoming Authority resource [id {}, srsId {}] is {} and will {}", + resource.getId(), resource.getFolioMetadata().getSrsId(), existence, action); + } + + private Long saveAndPublishEvent(Resource resource, Function resourceEventSupplier) { + var newResource = resourceGraphService.saveMergingGraph(resource); + var event = resourceEventSupplier.apply(newResource); + if (event instanceof ResourceReplacedEvent rre) { + resourceRepo.save(rre.previous()); + } + applicationEventPublisher.publishEvent(event); + return newResource.getId(); + } + + private Resource markObsolete(Resource resource) { + resource.setActive(false); + setPreferred(resource, false); + resource.setFolioMetadata(null); + return resource; + } + + private void setPreferred(Resource resource, boolean preferred) { + if (isNull(resource.getDoc())) { + resource.setDoc(objectMapper.createObjectNode()); + } + var arrayNode = objectMapper.createArrayNode().add(String.valueOf(preferred)); + ((ObjectNode) resource.getDoc()).set(RESOURCE_PREFERRED.getValue(), arrayNode); + } +} diff --git a/src/main/java/org/folio/linked/data/service/resource/ResourceMarcServiceImpl.java b/src/main/java/org/folio/linked/data/service/resource/impl/ResourceMarcBibServiceImpl.java similarity index 65% rename from src/main/java/org/folio/linked/data/service/resource/ResourceMarcServiceImpl.java rename to src/main/java/org/folio/linked/data/service/resource/impl/ResourceMarcBibServiceImpl.java index 3d220502..31f39542 100644 --- a/src/main/java/org/folio/linked/data/service/resource/ResourceMarcServiceImpl.java +++ b/src/main/java/org/folio/linked/data/service/resource/impl/ResourceMarcBibServiceImpl.java @@ -1,9 +1,6 @@ -package org.folio.linked.data.service.resource; +package org.folio.linked.data.service.resource.impl; import static java.lang.String.format; -import static java.util.Objects.isNull; -import static org.folio.ld.dictionary.PredicateDictionary.REPLACED_BY; -import static org.folio.ld.dictionary.PropertyDictionary.RESOURCE_PREFERRED; import static org.folio.ld.dictionary.ResourceTypeDictionary.INSTANCE; import static org.folio.ld.dictionary.model.ResourceSource.LINKED_DATA; import static org.folio.linked.data.util.BibframeUtils.extractWorkFromInstance; @@ -13,7 +10,6 @@ import static org.folio.marc4ld.util.MarcUtil.isMonographicComponentPartOrItem; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import feign.FeignException; import java.util.Map; import java.util.Optional; @@ -32,15 +28,15 @@ import org.folio.linked.data.mapper.ResourceModelMapper; import org.folio.linked.data.mapper.dto.ResourceDtoMapper; import org.folio.linked.data.model.entity.Resource; -import org.folio.linked.data.model.entity.ResourceEdge; import org.folio.linked.data.model.entity.ResourceTypeEntity; -import org.folio.linked.data.model.entity.event.ResourceCreatedEvent; import org.folio.linked.data.model.entity.event.ResourceEvent; import org.folio.linked.data.model.entity.event.ResourceReplacedEvent; import org.folio.linked.data.model.entity.event.ResourceUpdatedEvent; import org.folio.linked.data.repo.FolioMetadataRepository; import org.folio.linked.data.repo.ResourceEdgeRepository; import org.folio.linked.data.repo.ResourceRepository; +import org.folio.linked.data.service.resource.ResourceGraphService; +import org.folio.linked.data.service.resource.ResourceMarcBibService; import org.folio.marc4ld.service.ld2marc.Bibframe2MarcMapper; import org.folio.marc4ld.service.marc2ld.bib.MarcBib2ldMapper; import org.folio.rest.jaxrs.model.ParsedRecord; @@ -55,9 +51,8 @@ @Service @Transactional @RequiredArgsConstructor -public class ResourceMarcServiceImpl implements ResourceMarcService { +public class ResourceMarcBibServiceImpl implements ResourceMarcBibService { - private static final String RESOURCE_NOT_FOUND_BY_SRS_ID = "Resource not found by srsId: "; private static final String RECORD_NOT_FOUND_BY_INVENTORY_ID = "Record with inventoryId: %s was not found"; private final ObjectMapper objectMapper; @@ -72,18 +67,6 @@ public class ResourceMarcServiceImpl implements ResourceMarcService { private final ApplicationEventPublisher applicationEventPublisher; private final SrsClient srsClient; - - public Long saveMarcResource(org.folio.ld.dictionary.model.Resource modelResource) { - var mapped = resourceModelMapper.toEntity(modelResource); - if (resourceRepo.existsById(modelResource.getId())) { - return updateResource(mapped); - } - if (folioMetadataRepository.existsBySrsId(modelResource.getFolioMetadata().getSrsId())) { - return replaceResource(mapped); - } - return createResource(mapped); - } - @Override @Transactional(readOnly = true) public ResourceMarcViewDto getResourceMarcView(Long id) { @@ -108,15 +91,17 @@ public Boolean isSupportedByInventoryId(String inventoryId) { @Override public ResourceResponseDto getResourcePreviewByInventoryId(String inventoryId) { - return getResource(inventoryId) + var resourceResponseDto = getResource(inventoryId) .map(resourceModelMapper::toEntity) .map(resourceDtoMapper::toDto) .orElseThrow(() -> createNotFoundException(format(RECORD_NOT_FOUND_BY_INVENTORY_ID, inventoryId))); + log.info("Returning resource preview for MARC BIB record with inventory ID: {}", inventoryId); + return resourceResponseDto; } @Override public ResourceIdDto importMarcRecord(String inventoryId) { - return getResource(inventoryId) + var resourceIdDto = getResource(inventoryId) .map(resource -> { resource.getFolioMetadata().setSource(LINKED_DATA); return resource; @@ -125,6 +110,9 @@ public ResourceIdDto importMarcRecord(String inventoryId) { .map(String::valueOf) .map(id -> new ResourceIdDto().id(id)) .orElseThrow(() -> createNotFoundException(format(RECORD_NOT_FOUND_BY_INVENTORY_ID, inventoryId))); + log.info("MARC BIB record with inventory ID: {} is successfully imported to graph resource with ID: {}", + inventoryId, resourceIdDto.getId()); + return resourceIdDto; } private void validateMarkViewSupportedType(Resource resource) { @@ -141,113 +129,9 @@ private void validateMarkViewSupportedType(Resource resource) { ); } - private Long createResource(Resource resource) { - logMarcAction(resource, "not found by id and srsId", "be created"); - return saveAndPublishEvent(resource, ResourceCreatedEvent::new); - } - - private Long replaceResource(Resource resource) { - if (resource.isAuthority()) { - return replaceAuthority(resource); - } - return replaceBibliographic(resource); - } - - private Long replaceAuthority(Resource resource) { - var srsId = resource.getFolioMetadata().getSrsId(); - return resourceRepo.findByFolioMetadataSrsId(srsId) - .map(previous -> { - var previousObsolete = markObsolete(previous); - setPreferred(resource, true); - var re = new ResourceEdge(previousObsolete, resource, REPLACED_BY); - previousObsolete.addOutgoingEdge(re); - resource.addIncomingEdge(re); - logMarcAction(resource, "not found by id, but found by srsId [" + srsId + "]", - "be saved as a new version of previously existed resource [id " + previous.getId() + "]"); - return saveAndPublishEvent(resource, saved -> new ResourceReplacedEvent(previousObsolete, saved)); - }) - .orElseThrow(() -> createNotFoundException(RESOURCE_NOT_FOUND_BY_SRS_ID + srsId)); - } - - private Resource markObsolete(Resource resource) { - resource.setActive(false); - setPreferred(resource, false); - resource.setFolioMetadata(null); - return resource; - } - - private void setPreferred(Resource resource, boolean preferred) { - if (isNull(resource.getDoc())) { - resource.setDoc(objectMapper.createObjectNode()); - } - var arrayNode = objectMapper.createArrayNode().add(String.valueOf(preferred)); - ((ObjectNode) resource.getDoc()).set(RESOURCE_PREFERRED.getValue(), arrayNode); - } - - private Long replaceBibliographic(Resource resource) { - var srsId = resource.getFolioMetadata().getSrsId(); - return resourceRepo.findByFolioMetadataSrsId(srsId) - .map(Resource::new) - .map(existedBySrsId -> { - logMarcAction(resource, "not found by id, but found by srsId [" + srsId + "]", - "replace previously existed [id " + existedBySrsId.getId() + "]"); - return saveAndPublishEvent(resource, saved -> new ResourceReplacedEvent(existedBySrsId, saved)); - }) - .orElseThrow(() -> createNotFoundException(RESOURCE_NOT_FOUND_BY_SRS_ID + srsId)); - } - - private Long updateResource(Resource resource) { - var id = resource.getId(); - var srsId = resource.getFolioMetadata().getSrsId(); - logMarcAction(resource, "found by id [" + id + "] with srsId [" + srsId + "]", "be updated"); - return saveAndPublishEvent(resource, ResourceUpdatedEvent::new); - } - - private void logMarcAction(Resource resource, String existence, String action) { - log.info("Incoming {} resource [id {}, srsId {}] is {} and will {}", - getResourceKind(resource), resource.getId(), resource.getFolioMetadata().getSrsId(), existence, action); - } - - private String getResourceKind(Resource resource) { - if (resource.isAuthority()) { - return "Authority"; - } - return "Bibliographic"; - } - - private Long saveAndPublishEvent(Resource resource, Function resourceEventSupplier) { - var newResource = resourceGraphService.saveMergingGraph(resource); - refreshWork(newResource); - var event = resourceEventSupplier.apply(newResource); - if (event instanceof ResourceReplacedEvent rre) { - if (resource.isAuthority()) { - resourceRepo.save(rre.previous()); - } else { - resourceGraphService.breakEdgesAndDelete(rre.previous()); - } - } - applicationEventPublisher.publishEvent(event); - return newResource.getId(); - } - - private void refreshWork(Resource resource) { - if (resource.isOfType(INSTANCE)) { - extractWorkFromInstance(resource) - .ifPresent(work -> { - edgeRepo.findByIdTargetHash(work.getId()) - .forEach(work::addIncomingEdge); - addOutgoingEdges(work); - }); - } - } - - private void addOutgoingEdges(Resource resource) { - edgeRepo.findByIdSourceHash(resource.getId()) - .forEach(resource::addOutgoingEdge); - } - - private boolean isMonograph(String leader) { - return isLanguageMaterial(leader.charAt(6)) && isMonographicComponentPartOrItem(leader.charAt(7)); + private NotFoundException createNotFoundException(String message) { + log.error(message); + return new NotFoundException(message); } private Optional> getRecord(String inventoryId) { @@ -258,6 +142,10 @@ private Optional> getRecord(String inventoryId) { } } + private boolean isMonograph(String leader) { + return isLanguageMaterial(leader.charAt(6)) && isMonographicComponentPartOrItem(leader.charAt(7)); + } + private Optional getResource(String inventoryId) { return getRecord(inventoryId) .map(HttpEntity::getBody) @@ -267,6 +155,11 @@ private Optional getResource(String inve .flatMap(marcBib2ldMapper::fromMarcJson); } + @SneakyThrows + private String toJsonString(Object content) { + return objectMapper.writeValueAsString(content); + } + private Long save(org.folio.ld.dictionary.model.Resource modelResource) { var id = modelResource.getId(); if (resourceRepo.existsById(id)) { @@ -289,13 +182,30 @@ private Long save(org.folio.ld.dictionary.model.Resource modelResource) { return saveAndPublishEvent(resourceModelMapper.toEntity(modelResource), ResourceUpdatedEvent::new); } - @SneakyThrows - private String toJsonString(Object content) { - return objectMapper.writeValueAsString(content); + private Long saveAndPublishEvent(Resource resource, Function resourceEventSupplier) { + var newResource = resourceGraphService.saveMergingGraph(resource); + refreshWork(newResource); + var event = resourceEventSupplier.apply(newResource); + if (event instanceof ResourceReplacedEvent rre) { + resourceGraphService.breakEdgesAndDelete(rre.previous()); + } + applicationEventPublisher.publishEvent(event); + return newResource.getId(); } - private NotFoundException createNotFoundException(String message) { - log.error(message); - return new NotFoundException(message); + private void refreshWork(Resource resource) { + if (resource.isOfType(INSTANCE)) { + extractWorkFromInstance(resource) + .ifPresent(work -> { + edgeRepo.findByIdTargetHash(work.getId()) + .forEach(work::addIncomingEdge); + addOutgoingEdges(work); + }); + } + } + + private void addOutgoingEdges(Resource resource) { + edgeRepo.findByIdSourceHash(resource.getId()) + .forEach(resource::addOutgoingEdge); } } diff --git a/src/main/java/org/folio/linked/data/service/resource/ResourceServiceImpl.java b/src/main/java/org/folio/linked/data/service/resource/impl/ResourceServiceImpl.java similarity index 95% rename from src/main/java/org/folio/linked/data/service/resource/ResourceServiceImpl.java rename to src/main/java/org/folio/linked/data/service/resource/impl/ResourceServiceImpl.java index 2c426417..bcb76efc 100644 --- a/src/main/java/org/folio/linked/data/service/resource/ResourceServiceImpl.java +++ b/src/main/java/org/folio/linked/data/service/resource/impl/ResourceServiceImpl.java @@ -1,4 +1,4 @@ -package org.folio.linked.data.service.resource; +package org.folio.linked.data.service.resource.impl; import static org.folio.linked.data.util.Constants.IS_NOT_FOUND; import static org.folio.linked.data.util.Constants.RESOURCE_WITH_GIVEN_ID; @@ -20,6 +20,8 @@ import org.folio.linked.data.model.entity.event.ResourceUpdatedEvent; import org.folio.linked.data.repo.FolioMetadataRepository; import org.folio.linked.data.repo.ResourceRepository; +import org.folio.linked.data.service.resource.ResourceGraphService; +import org.folio.linked.data.service.resource.ResourceService; import org.folio.linked.data.service.resource.meta.MetadataService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.annotation.Async; diff --git a/src/test/java/org/folio/linked/data/controller/ResourceControllerTest.java b/src/test/java/org/folio/linked/data/controller/ResourceControllerTest.java index 4fb23e0c..a0e46235 100644 --- a/src/test/java/org/folio/linked/data/controller/ResourceControllerTest.java +++ b/src/test/java/org/folio/linked/data/controller/ResourceControllerTest.java @@ -4,7 +4,7 @@ import static org.mockito.Mockito.when; import org.folio.linked.data.domain.dto.ResourceMarcViewDto; -import org.folio.linked.data.service.resource.ResourceMarcService; +import org.folio.linked.data.service.resource.ResourceMarcBibService; import org.folio.linked.data.service.resource.ResourceService; import org.folio.spring.testing.type.UnitTest; import org.junit.jupiter.api.Test; @@ -24,7 +24,7 @@ class ResourceControllerTest { @Mock ResourceService resourceService; @Mock - ResourceMarcService resourceMarcService; + ResourceMarcBibService resourceMarcService; @Test void getResourceMarcViewById_shouldReturnOkResponse() { diff --git a/src/test/java/org/folio/linked/data/e2e/AuthorityUpdateAndReadWorkIT.java b/src/test/java/org/folio/linked/data/e2e/AuthorityUpdateAndReadWorkIT.java index a20f69c1..d8ff115f 100644 --- a/src/test/java/org/folio/linked/data/e2e/AuthorityUpdateAndReadWorkIT.java +++ b/src/test/java/org/folio/linked/data/e2e/AuthorityUpdateAndReadWorkIT.java @@ -36,7 +36,7 @@ import org.folio.linked.data.model.entity.Resource; import org.folio.linked.data.model.entity.ResourceEdge; import org.folio.linked.data.repo.ResourceEdgeRepository; -import org.folio.linked.data.service.resource.ResourceMarcService; +import org.folio.linked.data.service.resource.ResourceMarcAuthorityService; import org.folio.linked.data.service.tenant.TenantScopedExecutionService; import org.folio.linked.data.test.MonographTestUtil; import org.folio.linked.data.test.ResourceTestRepository; @@ -69,7 +69,7 @@ class AuthorityUpdateAndReadWorkIT { private ResourceTestRepository resourceTestRepository; @SpyBean @Autowired - private ResourceMarcService resourceMarcService; + private ResourceMarcAuthorityService resourceMarcService; @Autowired private Environment env; @Autowired diff --git a/src/test/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListenerIT.java b/src/test/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListenerIT.java index 7f573b1f..c5940962 100644 --- a/src/test/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListenerIT.java +++ b/src/test/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListenerIT.java @@ -1,6 +1,7 @@ package org.folio.linked.data.integration.kafka.listener; import static org.folio.linked.data.domain.dto.SourceRecordDomainEvent.EventTypeEnum.CREATED; +import static org.folio.linked.data.domain.dto.SourceRecordType.MARC_AUTHORITY; import static org.folio.linked.data.domain.dto.SourceRecordType.MARC_BIB; import static org.folio.linked.data.test.TestUtil.FOLIO_TEST_PROFILE; import static org.folio.linked.data.test.TestUtil.TENANT_ID; @@ -12,6 +13,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import org.folio.linked.data.domain.dto.ParsedRecord; import org.folio.linked.data.domain.dto.SourceRecord; @@ -21,6 +23,7 @@ import org.folio.linked.data.test.TestUtil; import org.folio.spring.tools.kafka.KafkaAdminService; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; @@ -42,6 +45,7 @@ static void setup(@Autowired KafkaAdminService kafkaAdminService) { kafkaAdminService.createTopics(TENANT_ID); } + @Disabled("Handling MARC BIB records is disabled temporarily") @Test void shouldConsumeSrsDomainEvent() { // given @@ -62,6 +66,7 @@ void shouldConsumeSrsDomainEvent() { awaitAndAssert(() -> verify(sourceRecordDomainEventHandler).handle(expectedEvent, MARC_BIB)); } + @Disabled("Handling MARC BIB records is disabled temporarily") @Test void shouldRetryIfErrorOccurs() { // given @@ -82,4 +87,32 @@ void shouldRetryIfErrorOccurs() { awaitAndAssert(() -> verify(sourceRecordDomainEventHandler, times(2)).handle(expectedEvent, recordType)); } + @Test + void shouldNotHandleSrsDomainEvent_whenSourceRecordType_isMarcBib() { + // given + var eventProducerRecord = getSrsDomainEventProducerRecord("1", "{}", CREATED, MARC_BIB); + + // when + eventKafkaTemplate.send(eventProducerRecord); + + // then + awaitAndAssert(() -> verifyNoInteractions(sourceRecordDomainEventHandler)); + } + + @Test + void shouldHandleSrsDomainEvent_whenSourceRecordType_isMarcAuthority() { + // given + var eventId = "2"; + var marc = "{}"; + var eventType = CREATED; + var eventProducerRecord = getSrsDomainEventProducerRecord(eventId, marc, eventType, MARC_AUTHORITY); + var expectedEvent = getSrsDomainEvent(eventId, marc, eventType); + + // when + eventKafkaTemplate.send(eventProducerRecord); + + // then + awaitAndAssert(() -> verify(sourceRecordDomainEventHandler).handle(expectedEvent, MARC_AUTHORITY)); + } + } diff --git a/src/test/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandlerIT.java b/src/test/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandlerIT.java index c702a906..6ce5f0bb 100644 --- a/src/test/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandlerIT.java +++ b/src/test/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandlerIT.java @@ -36,7 +36,7 @@ import org.folio.linked.data.model.entity.Resource; import org.folio.linked.data.model.entity.ResourceEdge; import org.folio.linked.data.repo.ResourceEdgeRepository; -import org.folio.linked.data.service.resource.ResourceMarcService; +import org.folio.linked.data.service.resource.ResourceMarcAuthorityService; import org.folio.linked.data.service.tenant.TenantScopedExecutionService; import org.folio.linked.data.test.ResourceTestRepository; import org.folio.linked.data.test.kafka.KafkaSearchWorkIndexTopicListener; @@ -45,6 +45,7 @@ import org.folio.spring.tools.kafka.KafkaAdminService; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -77,7 +78,7 @@ class SourceRecordDomainEventHandlerIT { private ResourceTestRepository resourceTestRepository; @SpyBean @Autowired - private ResourceMarcService resourceMarcService; + private ResourceMarcAuthorityService resourceMarcService; @SpyBean @Autowired private ResourceModificationEventListener eventListener; @@ -98,6 +99,7 @@ public void clean() { ); } + @Disabled("Handling MARC BIB records is disabled temporarily") @ParameterizedTest @CsvSource({ "samples/marc2ld/marc_non_monograph_leader.jsonl, 0", @@ -116,6 +118,7 @@ void shouldNotProcessEventForNullableResource(String resource, int interactions) .saveMarcResource(any(org.folio.ld.dictionary.model.Resource.class))); } + @Disabled("Handling MARC BIB records is disabled temporarily") @Test void shouldProcessMarcBibSourceRecordDomainEvent() { // given @@ -223,6 +226,7 @@ void shouldProcessAuthoritySourceRecordDomainUpdateEvent() { assertAuthority(updatedResource, expectedLabelUpdated, true, true, null); } + @Disabled("Handling MARC BIB records is disabled temporarily") @Test void marcBibSourceRecordDomainEvent_shouldSendToIndexWorkWithTwoInstances() { // given @@ -259,6 +263,7 @@ void marcBibSourceRecordDomainEvent_shouldSendToIndexWorkWithTwoInstances() { }); } + @Disabled("Handling MARC BIB records is disabled temporarily") @Test void marcBibSourceRecordDomainEvent_shouldKeepExistedEdgesAndPropertiesAndFolioMetadata_inCaseOfUpdate() { // given diff --git a/src/test/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandlerTest.java b/src/test/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandlerTest.java index 5ec53d12..af72d98b 100644 --- a/src/test/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandlerTest.java +++ b/src/test/java/org/folio/linked/data/integration/kafka/listener/handler/SourceRecordDomainEventHandlerTest.java @@ -1,26 +1,21 @@ package org.folio.linked.data.integration.kafka.listener.handler; import static org.folio.ld.dictionary.ResourceTypeDictionary.CONCEPT; -import static org.folio.ld.dictionary.ResourceTypeDictionary.INSTANCE; import static org.folio.ld.dictionary.ResourceTypeDictionary.PERSON; import static org.folio.linked.data.domain.dto.SourceRecordDomainEvent.EventTypeEnum.CREATED; import static org.folio.linked.data.domain.dto.SourceRecordType.MARC_AUTHORITY; import static org.folio.linked.data.domain.dto.SourceRecordType.MARC_BIB; -import static org.folio.linked.data.model.entity.ResourceSource.LINKED_DATA; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import java.util.List; -import java.util.Optional; -import java.util.UUID; -import org.folio.ld.dictionary.model.FolioMetadata; import org.folio.ld.dictionary.model.Resource; import org.folio.linked.data.domain.dto.ParsedRecord; import org.folio.linked.data.domain.dto.SourceRecord; import org.folio.linked.data.domain.dto.SourceRecordDomainEvent; import org.folio.linked.data.repo.FolioMetadataRepository; -import org.folio.linked.data.service.resource.ResourceMarcService; +import org.folio.linked.data.service.resource.ResourceMarcAuthorityService; import org.folio.marc4ld.service.marc2ld.authority.MarcAuthority2ldMapper; import org.folio.marc4ld.service.marc2ld.bib.MarcBib2ldMapper; import org.folio.spring.testing.type.UnitTest; @@ -42,7 +37,7 @@ class SourceRecordDomainEventHandlerTest { @Mock private MarcAuthority2ldMapper marcAuthority2ldMapper; @Mock - private ResourceMarcService resourceMarcService; + private ResourceMarcAuthorityService resourceMarcService; @Mock private FolioMetadataRepository folioMetadataRepository; @@ -98,67 +93,6 @@ void shouldNotTriggerSaving_ifResourceMappedOutOfIncomingEventIsEmpty() { verifyNoInteractions(resourceMarcService); } - @Test - void shouldNotTriggerSaving_ifResourceMappedOutOfIncomingEventIsExistedByIdInstanceWithLinkedDataSource() { - // given - var event = new SourceRecordDomainEvent().id("5") - .eventType(CREATED) - .eventPayload(new SourceRecord().parsedRecord(new ParsedRecord("{ \"key\": \"value\"}"))); - var mapped = new Resource().setId(4L).addType(INSTANCE); - doReturn(Optional.of(mapped)).when(marcBib2ldMapper) - .fromMarcJson(event.getEventPayload().getParsedRecord().getContent()); - var existedMetaData = - new org.folio.linked.data.model.entity.FolioMetadata(new org.folio.linked.data.model.entity.Resource()) - .setSource(LINKED_DATA); - doReturn(Optional.of(existedMetaData)).when(folioMetadataRepository).findById(mapped.getId()); - - // when - sourceRecordDomainEventHandler.handle(event, MARC_BIB); - - // then - verifyNoInteractions(resourceMarcService); - } - - @Test - void shouldNotTriggerSaving_ifResourceMappedOutOfIncomingEventIsExistedBySrsIdInstanceWithLinkedDataSource() { - // given - var event = new SourceRecordDomainEvent().id("6") - .eventType(CREATED) - .eventPayload(new SourceRecord().parsedRecord(new ParsedRecord("{ \"key\": \"value\"}"))); - var mapped = new Resource().setId(4L).addType(INSTANCE) - .setFolioMetadata(new FolioMetadata().setSrsId(UUID.randomUUID().toString())); - doReturn(Optional.of(mapped)).when(marcBib2ldMapper) - .fromMarcJson(event.getEventPayload().getParsedRecord().getContent()); - var existedMetaData = - new org.folio.linked.data.model.entity.FolioMetadata(new org.folio.linked.data.model.entity.Resource()) - .setSource(LINKED_DATA); - doReturn(Optional.of(existedMetaData)) - .when(folioMetadataRepository).findBySrsId(mapped.getFolioMetadata().getSrsId()); - - // when - sourceRecordDomainEventHandler.handle(event, MARC_BIB); - - // then - verifyNoInteractions(resourceMarcService); - } - - @Test - void shouldTriggerResourceSaving_forCorrectMarcBibEvent() { - // given - var event = new SourceRecordDomainEvent().id("7") - .eventType(CREATED) - .eventPayload(new SourceRecord().parsedRecord(new ParsedRecord("{ \"key\": \"value\"}"))); - var mapped = new Resource().setId(7L).addType(INSTANCE); - doReturn(Optional.of(mapped)).when(marcBib2ldMapper) - .fromMarcJson(event.getEventPayload().getParsedRecord().getContent()); - - // when - sourceRecordDomainEventHandler.handle(event, MARC_BIB); - - // then - verify(resourceMarcService).saveMarcResource(mapped); - } - @Test void shouldTriggerResourceSaving_forCorrectMarcAuthorityEvent() { // given diff --git a/src/test/java/org/folio/linked/data/service/resource/ResourceGraphServiceTest.java b/src/test/java/org/folio/linked/data/service/resource/impl/ResourceGraphServiceImplTest.java similarity index 96% rename from src/test/java/org/folio/linked/data/service/resource/ResourceGraphServiceTest.java rename to src/test/java/org/folio/linked/data/service/resource/impl/ResourceGraphServiceImplTest.java index ce9f22f6..6cba2451 100644 --- a/src/test/java/org/folio/linked/data/service/resource/ResourceGraphServiceTest.java +++ b/src/test/java/org/folio/linked/data/service/resource/impl/ResourceGraphServiceImplTest.java @@ -1,4 +1,4 @@ -package org.folio.linked.data.service.resource; +package org.folio.linked.data.service.resource.impl; import static org.assertj.core.api.Assertions.assertThat; import static org.folio.linked.data.test.TestUtil.randomLong; @@ -23,7 +23,7 @@ @UnitTest @ExtendWith(MockitoExtension.class) -class ResourceGraphServiceTest { +class ResourceGraphServiceImplTest { @InjectMocks private ResourceGraphServiceImpl resourceGraphService; diff --git a/src/test/java/org/folio/linked/data/service/resource/impl/ResourceMarcAuthorityServiceImplTest.java b/src/test/java/org/folio/linked/data/service/resource/impl/ResourceMarcAuthorityServiceImplTest.java new file mode 100644 index 00000000..dccb4a00 --- /dev/null +++ b/src/test/java/org/folio/linked/data/service/resource/impl/ResourceMarcAuthorityServiceImplTest.java @@ -0,0 +1,132 @@ +package org.folio.linked.data.service.resource.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.ld.dictionary.PredicateDictionary.REPLACED_BY; +import static org.folio.ld.dictionary.PropertyDictionary.RESOURCE_PREFERRED; +import static org.folio.ld.dictionary.ResourceTypeDictionary.PERSON; +import static org.folio.linked.data.test.TestUtil.OBJECT_MAPPER; +import static org.folio.linked.data.test.TestUtil.randomLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Optional; +import java.util.UUID; +import org.folio.ld.dictionary.model.FolioMetadata; +import org.folio.linked.data.mapper.ResourceModelMapper; +import org.folio.linked.data.model.entity.Resource; +import org.folio.linked.data.model.entity.ResourceEdge; +import org.folio.linked.data.model.entity.event.ResourceCreatedEvent; +import org.folio.linked.data.model.entity.event.ResourceReplacedEvent; +import org.folio.linked.data.model.entity.event.ResourceUpdatedEvent; +import org.folio.linked.data.repo.FolioMetadataRepository; +import org.folio.linked.data.repo.ResourceRepository; +import org.folio.linked.data.service.resource.ResourceGraphService; +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +@UnitTest +@ExtendWith(MockitoExtension.class) +class ResourceMarcAuthorityServiceImplTest { + + @InjectMocks + private ResourceMarcAuthorityServiceImpl resourceMarcService; + + @Mock + private ResourceModelMapper resourceModelMapper; + @Mock + private ResourceRepository resourceRepo; + @Mock + private ResourceGraphService resourceGraphService; + @Mock + private ApplicationEventPublisher applicationEventPublisher; + @Mock + private FolioMetadataRepository folioMetadataRepo; + @Spy + private ObjectMapper objectMapper = OBJECT_MAPPER; + + @Test + void saveMarcAuthority_shouldCreateNewAuthority_ifGivenModelDoesNotExistsByIdAndSrsId() { + // given + var id = randomLong(); + var srsId = UUID.randomUUID().toString(); + var model = new org.folio.ld.dictionary.model.Resource().setId(id) + .setFolioMetadata(new FolioMetadata().setSrsId(srsId)); + var mapped = new Resource().setId(id).addTypes(PERSON); + mapped.setFolioMetadata(new org.folio.linked.data.model.entity.FolioMetadata(mapped).setSrsId(srsId)); + doReturn(mapped).when(resourceModelMapper).toEntity(model); + doReturn(false).when(resourceRepo).existsById(id); + doReturn(mapped).when(resourceGraphService).saveMergingGraph(mapped); + + // when + var result = resourceMarcService.saveMarcResource(model); + + // then + assertThat(result).isEqualTo(id); + verify(resourceGraphService).saveMergingGraph(mapped); + verify(applicationEventPublisher).publishEvent(new ResourceCreatedEvent(mapped)); + } + + @Test + void saveMarcAuthority_shouldUpdateAuthority_ifGivenModelExistsById() { + // given + var id = randomLong(); + var srsId = UUID.randomUUID().toString(); + var model = new org.folio.ld.dictionary.model.Resource().setId(id) + .setFolioMetadata(new FolioMetadata().setSrsId(srsId)); + var mapped = new Resource().setId(id).addTypes(PERSON); + mapped.setFolioMetadata(new org.folio.linked.data.model.entity.FolioMetadata(mapped).setSrsId(srsId)); + doReturn(mapped).when(resourceModelMapper).toEntity(model); + doReturn(true).when(resourceRepo).existsById(id); + doReturn(mapped).when(resourceGraphService).saveMergingGraph(mapped); + + // when + var result = resourceMarcService.saveMarcResource(model); + + // then + assertThat(result).isEqualTo(id); + verify(resourceGraphService).saveMergingGraph(mapped); + verify(applicationEventPublisher).publishEvent(new ResourceUpdatedEvent(mapped)); + } + + @Test + void saveMarcAuthority_shouldCreateNewAuthorityVersionAndMarkOldAsObsolete_ifGivenModelExistsBySrsIdButNotById() { + // given + var id = randomLong(); + var srsId = UUID.randomUUID().toString(); + var existed = new Resource().setId(id).setManaged(true); + existed.setFolioMetadata(new org.folio.linked.data.model.entity.FolioMetadata(existed)); + doReturn(Optional.of(existed)).when(resourceRepo).findByFolioMetadataSrsId(srsId); + doReturn(true).when(folioMetadataRepo).existsBySrsId(srsId); + var model = new org.folio.ld.dictionary.model.Resource() + .setId(id) + .setFolioMetadata(new FolioMetadata().setSrsId(srsId)); + var mapped = new Resource().setId(id).addTypes(PERSON); + mapped.setFolioMetadata(new org.folio.linked.data.model.entity.FolioMetadata(mapped).setSrsId(srsId)); + doReturn(mapped).when(resourceModelMapper).toEntity(model); + doReturn(false).when(resourceRepo).existsById(id); + doReturn(existed).when(resourceRepo).save(existed); + + doReturn(mapped).when(resourceGraphService).saveMergingGraph(mapped); + + // when + var result = resourceMarcService.saveMarcResource(model); + + // then + assertThat(result).isEqualTo(id); + assertThat(existed.isActive()).isFalse(); + assertThat(existed.getDoc().get(RESOURCE_PREFERRED.getValue()).get(0).textValue()).isEqualTo("false"); + assertThat(existed.getFolioMetadata()).isNull(); + verify(resourceRepo).save(existed); + verify(resourceGraphService).saveMergingGraph(mapped); + verify(applicationEventPublisher).publishEvent(new ResourceReplacedEvent(existed, mapped)); + assertThat(mapped.getDoc().get(RESOURCE_PREFERRED.getValue()).get(0).textValue()).isEqualTo("true"); + assertThat(mapped.getIncomingEdges()).contains(new ResourceEdge(existed, mapped, REPLACED_BY)); + } +} diff --git a/src/test/java/org/folio/linked/data/service/resource/ResourceMarcServiceTest.java b/src/test/java/org/folio/linked/data/service/resource/impl/ResourceMarcBibServiceImplTest.java similarity index 63% rename from src/test/java/org/folio/linked/data/service/resource/ResourceMarcServiceTest.java rename to src/test/java/org/folio/linked/data/service/resource/impl/ResourceMarcBibServiceImplTest.java index c80f5e17..8cfa5c5e 100644 --- a/src/test/java/org/folio/linked/data/service/resource/ResourceMarcServiceTest.java +++ b/src/test/java/org/folio/linked/data/service/resource/impl/ResourceMarcBibServiceImplTest.java @@ -1,10 +1,7 @@ -package org.folio.linked.data.service.resource; +package org.folio.linked.data.service.resource.impl; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.folio.ld.dictionary.PredicateDictionary.REPLACED_BY; -import static org.folio.ld.dictionary.PropertyDictionary.RESOURCE_PREFERRED; -import static org.folio.ld.dictionary.ResourceTypeDictionary.PERSON; import static org.folio.linked.data.model.entity.ResourceSource.LINKED_DATA; import static org.folio.linked.data.test.MonographTestUtil.getSampleInstanceResource; import static org.folio.linked.data.test.MonographTestUtil.getSampleWork; @@ -14,7 +11,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,14 +31,11 @@ import org.folio.linked.data.mapper.ResourceModelMapper; import org.folio.linked.data.mapper.dto.ResourceDtoMapper; import org.folio.linked.data.model.entity.Resource; -import org.folio.linked.data.model.entity.ResourceEdge; -import org.folio.linked.data.model.entity.event.ResourceCreatedEvent; import org.folio.linked.data.model.entity.event.ResourceEvent; -import org.folio.linked.data.model.entity.event.ResourceReplacedEvent; import org.folio.linked.data.model.entity.event.ResourceUpdatedEvent; import org.folio.linked.data.repo.FolioMetadataRepository; -import org.folio.linked.data.repo.ResourceEdgeRepository; import org.folio.linked.data.repo.ResourceRepository; +import org.folio.linked.data.service.resource.ResourceGraphService; import org.folio.marc4ld.service.ld2marc.Bibframe2MarcMapper; import org.folio.marc4ld.service.marc2ld.bib.MarcBib2ldMapper; import org.folio.rest.jaxrs.model.ParsedRecord; @@ -55,7 +48,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatusCode; @@ -63,18 +55,16 @@ @UnitTest @ExtendWith(MockitoExtension.class) -class ResourceMarcServiceTest { +class ResourceMarcBibServiceImplTest { @InjectMocks - private ResourceMarcServiceImpl resourceMarcService; + private ResourceMarcBibServiceImpl resourceMarcService; @Mock private FolioMetadataRepository folioMetadataRepo; @Mock private ResourceRepository resourceRepo; @Mock - private ResourceEdgeRepository edgeRepo; - @Mock private ResourceDtoMapper resourceDtoMapper; @Mock private ResourceModelMapper resourceModelMapper; @@ -86,7 +76,7 @@ class ResourceMarcServiceTest { private ApplicationEventPublisher applicationEventPublisher; @Mock private ResourceGraphService resourceGraphService; - @Spy + @Mock private ObjectMapper objectMapper = OBJECT_MAPPER; @Mock private SrsClient srsClient; @@ -143,156 +133,6 @@ void getResourceMarcView_shouldThrowException_ifNotInstance() { .isThrownBy(() -> resourceMarcService.getResourceMarcView(notExistedId)); } - @Test - void saveMarcResource_shouldCreateNewBib_ifGivenModelDoesNotExistsByIdAndSrsId() { - // given - var id = randomLong(); - var srsId = UUID.randomUUID().toString(); - var model = new org.folio.ld.dictionary.model.Resource().setId(id) - .setFolioMetadata(new FolioMetadata().setSrsId(srsId)); - var mapped = new Resource().setId(id); - mapped.setFolioMetadata(new org.folio.linked.data.model.entity.FolioMetadata(mapped).setSrsId(srsId)); - doReturn(mapped).when(resourceModelMapper).toEntity(model); - doReturn(false).when(resourceRepo).existsById(id); - doReturn(mapped).when(resourceGraphService).saveMergingGraph(mapped); - - // when - var result = resourceMarcService.saveMarcResource(model); - - // then - assertThat(result).isEqualTo(id); - verify(resourceGraphService).saveMergingGraph(mapped); - verify(applicationEventPublisher).publishEvent(new ResourceCreatedEvent(mapped)); - } - - @Test - void saveMarcResource_shouldUpdateBib_ifGivenModelExistsById() { - // given - var id = randomLong(); - var srsId = UUID.randomUUID().toString(); - var model = new org.folio.ld.dictionary.model.Resource().setId(id) - .setFolioMetadata(new FolioMetadata().setSrsId(srsId)); - var mapped = new Resource().setId(id); - mapped.setFolioMetadata(new org.folio.linked.data.model.entity.FolioMetadata(mapped).setSrsId(srsId)); - doReturn(mapped).when(resourceModelMapper).toEntity(model); - doReturn(true).when(resourceRepo).existsById(id); - doReturn(mapped).when(resourceGraphService).saveMergingGraph(mapped); - - // when - var result = resourceMarcService.saveMarcResource(model); - - // then - assertThat(result).isEqualTo(id); - verify(resourceGraphService).saveMergingGraph(mapped); - verify(applicationEventPublisher).publishEvent(new ResourceUpdatedEvent(mapped)); - } - - @Test - void saveMarcResource_shouldReplaceBib_ifGivenModelExistsBySrsIdButNotById() { - // given - var id = randomLong(); - var srsId = UUID.randomUUID().toString(); - var existed = new Resource().setId(id).setManaged(true); - doReturn(Optional.of(existed)).when(resourceRepo).findByFolioMetadataSrsId(srsId); - doReturn(true).when(folioMetadataRepo).existsBySrsId(srsId); - var model = new org.folio.ld.dictionary.model.Resource() - .setId(id) - .setFolioMetadata(new FolioMetadata().setSrsId(srsId)); - var mapped = new Resource().setId(id); - mapped.setFolioMetadata(new org.folio.linked.data.model.entity.FolioMetadata(mapped).setSrsId(srsId)); - doReturn(mapped).when(resourceModelMapper).toEntity(model); - doReturn(false).when(resourceRepo).existsById(id); - - doReturn(mapped).when(resourceGraphService).saveMergingGraph(mapped); - - // when - var result = resourceMarcService.saveMarcResource(model); - - // then - assertThat(result).isEqualTo(id); - verify(resourceGraphService).saveMergingGraph(mapped); - verify(applicationEventPublisher).publishEvent(new ResourceReplacedEvent(existed, mapped)); - } - - @Test - void saveMarcAuthority_shouldCreateNewAuthority_ifGivenModelDoesNotExistsByIdAndSrsId() { - // given - var id = randomLong(); - var srsId = UUID.randomUUID().toString(); - var model = new org.folio.ld.dictionary.model.Resource().setId(id) - .setFolioMetadata(new FolioMetadata().setSrsId(srsId)); - var mapped = new Resource().setId(id).addTypes(PERSON); - mapped.setFolioMetadata(new org.folio.linked.data.model.entity.FolioMetadata(mapped).setSrsId(srsId)); - doReturn(mapped).when(resourceModelMapper).toEntity(model); - doReturn(false).when(resourceRepo).existsById(id); - doReturn(mapped).when(resourceGraphService).saveMergingGraph(mapped); - - // when - var result = resourceMarcService.saveMarcResource(model); - - // then - assertThat(result).isEqualTo(id); - verify(resourceGraphService).saveMergingGraph(mapped); - verify(applicationEventPublisher).publishEvent(new ResourceCreatedEvent(mapped)); - } - - @Test - void saveMarcAuthority_shouldUpdateAuthority_ifGivenModelExistsById() { - // given - var id = randomLong(); - var srsId = UUID.randomUUID().toString(); - var model = new org.folio.ld.dictionary.model.Resource().setId(id) - .setFolioMetadata(new FolioMetadata().setSrsId(srsId)); - var mapped = new Resource().setId(id).addTypes(PERSON); - mapped.setFolioMetadata(new org.folio.linked.data.model.entity.FolioMetadata(mapped).setSrsId(srsId)); - doReturn(mapped).when(resourceModelMapper).toEntity(model); - doReturn(true).when(resourceRepo).existsById(id); - doReturn(mapped).when(resourceGraphService).saveMergingGraph(mapped); - - // when - var result = resourceMarcService.saveMarcResource(model); - - // then - assertThat(result).isEqualTo(id); - verify(resourceGraphService).saveMergingGraph(mapped); - verify(applicationEventPublisher).publishEvent(new ResourceUpdatedEvent(mapped)); - } - - @Test - void saveMarcAuthority_shouldCreateNewAuthorityVersionAndMarkOldAsObsolete_ifGivenModelExistsBySrsIdButNotById() { - // given - var id = randomLong(); - var srsId = UUID.randomUUID().toString(); - var existed = new Resource().setId(id).setManaged(true); - existed.setFolioMetadata(new org.folio.linked.data.model.entity.FolioMetadata(existed)); - doReturn(Optional.of(existed)).when(resourceRepo).findByFolioMetadataSrsId(srsId); - doReturn(true).when(folioMetadataRepo).existsBySrsId(srsId); - var model = new org.folio.ld.dictionary.model.Resource() - .setId(id) - .setFolioMetadata(new FolioMetadata().setSrsId(srsId)); - var mapped = new Resource().setId(id).addTypes(PERSON); - mapped.setFolioMetadata(new org.folio.linked.data.model.entity.FolioMetadata(mapped).setSrsId(srsId)); - doReturn(mapped).when(resourceModelMapper).toEntity(model); - doReturn(false).when(resourceRepo).existsById(id); - doReturn(existed).when(resourceRepo).save(existed); - - doReturn(mapped).when(resourceGraphService).saveMergingGraph(mapped); - - // when - var result = resourceMarcService.saveMarcResource(model); - - // then - assertThat(result).isEqualTo(id); - assertThat(existed.isActive()).isFalse(); - assertThat(existed.getDoc().get(RESOURCE_PREFERRED.getValue()).get(0).textValue()).isEqualTo("false"); - assertThat(existed.getFolioMetadata()).isNull(); - verify(resourceRepo).save(existed); - verify(resourceGraphService).saveMergingGraph(mapped); - verify(applicationEventPublisher).publishEvent(new ResourceReplacedEvent(existed, mapped)); - assertThat(mapped.getDoc().get(RESOURCE_PREFERRED.getValue()).get(0).textValue()).isEqualTo("true"); - assertThat(mapped.getIncomingEdges()).contains(new ResourceEdge(existed, mapped, REPLACED_BY)); - } - @ParameterizedTest @CsvSource({ "a, a", diff --git a/src/test/java/org/folio/linked/data/service/resource/ResourceServiceTest.java b/src/test/java/org/folio/linked/data/service/resource/impl/ResourceServiceImplTest.java similarity index 98% rename from src/test/java/org/folio/linked/data/service/resource/ResourceServiceTest.java rename to src/test/java/org/folio/linked/data/service/resource/impl/ResourceServiceImplTest.java index 1a86ac61..c82bf069 100644 --- a/src/test/java/org/folio/linked/data/service/resource/ResourceServiceTest.java +++ b/src/test/java/org/folio/linked/data/service/resource/impl/ResourceServiceImplTest.java @@ -1,4 +1,4 @@ -package org.folio.linked.data.service.resource; +package org.folio.linked.data.service.resource.impl; import static org.assertj.core.api.Assertions.assertThat; import static org.folio.ld.dictionary.PredicateDictionary.INSTANTIATES; @@ -39,6 +39,7 @@ import org.folio.linked.data.repo.FolioMetadataRepository; import org.folio.linked.data.repo.ResourceEdgeRepository; import org.folio.linked.data.repo.ResourceRepository; +import org.folio.linked.data.service.resource.ResourceGraphService; import org.folio.linked.data.service.resource.meta.MetadataService; import org.folio.spring.testing.type.UnitTest; import org.junit.jupiter.api.Test; @@ -51,7 +52,7 @@ @UnitTest @ExtendWith(MockitoExtension.class) -class ResourceServiceTest { +class ResourceServiceImplTest { @InjectMocks private ResourceServiceImpl resourceService;