diff --git a/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/dto/ChangedEntities.java b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/dto/ChangedEntities.java new file mode 100644 index 0000000..e46d37e --- /dev/null +++ b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/dto/ChangedEntities.java @@ -0,0 +1,8 @@ +package edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.dto; + +import java.util.List; + +public record ChangedEntities(List createdEntities, + List updatedEntities, + List deletedEntities) { +} diff --git a/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesCommandHandler.java b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesCommandHandler.java new file mode 100644 index 0000000..da4b515 --- /dev/null +++ b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesCommandHandler.java @@ -0,0 +1,37 @@ +package edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.handlers; + +import edu.stanford.protege.webprotege.ipc.*; +import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.services.NewRevisionsEventService; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; + + +@Component +public class GetChangedEntitiesCommandHandler implements CommandHandler { + + private final NewRevisionsEventService service; + + public GetChangedEntitiesCommandHandler(NewRevisionsEventService service) { + this.service = service; + } + + + @Nonnull + @Override + public String getChannelName() { + return GetChangedEntitiesRequest.CHANNEL; + } + + @Override + public Class getRequestClass() { + return GetChangedEntitiesRequest.class; + } + + @Override + public Mono handleRequest(GetChangedEntitiesRequest request, ExecutionContext executionContext) { + var entityChanges = service.getChangedEntitiesAfterTimestamp(request.projectId(), request.timestamp()); + return Mono.just(GetChangedEntitiesResponse.create(entityChanges)); + } +} diff --git a/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesRequest.java b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesRequest.java new file mode 100644 index 0000000..445737c --- /dev/null +++ b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesRequest.java @@ -0,0 +1,25 @@ +package edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.handlers; + +import com.fasterxml.jackson.annotation.*; +import edu.stanford.protege.webprotege.common.*; + +import java.sql.Timestamp; + +@JsonTypeName(GetChangedEntitiesRequest.CHANNEL) +public record GetChangedEntitiesRequest( + @JsonProperty("projectId") ProjectId projectId, + @JsonProperty("timestamp") Timestamp timestamp +) implements Request { + + public static final String CHANNEL = "webprotege.history.GetChangedEntities"; + + @Override + public String getChannel() { + return CHANNEL; + } + + public static GetChangedEntitiesRequest create(ProjectId projectId, + Timestamp timestamp) { + return new GetChangedEntitiesRequest(projectId, timestamp); + } +} diff --git a/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesResponse.java b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesResponse.java new file mode 100644 index 0000000..f78a353 --- /dev/null +++ b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesResponse.java @@ -0,0 +1,15 @@ +package edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.handlers; + +import com.fasterxml.jackson.annotation.*; +import edu.stanford.protege.webprotege.common.Response; +import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.dto.ChangedEntities; + +import static edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.handlers.GetChangedEntitiesRequest.CHANNEL; + +@JsonTypeName(CHANNEL) +public record GetChangedEntitiesResponse( + @JsonProperty("changedEntities") ChangedEntities changedEntities) implements Response { + public static GetChangedEntitiesResponse create(ChangedEntities changedEntities) { + return new GetChangedEntitiesResponse(changedEntities); + } +} diff --git a/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/repositories/RevisionsEventRepository.java b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/repositories/RevisionsEventRepository.java index 49b21f8..ffdaae2 100644 --- a/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/repositories/RevisionsEventRepository.java +++ b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/repositories/RevisionsEventRepository.java @@ -1,7 +1,11 @@ package edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.repositories; import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.events.RevisionsEvent; -import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.*; -public interface RevisionsEventRepository extends MongoRepository { +import java.util.List; + +public interface RevisionsEventRepository extends MongoRepository { + @Query("{ 'projectId.id': ?0, 'timestamp': { $gt: ?1 } }") + List findByProjectIdAndTimestampAfter(String projectId, long timestamp); } diff --git a/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventService.java b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventService.java index e7967ce..e68ad28 100644 --- a/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventService.java +++ b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventService.java @@ -2,9 +2,11 @@ import edu.stanford.protege.webprotege.change.ProjectChange; import edu.stanford.protege.webprotege.common.*; -import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.events.*; +import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.dto.ChangedEntities; +import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.events.NewRevisionsEvent; import org.semanticweb.owlapi.model.OWLEntity; +import java.sql.Timestamp; import java.util.Optional; public interface NewRevisionsEventService { @@ -12,4 +14,6 @@ public interface NewRevisionsEventService { void registerEvent(NewRevisionsEvent newLinRevEvent); Page fetchPaginatedProjectChanges(ProjectId projectId, Optional subject, int pageNumber, int pageSize); + + ChangedEntities getChangedEntitiesAfterTimestamp(ProjectId projectId, Timestamp timestamp); } diff --git a/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventServiceImpl.java b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventServiceImpl.java index fadcdd1..561fd3d 100644 --- a/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventServiceImpl.java +++ b/src/main/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventServiceImpl.java @@ -3,6 +3,7 @@ import edu.stanford.protege.webprotege.change.ProjectChange; import edu.stanford.protege.webprotege.common.Page; import edu.stanford.protege.webprotege.common.*; +import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.dto.*; import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.events.*; import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.mappers.*; import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.repositories.RevisionsEventRepository; @@ -12,7 +13,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.sql.Timestamp; import java.util.*; +import java.util.stream.Collectors; import static edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.events.RevisionsEvent.*; @@ -74,4 +77,32 @@ public Page fetchPaginatedProjectChanges(ProjectId projectId, Opt //PageRequest from spring-data is starting from 0 return Page.create(pageNumber, revisionsEventPage.getTotalPages(), changes, revisionsEventPage.getTotalElements()); } + + @Override + public ChangedEntities getChangedEntitiesAfterTimestamp(ProjectId projectId, Timestamp timestamp) { + List revisionsEvents = repository.findByProjectIdAndTimestampAfter(projectId.id(), timestamp.getTime()); + + List createdEntities = revisionsEvents.stream() + .filter(event -> event.changeType() == ChangeType.CREATE_ENTITY) + .map(RevisionsEvent::whoficEntityIri) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + + List updatedEntities = revisionsEvents.stream() + .filter(event -> event.changeType() == ChangeType.UPDATE_ENTITY) + .map(RevisionsEvent::whoficEntityIri) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + + List deletedEntities = revisionsEvents.stream() + .filter(event -> event.changeType() == ChangeType.DELETE_ENTITY) + .map(RevisionsEvent::whoficEntityIri) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + + return new ChangedEntities(createdEntities, updatedEntities, deletedEntities); + } } diff --git a/src/test/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesCommandHandlerIT.java b/src/test/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesCommandHandlerIT.java new file mode 100644 index 0000000..e9bab5b --- /dev/null +++ b/src/test/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/handlers/GetChangedEntitiesCommandHandlerIT.java @@ -0,0 +1,73 @@ +package edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.handlers; + +import edu.stanford.protege.webprotege.common.ProjectId; +import edu.stanford.protege.webprotegeeventshistory.*; +import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.dto.*; +import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.events.RevisionsEvent; +import org.bson.Document; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import reactor.core.publisher.Mono; + +import java.sql.Timestamp; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@ExtendWith({SpringExtension.class, RabbitTestExtension.class, MongoTestExtension.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) +@Import({WebprotegeEventsHistoryApplication.class}) +public class GetChangedEntitiesCommandHandlerIT { + + @Autowired + private GetChangedEntitiesCommandHandler commandHandler; + + @Autowired + private MongoTemplate mongoTemplate; + + @BeforeEach + public void setUp() { + mongoTemplate.dropCollection(RevisionsEvent.class); + } + + @Test + public void GIVEN_eventsAfterTimestamp_WHEN_handleRequestCalled_THEN_returnChangedEntities() { + ProjectId projectId = ProjectId.generate(); + Timestamp timestamp = new Timestamp(System.currentTimeMillis() - 10000); + + insertMockRevisionsEvent(projectId, "entity1", timestamp.getTime() - 5000, ChangeType.CREATE_ENTITY); + insertMockRevisionsEvent(projectId, "entity2", timestamp.getTime() + 5000, ChangeType.UPDATE_ENTITY); + insertMockRevisionsEvent(projectId, "entity3", timestamp.getTime() + 6000, ChangeType.DELETE_ENTITY); + + GetChangedEntitiesRequest request = GetChangedEntitiesRequest.create(projectId, timestamp); + + Mono responseMono = commandHandler.handleRequest(request, null); + GetChangedEntitiesResponse response = responseMono.block(); + + assertNotNull(response); + ChangedEntities changedEntities = response.changedEntities(); + assertEquals(0, changedEntities.createdEntities().size()); + assertEquals(1, changedEntities.updatedEntities().size()); + assertEquals(1, changedEntities.deletedEntities().size()); + + assertEquals("entity2", changedEntities.updatedEntities().get(0)); + assertEquals("entity3", changedEntities.deletedEntities().get(0)); + } + + private void insertMockRevisionsEvent(ProjectId projectId, String entityIri, long timestamp, ChangeType changeType) { + RevisionsEvent revisionsEvent = RevisionsEvent.create( + projectId, + entityIri, + changeType, + timestamp, + new Document() + ); + mongoTemplate.save(revisionsEvent); + } +} diff --git a/src/test/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventServiceTest.java b/src/test/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventServiceTest.java index 40b885d..8920e4a 100644 --- a/src/test/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventServiceTest.java +++ b/src/test/java/edu/stanford/protege/webprotegeeventshistory/uiHistoryConcern/services/NewRevisionsEventServiceTest.java @@ -8,7 +8,7 @@ import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.mappers.*; import edu.stanford.protege.webprotegeeventshistory.uiHistoryConcern.repositories.RevisionsEventRepository; import org.bson.Document; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; @@ -16,6 +16,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.*; +import java.sql.Timestamp; import java.util.*; import static org.junit.jupiter.api.Assertions.*; @@ -37,9 +38,19 @@ public class NewRevisionsEventServiceTest { @InjectMocks private NewRevisionsEventServiceImpl service; + + private ProjectId projectId; + private Timestamp timestamp; + + + @BeforeEach + public void setUp() { + projectId = new ProjectId("testProjectId"); + timestamp = new Timestamp(System.currentTimeMillis()); + } + @Test public void GIVEN_validNewLinearizationRevisionsEvent_WHEN_registerEventCalled_THEN_revisionsEventsSavedToRepository() { - ProjectId projectId = new ProjectId("testProjectId"); Set changes = Set.of(mock(ProjectChangeForEntity.class)); NewRevisionsEvent event = NewRevisionsEvent.create(EventId.generate(), projectId, changes); @@ -55,12 +66,11 @@ public void GIVEN_validNewLinearizationRevisionsEvent_WHEN_registerEventCalled_T @Test public void GIVEN_validProjectIdAndSubject_WHEN_fetchPaginatedProjectChangesCalled_THEN_returnPaginatedProjectChanges() { - ProjectId projectId = new ProjectId("testProjectId"); OWLEntity mockEntity = mock(OWLEntity.class); IRI mockIri = IRI.create("http://example.com/entity"); when(mockEntity.getIRI()).thenReturn(mockIri); - RevisionsEvent mockRevisionsEvent = RevisionsEvent.create(projectId, mockIri.toString(), ChangeType.UPDATE_ENTITY,12345L, new Document()); + RevisionsEvent mockRevisionsEvent = RevisionsEvent.create(projectId, mockIri.toString(), ChangeType.UPDATE_ENTITY, 12345L, new Document()); PageRequest pageRequest = PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "timestamp")); org.springframework.data.domain.Page mockPage = new PageImpl<>(List.of(mockRevisionsEvent), pageRequest, 1); @@ -81,9 +91,8 @@ public void GIVEN_validProjectIdAndSubject_WHEN_fetchPaginatedProjectChangesCall @Test public void GIVEN_nullSubject_WHEN_fetchPaginatedProjectChangesCalled_THEN_returnPaginatedProjectChanges() { - ProjectId projectId = new ProjectId("testProjectId"); - RevisionsEvent mockRevisionsEvent = RevisionsEvent.create(projectId, null, ChangeType.CREATE_ENTITY,12345L, new Document()); + RevisionsEvent mockRevisionsEvent = RevisionsEvent.create(projectId, null, ChangeType.CREATE_ENTITY, 12345L, new Document()); PageRequest pageRequest = PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "timestamp")); org.springframework.data.domain.Page mockPage = new PageImpl<>(List.of(mockRevisionsEvent), pageRequest, 1); @@ -104,7 +113,6 @@ public void GIVEN_nullSubject_WHEN_fetchPaginatedProjectChangesCalled_THEN_retur @Test public void GIVEN_noResults_WHEN_fetchPaginatedProjectChangesCalled_THEN_returnEmptyPage() { - ProjectId projectId = new ProjectId("testProjectId"); PageRequest pageRequest = PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "timestamp")); org.springframework.data.domain.Page mockPage = new PageImpl<>(List.of(), pageRequest, 0); @@ -119,4 +127,60 @@ public void GIVEN_noResults_WHEN_fetchPaginatedProjectChangesCalled_THEN_returnE verify(repository).findAll(any(Example.class), eq(pageRequest)); verifyNoInteractions(projectChangeMapper); } + + @Test + public void GIVEN_noEntitiesChangedAfterTimestamp_WHEN_getChangedEntitiesAfterTimestampCalled_THEN_emptyChangedEntitiesReturned() { + when(repository.findByProjectIdAndTimestampAfter(projectId.id(), timestamp.getTime())).thenReturn(List.of()); + + ChangedEntities result = service.getChangedEntitiesAfterTimestamp(projectId, timestamp); + + assertEquals(0, result.createdEntities().size()); + assertEquals(0, result.updatedEntities().size()); + assertEquals(0, result.deletedEntities().size()); + + verify(repository).findByProjectIdAndTimestampAfter(projectId.id(), timestamp.getTime()); + } + + @Test + public void GIVEN_entitiesChangedAfterTimestamp_WHEN_getChangedEntitiesAfterTimestampCalled_THEN_returnGroupedChangedEntities() { + RevisionsEvent createdEntity = RevisionsEvent.create(projectId, "entityIRI1", ChangeType.CREATE_ENTITY, timestamp.getTime(), new Document()); + RevisionsEvent updatedEntity = RevisionsEvent.create(projectId, "entityIRI2", ChangeType.UPDATE_ENTITY, timestamp.getTime() + 1000, new Document()); + RevisionsEvent deletedEntity = RevisionsEvent.create(projectId, "entityIRI3", ChangeType.DELETE_ENTITY, timestamp.getTime() + 2000, new Document()); + + when(repository.findByProjectIdAndTimestampAfter(projectId.id(), timestamp.getTime())).thenReturn(List.of(createdEntity, updatedEntity, deletedEntity)); + + ChangedEntities result = service.getChangedEntitiesAfterTimestamp(projectId, timestamp); + + assertEquals(1, result.createdEntities().size()); + assertEquals("entityIRI1", result.createdEntities().get(0)); + + assertEquals(1, result.updatedEntities().size()); + assertEquals("entityIRI2", result.updatedEntities().get(0)); + + assertEquals(1, result.deletedEntities().size()); + assertEquals("entityIRI3", result.deletedEntities().get(0)); + + verify(repository).findByProjectIdAndTimestampAfter(projectId.id(), timestamp.getTime()); + } + + @Test + public void GIVEN_multipleEntitiesChangedAfterTimestamp_WHEN_getChangedEntitiesAfterTimestampCalled_THEN_returnDeduplicatedChangedEntities() { + RevisionsEvent createdEntity1 = RevisionsEvent.create(projectId, "entityIRI1", ChangeType.CREATE_ENTITY, timestamp.getTime(), new Document()); + RevisionsEvent createdEntity2 = RevisionsEvent.create(projectId, "entityIRI1", ChangeType.CREATE_ENTITY, timestamp.getTime() + 1000, new Document()); + RevisionsEvent updatedEntity = RevisionsEvent.create(projectId, "entityIRI2", ChangeType.UPDATE_ENTITY, timestamp.getTime() + 2000, new Document()); + + when(repository.findByProjectIdAndTimestampAfter(projectId.id(), timestamp.getTime())).thenReturn(List.of(createdEntity1, createdEntity2, updatedEntity)); + + ChangedEntities result = service.getChangedEntitiesAfterTimestamp(projectId, timestamp); + + assertEquals(1, result.createdEntities().size()); + assertEquals("entityIRI1", result.createdEntities().get(0)); + + assertEquals(1, result.updatedEntities().size()); + assertEquals("entityIRI2", result.updatedEntities().get(0)); + + assertEquals(0, result.deletedEntities().size()); + + verify(repository).findByProjectIdAndTimestampAfter(projectId.id(), timestamp.getTime()); + } }