From cb45c5717af15cc49e7c7dd1c6469f98b3f91059 Mon Sep 17 00:00:00 2001 From: PK Jacob Date: Wed, 15 Jan 2025 15:15:58 -0500 Subject: [PATCH] MODLD-642: Retain edges when resource is fetched from DB --- .../linked/data/model/entity/Resource.java | 11 - .../ResourceMarcAuthorityServiceImpl.java | 4 +- .../ResourceControllerUpdateWorkIT.java | 207 ++++++++++++++++++ 3 files changed, 208 insertions(+), 14 deletions(-) create mode 100644 src/test/java/org/folio/linked/data/e2e/resource/ResourceControllerUpdateWorkIT.java diff --git a/src/main/java/org/folio/linked/data/model/entity/Resource.java b/src/main/java/org/folio/linked/data/model/entity/Resource.java index dffad050..fdfa424e 100644 --- a/src/main/java/org/folio/linked/data/model/entity/Resource.java +++ b/src/main/java/org/folio/linked/data/model/entity/Resource.java @@ -147,17 +147,6 @@ public Resource(@NonNull Resource that) { .orElse(null); } - public static Resource copyWithNoEdges(@NonNull Resource that) { - return new Resource() - .setId(that.id) - .setLabel(that.label) - .setDoc((JsonNode) ofNullable(that.getDoc()).map(JsonNode::deepCopy).orElse(null)) - .setIndexDate(that.indexDate) - .setTypes(new LinkedHashSet<>(that.getTypes())) - .setIncomingEdges(new LinkedHashSet<>()) - .setOutgoingEdges(new LinkedHashSet<>()); - } - @Override public boolean isNew() { return !managed; diff --git a/src/main/java/org/folio/linked/data/service/resource/marc/ResourceMarcAuthorityServiceImpl.java b/src/main/java/org/folio/linked/data/service/resource/marc/ResourceMarcAuthorityServiceImpl.java index 5f60e28d..e6e37834 100644 --- a/src/main/java/org/folio/linked/data/service/resource/marc/ResourceMarcAuthorityServiceImpl.java +++ b/src/main/java/org/folio/linked/data/service/resource/marc/ResourceMarcAuthorityServiceImpl.java @@ -73,8 +73,7 @@ private Optional fetchResourceFromRepo(Identifiable identifiable) { .flatMap(id -> resourceRepo.findById(parseLong(id))) .map(ResourceUtils::ensureLatestReplaced) .or(() -> Optional.ofNullable(identifiable.getSrsId()) - .flatMap(resourceRepo::findByFolioMetadataSrsId)) - .map(Resource::copyWithNoEdges); + .flatMap(resourceRepo::findByFolioMetadataSrsId)); } private Resource createResourceFromSrs(String srsId) { @@ -83,7 +82,6 @@ private Resource createResourceFromSrs(String srsId) { .flatMap(this::contentAsJsonString) .flatMap(this::firstAuthorityToEntity) .map(resourceGraphService::saveMergingGraph) - .map(Resource::copyWithNoEdges) .orElseThrow(() -> notFoundException(srsId)); } catch (FeignException.NotFound e) { throw notFoundException(srsId); diff --git a/src/test/java/org/folio/linked/data/e2e/resource/ResourceControllerUpdateWorkIT.java b/src/test/java/org/folio/linked/data/e2e/resource/ResourceControllerUpdateWorkIT.java new file mode 100644 index 00000000..103736a6 --- /dev/null +++ b/src/test/java/org/folio/linked/data/e2e/resource/ResourceControllerUpdateWorkIT.java @@ -0,0 +1,207 @@ +package org.folio.linked.data.e2e.resource; + +import static org.folio.linked.data.e2e.resource.ResourceControllerITBase.RESOURCE_URL; +import static org.folio.linked.data.test.TestUtil.awaitAndAssert; +import static org.folio.linked.data.test.TestUtil.defaultHeaders; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.folio.ld.dictionary.PredicateDictionary; +import org.folio.ld.dictionary.ResourceTypeDictionary; +import org.folio.linked.data.domain.dto.InstanceIngressEvent; +import org.folio.linked.data.e2e.base.IntegrationTest; +import org.folio.linked.data.model.entity.FolioMetadata; +import org.folio.linked.data.model.entity.Resource; +import org.folio.linked.data.model.entity.ResourceEdge; +import org.folio.linked.data.service.resource.hash.HashService; +import org.folio.linked.data.test.kafka.KafkaInventoryTopicListener; +import org.folio.linked.data.test.kafka.KafkaProducerTestConfiguration; +import org.folio.linked.data.test.resource.ResourceTestService; +import org.folio.marc4ld.service.marc2ld.reader.MarcReaderProcessor; +import org.junit.jupiter.api.Test; +import org.marc4j.marc.DataField; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.test.web.servlet.MockMvc; + +@IntegrationTest +@SpringBootTest(classes = {KafkaProducerTestConfiguration.class}) +class ResourceControllerUpdateWorkIT { + @Autowired + private ResourceTestService resourceTestService; + @Autowired + private ObjectMapper objectMapper; + @Autowired + private HashService hashService; + @Autowired + private Environment env; + @Autowired + private MockMvc mockMvc; + @Autowired + private KafkaInventoryTopicListener inventoryTopicListener; + @Autowired + private MarcReaderProcessor marcReader; + + @Test + void updateWork_should_send_update_instance_event_to_inventory() throws Exception { + // given + var person = getPerson(); + resourceTestService.saveGraph(person); + var work = getWork(); + var instance = getInstance(work); + resourceTestService.saveGraph(instance); + + // when + var workUpdateRequestDto = getWorkRequestDto(person.getId(), instance.getId()); + + var updateRequest = put(RESOURCE_URL + "/" + work.getId()) + .contentType(APPLICATION_JSON) + .headers(defaultHeaders(env)) + .content(workUpdateRequestDto); + + mockMvc.perform(updateRequest).andExpect(status().isOk()); + + // then + awaitAndAssert(() -> + assertTrue(inventoryTopicListener.getMessages().stream() + .anyMatch(m -> isExpectedEvent(m, instance.getId())) + ) + ); + } + + private String getWorkRequestDto(Long personId, Long instanceId) { + return """ + { + "resource": { + "http://bibfra.me/vocab/lite/Work": { + "http://bibfra.me/vocab/marc/title": [ + { + "http://bibfra.me/vocab/marc/Title": { + "http://bibfra.me/vocab/marc/mainTitle": [ "simple_work" ] + } + } + ], + "_creatorReference": [ { "id": "%PERSON_ID%" } ], + "_instanceReference": [ { "id": "%INSTANCE_ID%"} ] + } + } + } + """ + .replace("%PERSON_ID%", personId.toString()) + .replace("%INSTANCE_ID%", instanceId.toString()); + } + + private Resource getWork() { + var title = new Resource() + .addTypes(ResourceTypeDictionary.TITLE) + .setDoc(getDoc(""" + { + "http://bibfra.me/vocab/marc/mainTitle": ["simple_work"] + } + """)) + .setLabel("simple_work"); + var work = new Resource() + .addTypes(ResourceTypeDictionary.WORK) + .setDoc(getDoc("{}")) + .setLabel("simple_work"); + + work.addOutgoingEdge(new ResourceEdge(work, title, PredicateDictionary.TITLE)); + + title.setId(hashService.hash(title)); + work.setId(hashService.hash(work)); + + return work; + } + + private Resource getInstance(Resource work) { + var title = new Resource() + .addTypes(ResourceTypeDictionary.TITLE) + .setDoc(getDoc(""" + { + "http://bibfra.me/vocab/marc/mainTitle": ["simple_instance"] + } + """)) + .setLabel("simple_instance"); + var instance = new Resource() + .addTypes(ResourceTypeDictionary.INSTANCE) + .setDoc(getDoc("{}")) + .setLabel("simple_instance"); + + instance.addOutgoingEdge(new ResourceEdge(instance, title, PredicateDictionary.TITLE)); + instance.addOutgoingEdge(new ResourceEdge(instance, work, PredicateDictionary.INSTANTIATES)); + + title.setId(hashService.hash(title)); + instance.setId(hashService.hash(instance)); + + return instance; + } + + private Resource getPerson() { + var person = new Resource() + .addTypes(ResourceTypeDictionary.PERSON) + .setDoc(getDoc(""" + { + "http://bibfra.me/vocab/lite/name": ["Person name"] + } + """)) + .setLabel("Person name"); + + var lccn = new Resource() + .addTypes(ResourceTypeDictionary.ID_LCCN, ResourceTypeDictionary.IDENTIFIER) + .setDoc(getDoc(""" + { + "http://bibfra.me/vocab/lite/link": ["n123456789"] + } + """)) + .setLabel("n123456789"); + + person.setFolioMetadata(new FolioMetadata(person).setInventoryId("123456789")); + person.addOutgoingEdge(new ResourceEdge(person, lccn, PredicateDictionary.MAP)); + + person.setId(hashService.hash(person)); + lccn.setId(hashService.hash(lccn)); + + return person; + } + + @SneakyThrows + private JsonNode getDoc(String doc) { + return objectMapper.readTree(doc); + } + + @SneakyThrows + private T parse(String json, Class clazz) { + return objectMapper.readValue(json, clazz); + } + + private boolean isExpectedEvent(String eventStr, long linkedDataId) { + var event = parse(eventStr, InstanceIngressEvent.class); + var eventPayload = event.getEventPayload(); + var marc = eventPayload.getSourceRecordObject(); + return event.getEventType() == InstanceIngressEvent.EventTypeEnum.UPDATE_INSTANCE + && eventPayload.getAdditionalProperties().get("linkedDataId").equals(linkedDataId) + && isExpectedMarc(marc); + } + + private boolean isExpectedMarc(String marcStr) { + var marc = marcReader.readMarc(marcStr).toList().get(0); + var df100 = (DataField) marc.getVariableField("100"); + var df245 = (DataField) marc.getVariableField("245"); + var isDf100Valid = df100.getSubfields().stream().anyMatch( + sf -> + sf.getCode() == 'a' && sf.getData().equals("Person name") + || sf.getCode() == '0' && sf.getData().equals("n123456789") + || sf.getCode() == '9' && sf.getData().equals("123456789") + ); + var isDf245Valid = df245.getSubfields().stream().anyMatch( + sf -> sf.getCode() == 'a' && sf.getData().equals("simple_instance") + ); + return isDf100Valid && isDf245Valid; + } +}