diff --git a/src/main/java/org/folio/linked/data/service/resource/ResourceServiceImpl.java b/src/main/java/org/folio/linked/data/service/resource/ResourceServiceImpl.java index a3860eaf..a8b18699 100644 --- a/src/main/java/org/folio/linked/data/service/resource/ResourceServiceImpl.java +++ b/src/main/java/org/folio/linked/data/service/resource/ResourceServiceImpl.java @@ -1,11 +1,9 @@ package org.folio.linked.data.service.resource; -import static org.folio.ld.dictionary.ResourceTypeDictionary.INSTANCE; import static org.folio.linked.data.util.ResourceUtils.getPrimaryMainTitles; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -16,7 +14,6 @@ import org.folio.linked.data.domain.dto.WorkField; import org.folio.linked.data.exception.RequestProcessingExceptionBuilder; import org.folio.linked.data.mapper.dto.ResourceDtoMapper; -import org.folio.linked.data.model.entity.RawMarc; import org.folio.linked.data.model.entity.Resource; import org.folio.linked.data.model.entity.event.ResourceCreatedEvent; import org.folio.linked.data.model.entity.event.ResourceDeletedEvent; @@ -24,7 +21,7 @@ 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.edge.ResourceEdgeService; +import org.folio.linked.data.service.resource.copy.ResourceCopyService; import org.folio.linked.data.service.resource.graph.ResourceGraphService; import org.folio.linked.data.service.resource.meta.MetadataService; import org.folio.spring.FolioExecutionContext; @@ -46,8 +43,8 @@ public class ResourceServiceImpl implements ResourceService { private final FolioMetadataRepository folioMetadataRepo; private final RequestProcessingExceptionBuilder exceptionBuilder; private final ApplicationEventPublisher applicationEventPublisher; - private final ResourceEdgeService resourceEdgeService; private final FolioExecutionContext folioExecutionContext; + private final ResourceCopyService resourceCopyService; @Override public ResourceResponseDto createResource(ResourceRequestDto resourceDto) { @@ -113,9 +110,8 @@ private Resource getResource(Long id) { private Resource saveNewResource(ResourceRequestDto resourceDto, Resource old) { var mapped = resourceDtoMapper.toEntity(resourceDto); - resourceEdgeService.copyOutgoingEdges(old, mapped); + resourceCopyService.copyEdgesAndProperties(old, mapped); metadataService.ensure(mapped, old.getFolioMetadata()); - copyUnmappedMarc(old, mapped); mapped.setCreatedDate(old.getCreatedDate()); mapped.setVersion(old.getVersion() + 1); mapped.setCreatedBy(old.getCreatedBy()); @@ -136,14 +132,4 @@ private String toLogString(ResourceRequestDto resourceDto) { return String.format("Type: %s, Title: %s", type, titles); } - private void copyUnmappedMarc(Resource old, Resource mapped) { - if (mapped.isOfType(INSTANCE)) { - Optional.ofNullable(old.getUnmappedMarc()) - .ifPresent(unmappedMarc -> { - var newUnmappedMarc = new RawMarc(mapped).setContent(unmappedMarc.getContent()); - mapped.setUnmappedMarc(newUnmappedMarc); - }); - } - } - } diff --git a/src/main/java/org/folio/linked/data/service/resource/copy/ResourceCopyService.java b/src/main/java/org/folio/linked/data/service/resource/copy/ResourceCopyService.java new file mode 100644 index 00000000..f6679999 --- /dev/null +++ b/src/main/java/org/folio/linked/data/service/resource/copy/ResourceCopyService.java @@ -0,0 +1,8 @@ +package org.folio.linked.data.service.resource.copy; + +import org.folio.linked.data.model.entity.Resource; + +public interface ResourceCopyService { + + void copyEdgesAndProperties(Resource old, Resource updated); +} diff --git a/src/main/java/org/folio/linked/data/service/resource/copy/ResourceCopyServiceImpl.java b/src/main/java/org/folio/linked/data/service/resource/copy/ResourceCopyServiceImpl.java new file mode 100644 index 00000000..1f2d0d4e --- /dev/null +++ b/src/main/java/org/folio/linked/data/service/resource/copy/ResourceCopyServiceImpl.java @@ -0,0 +1,102 @@ +package org.folio.linked.data.service.resource.copy; + +import static java.util.Collections.emptySet; +import static org.folio.ld.dictionary.PropertyDictionary.CITATION_COVERAGE; +import static org.folio.ld.dictionary.PropertyDictionary.CREDITS_NOTE; +import static org.folio.ld.dictionary.PropertyDictionary.DATES_OF_PUBLICATION_NOTE; +import static org.folio.ld.dictionary.PropertyDictionary.GEOGRAPHIC_COVERAGE; +import static org.folio.ld.dictionary.PropertyDictionary.GOVERNING_ACCESS_NOTE; +import static org.folio.ld.dictionary.PropertyDictionary.LOCATION_OF_ORIGINALS_DUPLICATES; +import static org.folio.ld.dictionary.PropertyDictionary.OTHER_EVENT_INFORMATION; +import static org.folio.ld.dictionary.PropertyDictionary.PARTICIPANT_NOTE; +import static org.folio.ld.dictionary.PropertyDictionary.PUBLICATION_FREQUENCY; +import static org.folio.ld.dictionary.PropertyDictionary.REFERENCES; +import static org.folio.ld.dictionary.ResourceTypeDictionary.INSTANCE; +import static org.folio.ld.dictionary.ResourceTypeDictionary.WORK; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.folio.linked.data.model.entity.RawMarc; +import org.folio.linked.data.model.entity.Resource; +import org.folio.linked.data.service.resource.edge.ResourceEdgeService; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class ResourceCopyServiceImpl implements ResourceCopyService { + + private static final Map> PROPERTIES_TO_BE_COPIED = Map.of( + INSTANCE.getUri(), Set.of( + PUBLICATION_FREQUENCY.getValue(), + DATES_OF_PUBLICATION_NOTE.getValue(), + GOVERNING_ACCESS_NOTE.getValue(), + CREDITS_NOTE.getValue(), + PARTICIPANT_NOTE.getValue(), + CITATION_COVERAGE.getValue(), + LOCATION_OF_ORIGINALS_DUPLICATES.getValue() + ), + WORK.getUri(), Set.of( + REFERENCES.getValue(), + OTHER_EVENT_INFORMATION.getValue(), + GEOGRAPHIC_COVERAGE.getValue() + ) + ); + + private final ResourceEdgeService resourceEdgeService; + private final ObjectMapper objectMapper; + + @Override + public void copyEdgesAndProperties(Resource old, Resource updated) { + resourceEdgeService.copyOutgoingEdges(old, updated); + copyUnmappedMarc(old, updated); + copyProperties(old, updated); + } + + private void copyUnmappedMarc(Resource from, Resource to) { + if (to.isOfType(INSTANCE)) { + Optional.ofNullable(from.getUnmappedMarc()) + .ifPresent(unmappedMarc -> { + var newUnmappedMarc = new RawMarc(to).setContent(unmappedMarc.getContent()); + to.setUnmappedMarc(newUnmappedMarc); + }); + } + } + + @SneakyThrows + private void copyProperties(Resource from, Resource to) { + if (from.getDoc() == null) { + return; + } + var properties = getProperties(from); + if (!properties.isEmpty()) { + var toDoc = to.getDoc() == null + ? new HashMap>() + : objectMapper.treeToValue(to.getDoc(), new TypeReference>>() {}); + properties.forEach(entry -> toDoc.put(entry.getKey(), entry.getValue())); + to.setDoc(objectMapper.convertValue(toDoc, JsonNode.class)); + } + } + + private List>> getProperties(Resource from) throws JsonProcessingException { + var fromDoc = objectMapper.treeToValue(from.getDoc(), new TypeReference>>() {}); + var fromType = from.getTypes() + .stream() + .findFirst() + .orElseThrow() + .getUri(); + var propertiesToCopy = PROPERTIES_TO_BE_COPIED.getOrDefault(fromType, emptySet()); + return fromDoc.entrySet() + .stream() + .filter(entry -> propertiesToCopy.contains(entry.getKey())) + .toList(); + } +} diff --git a/src/test/java/org/folio/linked/data/service/resource/ResourceServiceImplTest.java b/src/test/java/org/folio/linked/data/service/resource/ResourceServiceImplTest.java index 4bff7958..963eeca6 100644 --- a/src/test/java/org/folio/linked/data/service/resource/ResourceServiceImplTest.java +++ b/src/test/java/org/folio/linked/data/service/resource/ResourceServiceImplTest.java @@ -8,8 +8,6 @@ import static org.folio.linked.data.test.TestUtil.emptyRequestProcessingException; import static org.folio.linked.data.test.TestUtil.random; import static org.folio.linked.data.test.TestUtil.randomLong; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -33,7 +31,6 @@ import org.folio.linked.data.exception.RequestProcessingException; import org.folio.linked.data.exception.RequestProcessingExceptionBuilder; import org.folio.linked.data.mapper.dto.ResourceDtoMapper; -import org.folio.linked.data.model.entity.RawMarc; 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; @@ -42,7 +39,7 @@ 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.edge.ResourceEdgeService; +import org.folio.linked.data.service.resource.copy.ResourceCopyService; import org.folio.linked.data.service.resource.graph.ResourceGraphService; import org.folio.linked.data.service.resource.meta.MetadataService; import org.folio.spring.FolioExecutionContext; @@ -77,9 +74,9 @@ class ResourceServiceImplTest { @Mock private RequestProcessingExceptionBuilder exceptionBuilder; @Mock - private ResourceEdgeService resourceEdgeService; - @Mock private FolioExecutionContext folioExecutionContext; + @Mock + private ResourceCopyService resourceCopyService; @Test void create_shouldPersistMappedResourceAndNotPublishResourceCreatedEvent_forResourceWithNoWork() { @@ -235,6 +232,7 @@ void update_shouldSaveUpdatedResourceAndSendResourceUpdatedEvent_forResourceWith verify(resourceGraphService).saveMergingGraph(work); verify(folioExecutionContext).getUserId(); verify(applicationEventPublisher).publishEvent(new ResourceUpdatedEvent(work)); + verify(resourceCopyService).copyEdgesAndProperties(oldWork, work); } @Test @@ -263,30 +261,7 @@ void update_shouldSaveUpdatedResourceAndSendReplaceEvent_forResourceWithDifferen verify(resourceGraphService).breakEdgesAndDelete(oldInstance); verify(resourceGraphService).saveMergingGraph(mapped); verify(applicationEventPublisher).publishEvent(new ResourceReplacedEvent(oldInstance, mapped.getId())); - } - - @Test - void update_shouldRetainUnmappedMarc() { - // given - var oldId = randomLong(); - var oldInstance = new Resource() - .setId(oldId) - .addTypes(INSTANCE); - var rawMarc = "raw marc"; - var unmappedMarc = new RawMarc(oldInstance).setContent(rawMarc); - oldInstance.setUnmappedMarc(unmappedMarc); - when(resourceRepo.findById(oldId)).thenReturn(Optional.of(oldInstance)); - var mapped = new Resource().addTypes(INSTANCE); - var instanceDto = new ResourceRequestDto(); - when(resourceDtoMapper.toEntity(instanceDto)).thenReturn(mapped); - when(resourceGraphService.saveMergingGraph(mapped)).thenReturn(new Resource()); - - // when - resourceService.updateResource(oldId, instanceDto); - - // then - assertNotNull(mapped.getUnmappedMarc()); - assertEquals(rawMarc, mapped.getUnmappedMarc().getContent()); + verify(resourceCopyService).copyEdgesAndProperties(oldInstance, mapped); } @Test diff --git a/src/test/java/org/folio/linked/data/service/resource/copy/ResourceCopyServiceImplTest.java b/src/test/java/org/folio/linked/data/service/resource/copy/ResourceCopyServiceImplTest.java new file mode 100644 index 00000000..5db537fe --- /dev/null +++ b/src/test/java/org/folio/linked/data/service/resource/copy/ResourceCopyServiceImplTest.java @@ -0,0 +1,160 @@ +package org.folio.linked.data.service.resource.copy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.ld.dictionary.ResourceTypeDictionary.INSTANCE; +import static org.folio.ld.dictionary.ResourceTypeDictionary.WORK; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Stream; +import org.folio.linked.data.model.entity.RawMarc; +import org.folio.linked.data.model.entity.Resource; +import org.folio.linked.data.model.entity.ResourceTypeEntity; +import org.folio.linked.data.service.resource.edge.ResourceEdgeService; +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@UnitTest +class ResourceCopyServiceImplTest { + + @InjectMocks + private ResourceCopyServiceImpl service; + + @Mock + private ResourceEdgeService resourceEdgeService; + @Mock + private ObjectMapper objectMapper; + + @Test + void copyEdgesAndProperties_shouldRetainUnmappedMarc_whenUpdatedResourceIsInstance() { + // given + var old = new Resource(); + var rawMarc = "raw marc"; + old.setUnmappedMarc(new RawMarc(old).setContent(rawMarc)); + var updated = new Resource().addTypes(INSTANCE); + + // when + service.copyEdgesAndProperties(old, updated); + + // then + assertNotNull(updated.getUnmappedMarc()); + assertEquals(rawMarc, updated.getUnmappedMarc().getContent()); + verify(resourceEdgeService).copyOutgoingEdges(old, updated); + } + + @Test + void copyEdgesAndProperties_shouldNotRetainUnmappedMarc_whenUpdatedResourceIsNotInstance() { + // given + var old = new Resource(); + old.setUnmappedMarc(new RawMarc(old).setContent("raw marc")); + var updated = new Resource(); + + // when + service.copyEdgesAndProperties(old, updated); + + // then + assertNull(updated.getUnmappedMarc()); + verify(resourceEdgeService).copyOutgoingEdges(old, updated); + } + + @Test + void copyEdgesAndProperties_shouldNotCopyProperties_whenOldResourceDoesNotHaveDoc() { + // given + var old = new Resource(); + var updated = new Resource(); + + // when + service.copyEdgesAndProperties(old, updated); + + // then + verifyNoInteractions(objectMapper); + verify(resourceEdgeService).copyOutgoingEdges(old, updated); + } + + private static Stream dataProvider() { + return Stream.of( + arguments(new Resource() + .addType(new ResourceTypeEntity().setUri(INSTANCE.getUri())).setDoc(new ArrayNode(new JsonNodeFactory(true))), + new Resource(), getOldInstanceDoc(), getUpdatedInstanceDoc()), + arguments(new Resource() + .addType(new ResourceTypeEntity().setUri(WORK.getUri())).setDoc(new ArrayNode(new JsonNodeFactory(true))), + new Resource(), getOldWorkDoc(), getUpdatedWorkDoc()) + ); + } + + @ParameterizedTest + @MethodSource("dataProvider") + void copyEdgesAndProperties_shouldRetainProperties_thatAreNotSupportedOnUi(Resource old, + Resource updated, + HashMap> fromDoc, + HashMap> expectedDoc) + throws JsonProcessingException { + // given + when(objectMapper.treeToValue(eq(old.getDoc()), any(TypeReference.class))).thenReturn(fromDoc); + + // when + service.copyEdgesAndProperties(old, updated); + + // then + var docCaptor = ArgumentCaptor.forClass(HashMap.class); + verify(objectMapper).convertValue(docCaptor.capture(), eq(JsonNode.class)); + assertThat(docCaptor.getValue()).isEqualTo(expectedDoc); + verify(resourceEdgeService).copyOutgoingEdges(old, updated); + } + + private static HashMap> getOldInstanceDoc() { + var doc = getUpdatedInstanceDoc(); + doc.put("http://bibfra.me/vocab/lite/note", List.of("generalNote")); + return doc; + } + + private static HashMap> getUpdatedInstanceDoc() { + var doc = new HashMap>(); + doc.put("http://bibfra.me/vocab/marc/publicationFrequency", List.of("publicationFrequency")); + doc.put("http://bibfra.me/vocab/marc/datesOfPublicationNote", List.of("datesOfPublicationNote")); + doc.put("http://bibfra.me/vocab/marc/governingAccessNote", List.of("governingAccessNote")); + doc.put("http://bibfra.me/vocab/marc/creditsNote", List.of("creditsNote")); + doc.put("http://bibfra.me/vocab/marc/participantNote", List.of("participantNote")); + doc.put("http://bibfra.me/vocab/marc/citationCoverage", List.of("citationCoverage")); + doc.put("http://bibfra.me/vocab/marc/locationOfOriginalsDuplicates", List.of("locationOfOriginalsDuplicates")); + return doc; + } + + private static HashMap> getOldWorkDoc() { + var doc = getUpdatedWorkDoc(); + doc.put("http://bibfra.me/vocab/lite/note", List.of("generalNote")); + return doc; + } + + private static HashMap> getUpdatedWorkDoc() { + var doc = new HashMap>(); + doc.put("http://bibfra.me/vocab/marc/references", List.of("references")); + doc.put("http://bibfra.me/vocab/marc/otherEventInformation", List.of("otherEventInformation")); + doc.put("http://bibfra.me/vocab/marc/geographicCoverage", List.of("geographicCoverage")); + return doc; + } +}