Skip to content

Commit 19663ec

Browse files
committed
MODLD-969: Index modified Hubs in mod-search
1 parent b3136f0 commit 19663ec

File tree

9 files changed

+390
-1
lines changed

9 files changed

+390
-1
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
- Set LINK of the identity resource based on FOLIO configuration [MODLD-923](https://folio-org.atlassian.net/browse/MODLD-923)
8484
- Include tenant ID in cache [MODLD-952](https://folio-org.atlassian.net/browse/MODLD-952)
8585
- Handle free form language text values in HUBs in GET, PUT /resource APIs [MODLD-977](https://folio-org.atlassian.net/browse/MODLD-977)
86+
- Index modified Hubs in mod-search [MODLD-969](https://folio-org.atlassian.net/browse/MODLD-969)
8687

8788
## 1.0.4 (04-24-2025)
8889
- Work Edit form - Instance read-only section: "Notes about the instance" data is not shown [MODLD-716](https://folio-org.atlassian.net/browse/MODLD-716)

src/main/java/org/folio/linked/data/integration/kafka/sender/search/HubCreateMessageSender.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class HubCreateMessageSender implements CreateMessageSender {
3333

3434
@Override
3535
public void accept(Resource resource) {
36+
log.debug("Publishing Index create message for HUB with ID [{}]", resource.getId());
3637
var message = mapper.toIndex(resource)
3738
.type(CREATE);
3839
hubIndexMessageProducer.sendMessages(singletonList(message));
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.folio.linked.data.integration.kafka.sender.search;
2+
3+
import static org.folio.ld.dictionary.ResourceTypeDictionary.HUB;
4+
import static org.folio.linked.data.domain.dto.ResourceIndexEventType.DELETE;
5+
import static org.folio.linked.data.util.Constants.STANDALONE_PROFILE;
6+
7+
import java.util.Collection;
8+
import java.util.List;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.log4j.Log4j2;
11+
import org.folio.linked.data.domain.dto.ResourceIndexEvent;
12+
import org.folio.linked.data.integration.kafka.sender.DeleteMessageSender;
13+
import org.folio.linked.data.mapper.kafka.search.HubSearchMessageMapper;
14+
import org.folio.linked.data.model.entity.Resource;
15+
import org.folio.spring.tools.kafka.FolioMessageProducer;
16+
import org.springframework.beans.factory.annotation.Qualifier;
17+
import org.springframework.context.annotation.Profile;
18+
import org.springframework.stereotype.Service;
19+
20+
@Log4j2
21+
@Service
22+
@RequiredArgsConstructor
23+
@Profile("!" + STANDALONE_PROFILE)
24+
public class HubDeleteMessageSender implements DeleteMessageSender {
25+
@Qualifier("hubIndexMessageProducer")
26+
private final FolioMessageProducer<ResourceIndexEvent> hubIndexMessageProducer;
27+
private final HubSearchMessageMapper mapper;
28+
29+
@Override
30+
public void accept(Resource resource) {
31+
log.debug("Publishing Index delete message for HUB with ID [{}]", resource.getId());
32+
var onlyIdResource = new Resource().setIdAndRefreshEdges(resource.getId());
33+
var indexMessage = mapper.toIndex(onlyIdResource).type(DELETE);
34+
hubIndexMessageProducer.sendMessages(List.of(indexMessage));
35+
}
36+
37+
@Override
38+
public Collection<Resource> apply(Resource resource) {
39+
if (resource.isOfType(HUB) && resource.getTypes().size() == 1) {
40+
return List.of(resource);
41+
}
42+
return List.of();
43+
}
44+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.folio.linked.data.integration.kafka.sender.search;
2+
3+
import static org.folio.ld.dictionary.ResourceTypeDictionary.HUB;
4+
import static org.folio.linked.data.util.Constants.STANDALONE_PROFILE;
5+
6+
import java.util.Collection;
7+
import java.util.List;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.log4j.Log4j2;
10+
import org.folio.linked.data.integration.kafka.sender.ReplaceMessageSender;
11+
import org.folio.linked.data.model.entity.Resource;
12+
import org.springframework.context.annotation.Profile;
13+
import org.springframework.data.util.Pair;
14+
import org.springframework.stereotype.Service;
15+
16+
@Log4j2
17+
@Service
18+
@RequiredArgsConstructor
19+
@Profile("!" + STANDALONE_PROFILE)
20+
public class HubReplaceMessageSender implements ReplaceMessageSender {
21+
22+
private final HubCreateMessageSender hubCreateMessageSender;
23+
private final HubDeleteMessageSender hubDeleteMessageSender;
24+
25+
@Override
26+
public Collection<Pair<Resource, Resource>> apply(Resource previous, Resource current) {
27+
if (current.isOfType(HUB) && current.getTypes().size() == 1) {
28+
return List.of(Pair.of(previous, current));
29+
}
30+
return List.of();
31+
}
32+
33+
@Override
34+
public void accept(Pair<Resource, Resource> pair) {
35+
log.info("HUB replace with different Id triggered old HUB [id {}] index deletion and new HUB [id {}] "
36+
+ "index creation", pair.getFirst().getId(), pair.getSecond().getId());
37+
hubDeleteMessageSender.produce(pair.getFirst());
38+
hubCreateMessageSender.produce(pair.getSecond());
39+
}
40+
}

src/test/java/org/folio/linked/data/e2e/resource/ResourceControllerHubIT.java renamed to src/test/java/org/folio/linked/data/e2e/resource/ResourceControllerPostHubIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
@IntegrationTest
2727
@SpringBootTest(classes = {KafkaProducerTestConfiguration.class})
28-
class ResourceControllerHubIT extends ITBase {
28+
class ResourceControllerPostHubIT extends ITBase {
2929

3030
@Autowired
3131
private MockMvc mockMvc;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package org.folio.linked.data.e2e.resource;
2+
3+
import static org.folio.linked.data.domain.dto.ResourceIndexEventType.CREATE;
4+
import static org.folio.linked.data.domain.dto.ResourceIndexEventType.DELETE;
5+
import static org.folio.linked.data.e2e.resource.ResourceControllerITBase.RESOURCE_URL;
6+
import static org.folio.linked.data.test.TestUtil.awaitAndAssert;
7+
import static org.folio.linked.data.test.TestUtil.defaultHeaders;
8+
import static org.junit.jupiter.api.Assertions.assertNotNull;
9+
import static org.junit.jupiter.api.Assertions.assertTrue;
10+
import static org.springframework.http.MediaType.APPLICATION_JSON;
11+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
12+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
13+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
14+
15+
import com.fasterxml.jackson.databind.ObjectMapper;
16+
import org.folio.ld.dictionary.ResourceTypeDictionary;
17+
import org.folio.linked.data.domain.dto.ResourceIndexEventType;
18+
import org.folio.linked.data.e2e.ITBase;
19+
import org.folio.linked.data.e2e.base.IntegrationTest;
20+
import org.folio.linked.data.model.entity.Resource;
21+
import org.folio.linked.data.repo.ResourceRepository;
22+
import org.folio.linked.data.test.kafka.KafkaProducerTestConfiguration;
23+
import org.folio.linked.data.test.kafka.KafkaSearchHubIndexTopicListener;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.boot.test.context.SpringBootTest;
28+
import org.springframework.test.web.servlet.MockMvc;
29+
30+
@IntegrationTest
31+
@SpringBootTest(classes = {KafkaProducerTestConfiguration.class})
32+
class ResourceControllerPutHubIT extends ITBase {
33+
34+
@Autowired
35+
private MockMvc mockMvc;
36+
@Autowired
37+
private KafkaSearchHubIndexTopicListener searchIndexTopicListener;
38+
@Autowired
39+
private ObjectMapper objectMapper;
40+
@Autowired
41+
private ResourceRepository resourceRepository;
42+
43+
@BeforeEach
44+
@Override
45+
public void beforeEach() {
46+
super.beforeEach();
47+
searchIndexTopicListener.getMessages().clear();
48+
}
49+
50+
@Test
51+
void shouldUpdateHubAndEmitSearchIndexEvents() throws Exception {
52+
var existingHub = new Resource()
53+
.setIdAndRefreshEdges(1L)
54+
.addTypes(ResourceTypeDictionary.HUB);
55+
resourceRepository.save(existingHub);
56+
57+
var putRequest = put(RESOURCE_URL + "/" + existingHub.getId())
58+
.contentType(APPLICATION_JSON)
59+
.headers(defaultHeaders(env))
60+
.content("""
61+
{
62+
"resource": {
63+
"http://bibfra.me/vocab/lite/Hub": {
64+
"profileId": 3,
65+
"http://bibfra.me/vocab/library/title": [
66+
{
67+
"http://bibfra.me/vocab/library/Title": {
68+
"http://bibfra.me/vocab/library/mainTitle": [ "HUB TEST TITLE" ]
69+
}
70+
}
71+
]
72+
}
73+
}
74+
}"""
75+
);
76+
77+
// when
78+
var resultActions = mockMvc.perform(putRequest);
79+
80+
// then
81+
var responseJson = resultActions
82+
.andExpect(status().isOk())
83+
.andExpect(content().contentType(APPLICATION_JSON))
84+
.andReturn().getResponse()
85+
.getContentAsString();
86+
87+
var hubIdStr = objectMapper.readTree(responseJson)
88+
.path("resource")
89+
.path("http://bibfra.me/vocab/lite/Hub")
90+
.path("id")
91+
.asText();
92+
checkSearchIndexMessage(existingHub.getId(), DELETE);
93+
checkSearchIndexMessage(Long.parseLong(hubIdStr), CREATE);
94+
checkIndexDate(hubIdStr);
95+
}
96+
97+
protected void checkSearchIndexMessage(Long id, ResourceIndexEventType eventType) {
98+
awaitAndAssert(() ->
99+
assertTrue(searchIndexTopicListener.getMessages().stream().anyMatch(m -> m.contains(id.toString())
100+
&& m.contains(eventType.getValue())))
101+
);
102+
}
103+
104+
protected void checkIndexDate(String id) {
105+
assertNotNull(resourceTestService.getResourceById(id, 0).getIndexDate());
106+
}
107+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.folio.linked.data.integration.kafka.sender.search;
2+
3+
import static java.lang.Long.parseLong;
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
import static org.folio.ld.dictionary.ResourceTypeDictionary.CONCEPT;
6+
import static org.folio.ld.dictionary.ResourceTypeDictionary.FAMILY;
7+
import static org.folio.ld.dictionary.ResourceTypeDictionary.HUB;
8+
import static org.folio.linked.data.domain.dto.ResourceIndexEventType.CREATE;
9+
import static org.mockito.Mockito.verify;
10+
import static org.mockito.Mockito.verifyNoInteractions;
11+
import static org.mockito.Mockito.when;
12+
13+
import java.util.List;
14+
import org.folio.linked.data.domain.dto.ResourceIndexEvent;
15+
import org.folio.linked.data.mapper.kafka.search.HubSearchMessageMapper;
16+
import org.folio.linked.data.model.entity.Resource;
17+
import org.folio.linked.data.model.entity.event.ResourceIndexedEvent;
18+
import org.folio.spring.testing.type.UnitTest;
19+
import org.folio.spring.tools.kafka.FolioMessageProducer;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.ExtendWith;
22+
import org.mockito.ArgumentCaptor;
23+
import org.mockito.InjectMocks;
24+
import org.mockito.Mock;
25+
import org.mockito.junit.jupiter.MockitoExtension;
26+
import org.springframework.context.ApplicationEventPublisher;
27+
28+
@UnitTest
29+
@ExtendWith(MockitoExtension.class)
30+
class HubCreateMessageSenderTest {
31+
32+
@InjectMocks
33+
private HubCreateMessageSender producer;
34+
@Mock
35+
private ApplicationEventPublisher eventPublisher;
36+
@Mock
37+
private HubSearchMessageMapper hubSearchMessageMapper;
38+
@Mock
39+
private FolioMessageProducer<ResourceIndexEvent> hubIndexMessageProducer;
40+
41+
@Test
42+
void shouldSendMessageAndPublishIndexEvent_ifGivenResourceIsHubAndIndexable() {
43+
// given
44+
var resource = new Resource().addTypes(HUB).setIdAndRefreshEdges(123L);
45+
var expectedMessage = new ResourceIndexEvent().id(String.valueOf(resource.getId()));
46+
when(hubSearchMessageMapper.toIndex(resource)).thenReturn(expectedMessage);
47+
48+
// when
49+
producer.produce(resource);
50+
51+
// then
52+
var messageCaptor = ArgumentCaptor.forClass(List.class);
53+
verify(hubIndexMessageProducer).sendMessages(messageCaptor.capture());
54+
assertThat(messageCaptor.getValue()).containsOnly(expectedMessage);
55+
assertThat(expectedMessage.getType()).isEqualTo(CREATE);
56+
assertThat(expectedMessage.getId()).isNotNull();
57+
var expectedIndexEvent = new ResourceIndexedEvent(parseLong(expectedMessage.getId()));
58+
verify(eventPublisher).publishEvent(expectedIndexEvent);
59+
}
60+
61+
@Test
62+
void shouldNotSendMessageAndIndexEvent_ifGivenResourceIsNotHub() {
63+
// given
64+
var resource = new Resource().addTypes(FAMILY);
65+
66+
// when
67+
producer.produce(resource);
68+
69+
// then
70+
verifyNoInteractions(eventPublisher, hubIndexMessageProducer);
71+
}
72+
73+
@Test
74+
void shouldNotSendMessageAndIndexEvent_ifGivenHubResourceHasAnotherType() {
75+
// given
76+
var resource = new Resource().addTypes(HUB, CONCEPT);
77+
78+
// when
79+
producer.produce(resource);
80+
81+
// then
82+
verifyNoInteractions(eventPublisher, hubIndexMessageProducer);
83+
}
84+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.folio.linked.data.integration.kafka.sender.search;
2+
3+
import static org.folio.ld.dictionary.ResourceTypeDictionary.FAMILY;
4+
import static org.folio.ld.dictionary.ResourceTypeDictionary.HUB;
5+
import static org.mockito.Mockito.verify;
6+
import static org.mockito.Mockito.verifyNoInteractions;
7+
import static org.mockito.Mockito.when;
8+
9+
import java.util.List;
10+
import org.folio.linked.data.domain.dto.ResourceIndexEvent;
11+
import org.folio.linked.data.mapper.kafka.search.HubSearchMessageMapper;
12+
import org.folio.linked.data.model.entity.Resource;
13+
import org.folio.spring.testing.type.UnitTest;
14+
import org.folio.spring.tools.kafka.FolioMessageProducer;
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.extension.ExtendWith;
17+
import org.mockito.InjectMocks;
18+
import org.mockito.Mock;
19+
import org.mockito.junit.jupiter.MockitoExtension;
20+
21+
@UnitTest
22+
@ExtendWith(MockitoExtension.class)
23+
class HubDeleteMessageSenderTest {
24+
25+
@InjectMocks
26+
private HubDeleteMessageSender sender;
27+
@Mock
28+
private FolioMessageProducer<ResourceIndexEvent> hubIndexMessageProducer;
29+
@Mock
30+
private HubSearchMessageMapper mapper;
31+
32+
@Test
33+
void produce_shouldSendDeleteMessage_ifResourceIsHub() {
34+
// given
35+
var resource = new Resource().addTypes(HUB).setIdAndRefreshEdges(456L);
36+
var onlyIdResource = new Resource().setIdAndRefreshEdges(resource.getId());
37+
var expectedMessage = new ResourceIndexEvent().id(String.valueOf(resource.getId()));
38+
when(mapper.toIndex(onlyIdResource)).thenReturn(expectedMessage);
39+
40+
// when
41+
sender.produce(resource);
42+
43+
// then
44+
verify(hubIndexMessageProducer).sendMessages(List.of(expectedMessage));
45+
verify(mapper).toIndex(onlyIdResource);
46+
}
47+
48+
@Test
49+
void produce_shouldNotSendDeleteMessage_ifResourceIsNotHub() {
50+
// given
51+
var resource = new Resource().addTypes(FAMILY);
52+
53+
// when
54+
sender.produce(resource);
55+
56+
// then
57+
verifyNoInteractions(hubIndexMessageProducer, mapper);
58+
}
59+
}

0 commit comments

Comments
 (0)