diff --git a/src/main/java/cz/cvut/kbss/termit/persistence/dao/VocabularyDao.java b/src/main/java/cz/cvut/kbss/termit/persistence/dao/VocabularyDao.java index 1e78b17d8..4744df72b 100644 --- a/src/main/java/cz/cvut/kbss/termit/persistence/dao/VocabularyDao.java +++ b/src/main/java/cz/cvut/kbss/termit/persistence/dao/VocabularyDao.java @@ -415,42 +415,52 @@ public List getDetailedHistoryOfContent(Vocabulary vocabul private TypedQuery createDetailedContentChangesQuery(Vocabulary vocabulary, VocabularyContentChangeFilterDto filter, Pageable pageReq) { TypedQuery query = em.createNativeQuery(""" - SELECT ?record WHERE { + SELECT DISTINCT ?record WHERE { +""" + /* Select anything from change context */ """ GRAPH ?changeContext { ?record a ?changeRecord . } +""" + /* The record should be a subclass of "zmena" */ """ ?changeRecord ?subClassOf+ ?zmena . ?record ?relatesTo ?term ; - ?hasTime ?timestamp ; - ?hasAuthor ?author . - ?author ?hasFirstName ?firstName ; - ?hasLastName ?lastName . - BIND(CONCAT(?firstName, " ", ?lastName) as ?authorFullName) - OPTIONAL { - ?record ?hasChangedAttribute ?attribute . - ?attribute ?hasRdfsLabel ?changedAttributeName . - } - OPTIONAL { - ?term ?inVocabulary ?vocabulary ; - a ?termType ; - ?hasLabel ?label . - } - OPTIONAL { - ?record ?hasRdfsLabel ?label . - } - BIND(?termName as ?termNameVal) - BIND(?authorName as ?authorNameVal) - BIND(?attributeName as ?changedAttributeNameVal) - FILTER (!BOUND(?termNameVal) || CONTAINS(LCASE(?label), LCASE(?termNameVal))) - FILTER (!BOUND(?authorNameVal) || CONTAINS(LCASE(?authorFullName), LCASE(?authorNameVal))) - FILTER (!BOUND(?changedAttributeName) || !BOUND(?changedAttributeNameVal) || CONTAINS(LCASE(?changedAttributeName), LCASE(?changedAttributeName))) + ?hasTime ?timestamp ; + ?hasAuthor ?author . +""" + /* Get author's name */ """ + ?author ?hasFirstName ?firstName ; + ?hasLastName ?lastName . + BIND(CONCAT(?firstName, " ", ?lastName) as ?authorFullName) +""" + /* When its update record, there will be a changed attribute */ """ + OPTIONAL { + ?record ?hasChangedAttribute ?attribute . + ?attribute ?hasRdfsLabel ?changedAttributeName . + } +""" + /* Get term's name (but the term might have been already deleted) */ """ + OPTIONAL { + ?term a ?termType ; + ?hasLabel ?label . + } +""" + /* then try to get the label from (delete) record */ """ + OPTIONAL { + ?record ?hasRdfsLabel ?label . + } +""" + /* When label is still not bound, the term was probably deleted, find the delete record and get the label from it */ """ + OPTIONAL { + FILTER(!BOUND(?label)) . + ?deleteRecord a ; + ?term; + ?label. + } + BIND(?termName as ?termNameVal) + BIND(?authorName as ?authorNameVal) + BIND(?attributeName as ?changedAttributeNameVal) + FILTER (!BOUND(?termNameVal) || CONTAINS(LCASE(?label), LCASE(?termNameVal))) + FILTER (!BOUND(?authorNameVal) || CONTAINS(LCASE(?authorFullName), LCASE(?authorNameVal))) + FILTER (!BOUND(?changedAttributeName) || !BOUND(?changedAttributeNameVal) || CONTAINS(LCASE(?changedAttributeName), LCASE(?changedAttributeName))) } ORDER BY DESC(?timestamp) ?attribute """, AbstractChangeRecord.class) .setParameter("changeContext", changeTrackingContextResolver.resolveChangeTrackingContext(vocabulary)) .setParameter("subClassOf", URI.create(RDFS.SUB_CLASS_OF)) .setParameter("zmena", URI.create(cz.cvut.kbss.termit.util.Vocabulary.s_c_zmena)) - .setParameter("inVocabulary", URI.create(cz.cvut.kbss.termit.util.Vocabulary.s_p_je_pojmem_ze_slovniku)) - .setParameter("vocabulary", vocabulary) .setParameter("termType", URI.create(SKOS.CONCEPT)) .setParameter("relatesTo", URI.create(cz.cvut.kbss.termit.util.Vocabulary.s_p_ma_zmenenou_entitu)) .setParameter("hasTime", URI.create(cz.cvut.kbss.termit.util.Vocabulary.s_p_ma_datum_a_cas_modifikace)) @@ -474,7 +484,11 @@ private TypedQuery createDetailedContentChangesQuery(Vocab query = query.setParameter("attributeName", filter.getChangedAttributeName().trim()); } - return query.setFirstResult((int) pageReq.getOffset()) + if(pageReq.isUnpaged()) { + return query; + } + + return query.setFirstResult((int) pageReq.getOffset()) .setMaxResults(pageReq.getPageSize()); } diff --git a/src/test/java/cz/cvut/kbss/termit/persistence/dao/VocabularyDaoTest.java b/src/test/java/cz/cvut/kbss/termit/persistence/dao/VocabularyDaoTest.java index 23b72777c..972ccee1c 100644 --- a/src/test/java/cz/cvut/kbss/termit/persistence/dao/VocabularyDaoTest.java +++ b/src/test/java/cz/cvut/kbss/termit/persistence/dao/VocabularyDaoTest.java @@ -25,6 +25,7 @@ import cz.cvut.kbss.termit.dto.PrefixDeclaration; import cz.cvut.kbss.termit.dto.RdfsStatement; import cz.cvut.kbss.termit.dto.Snapshot; +import cz.cvut.kbss.termit.dto.filter.VocabularyContentChangeFilterDto; import cz.cvut.kbss.termit.environment.Environment; import cz.cvut.kbss.termit.environment.Generator; import cz.cvut.kbss.termit.event.AssetPersistEvent; @@ -37,13 +38,18 @@ import cz.cvut.kbss.termit.model.User; import cz.cvut.kbss.termit.model.Vocabulary; import cz.cvut.kbss.termit.model.changetracking.AbstractChangeRecord; +import cz.cvut.kbss.termit.model.changetracking.DeleteChangeRecord; import cz.cvut.kbss.termit.model.changetracking.PersistChangeRecord; import cz.cvut.kbss.termit.model.changetracking.UpdateChangeRecord; import cz.cvut.kbss.termit.model.resource.Document; import cz.cvut.kbss.termit.model.resource.File; import cz.cvut.kbss.termit.model.util.EntityToOwlClassMapper; import cz.cvut.kbss.termit.persistence.context.DescriptorFactory; +import cz.cvut.kbss.termit.persistence.dao.changetracking.ChangeRecordDao; +import cz.cvut.kbss.termit.persistence.dao.changetracking.ChangeTrackingContextResolver; +import cz.cvut.kbss.termit.util.Configuration; import cz.cvut.kbss.termit.util.Constants; +import cz.cvut.kbss.termit.util.Utils; import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.vocabulary.RDF; @@ -59,6 +65,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Pageable; import org.springframework.test.annotation.DirtiesContext; import java.net.URI; @@ -108,6 +115,9 @@ class VocabularyDaoTest extends BaseDaoTestRunner { @Autowired private VocabularyDao sut; + @Autowired + private TermDao termDao; + private User author; @BeforeEach @@ -927,4 +937,166 @@ void getAnyExternalRelationsReturnsTermsWithBothRelations(URI termRelation) { } }); } + + @Test + void getDetailedHistoryOfContentReturnsRecordsForAllChangeTypes() { + enableRdfsInference(em); + final Configuration config = new Configuration(); + config.getChangetracking().getContext().setExtension("/zmeny"); + final ChangeTrackingContextResolver resolver = new ChangeTrackingContextResolver(em, config); + final ChangeRecordDao changeRecordDao = new ChangeRecordDao(resolver, em); + + final Vocabulary vocabulary = Generator.generateVocabularyWithId(); + final Term firstTerm = Generator.generateTermWithId(vocabulary.getUri()); + final Term termToRemove = Generator.generateTermWithId(vocabulary.getUri()); + + final List firstChanges = Generator.generateChangeRecords(firstTerm, author); + final List termToRemoveChanges = Generator.generateChangeRecords(termToRemove, author); + final DeleteChangeRecord deleteChangeRecord = new DeleteChangeRecord(); + deleteChangeRecord.setChangedEntity(termToRemove.getUri()); + deleteChangeRecord.setTimestamp(Utils.timestamp()); + deleteChangeRecord.setAuthor(author); + deleteChangeRecord.setLabel(termToRemove.getLabel()); + + transactional(() -> { + vocabulary.getGlossary().addRootTerm(firstTerm); + sut.persist(vocabulary); + Environment.addRelation(vocabulary.getUri(), URI.create(cz.cvut.kbss.termit.util.Vocabulary.s_p_ma_glosar), vocabulary.getGlossary().getUri(), em); + + termDao.persist(firstTerm, vocabulary); + termDao.persist(termToRemove, vocabulary); + + firstChanges.forEach(r -> changeRecordDao.persist(r, firstTerm)); + termToRemoveChanges.forEach(r -> changeRecordDao.persist(r, termToRemove)); + changeRecordDao.persist(deleteChangeRecord, termToRemove); + }); + + final VocabularyContentChangeFilterDto filter = new VocabularyContentChangeFilterDto(); + final int recordsCount = firstChanges.size() + termToRemoveChanges.size() + 1; // +1 for the delete record + final Pageable pageable = Pageable.ofSize(recordsCount * 3); + final List contentChanges = sut.getDetailedHistoryOfContent(vocabulary, filter, pageable); + + assertEquals(recordsCount, contentChanges.size()); + final long persistCount = contentChanges.stream().filter(ch -> ch instanceof PersistChangeRecord).count(); + final long updatesCount = contentChanges.stream().filter(ch -> ch instanceof UpdateChangeRecord).count(); + final long deleteCount = contentChanges.stream().filter(ch -> ch instanceof DeleteChangeRecord).count(); + assertEquals(2, persistCount); + assertEquals(recordsCount - 3, updatesCount); // -2 persist records, -1 delete record + assertEquals(1, deleteCount); + } + + + @Test + void getDetailedHistoryOfContentReturnsRecordsOfExistingTermFilteredByTermName() { + enableRdfsInference(em); + final Configuration config = new Configuration(); + config.getChangetracking().getContext().setExtension("/zmeny"); + final ChangeTrackingContextResolver resolver = new ChangeTrackingContextResolver(em, config); + final ChangeRecordDao changeRecordDao = new ChangeRecordDao(resolver, em); + + final String needle = "needle"; + final String haystack = "A label that contains needle somewhere"; + final String mud = "The n3edle is not here"; + + // Two terms with needle in the label, one term without needle in the label + final Vocabulary vocabulary = Generator.generateVocabularyWithId(); + final Term firstTerm = Generator.generateTermWithId(vocabulary.getUri()); + firstTerm.getLabel().set(Environment.LANGUAGE, haystack); + final Term secondTerm = Generator.generateTermWithId(vocabulary.getUri()); + secondTerm.getLabel().set(mud + needle); + final Term thirdTerm = Generator.generateTermWithId(vocabulary.getUri()); + thirdTerm.getLabel().set(Environment.LANGUAGE, mud); + + final List firstChanges = Generator.generateChangeRecords(firstTerm, author); + final List secondChanges = Generator.generateChangeRecords(secondTerm, author); + final List thirdChanges = Generator.generateChangeRecords(thirdTerm, author); + + transactional(() -> { + vocabulary.getGlossary().addRootTerm(firstTerm); + sut.persist(vocabulary); + Environment.addRelation(vocabulary.getUri(), URI.create(cz.cvut.kbss.termit.util.Vocabulary.s_p_ma_glosar), vocabulary.getGlossary().getUri(), em); + + termDao.persist(firstTerm, vocabulary); + termDao.persist(secondTerm, vocabulary); + termDao.persist(thirdTerm, vocabulary); + + firstChanges.forEach(r -> changeRecordDao.persist(r, firstTerm)); + secondChanges.forEach(r -> changeRecordDao.persist(r, secondTerm)); + thirdChanges.forEach(r -> changeRecordDao.persist(r, thirdTerm)); + }); + + final VocabularyContentChangeFilterDto filter = new VocabularyContentChangeFilterDto(); + filter.setTermName(needle); + + final int recordsCount = firstChanges.size() + secondChanges.size(); + final Pageable pageable = Pageable.ofSize(recordsCount * 2); + final List contentChanges = sut.getDetailedHistoryOfContent(vocabulary, filter, pageable); + + assertEquals(recordsCount, contentChanges.size()); + final long persistCount = contentChanges.stream().filter(ch -> ch instanceof PersistChangeRecord).count(); + final long updatesCount = contentChanges.stream().filter(ch -> ch instanceof UpdateChangeRecord).count(); + final long deleteCount = contentChanges.stream().filter(ch -> ch instanceof DeleteChangeRecord).count(); + assertEquals(2, persistCount); + assertEquals(recordsCount - 2, updatesCount); // -2 persist records + assertEquals(0, deleteCount); + } + + @Test + void getDetailedHistoryOfContentReturnsRecordsOfDeletedTermFilteredByTermName() { + enableRdfsInference(em); + final Configuration config = new Configuration(); + config.getChangetracking().getContext().setExtension("/zmeny"); + final ChangeTrackingContextResolver resolver = new ChangeTrackingContextResolver(em, config); + final ChangeRecordDao changeRecordDao = new ChangeRecordDao(resolver, em); + + final String needle = "needle"; + final String haystack = "A label that contains needle somewhere"; + final String mud = "The n3edle is not here"; + + final Vocabulary vocabulary = Generator.generateVocabularyWithId(); + final Term firstTerm = Generator.generateTermWithId(vocabulary.getUri()); + // the needle is placed in the term which will be removed + firstTerm.getLabel().set(Environment.LANGUAGE, mud); + final Term termToRemove = Generator.generateTermWithId(vocabulary.getUri()); + termToRemove.getLabel().set(Environment.LANGUAGE, haystack); + + final List firstChanges = Generator.generateChangeRecords(firstTerm, author); + final List termToRemoveChanges = Generator.generateChangeRecords(termToRemove, author); + final DeleteChangeRecord deleteChangeRecord = new DeleteChangeRecord(); + deleteChangeRecord.setChangedEntity(termToRemove.getUri()); + deleteChangeRecord.setTimestamp(Utils.timestamp()); + deleteChangeRecord.setAuthor(author); + deleteChangeRecord.setLabel(termToRemove.getLabel()); + + transactional(() -> { + vocabulary.getGlossary().addRootTerm(firstTerm); + sut.persist(vocabulary); + Environment.addRelation(vocabulary.getUri(), URI.create(cz.cvut.kbss.termit.util.Vocabulary.s_p_ma_glosar), vocabulary.getGlossary().getUri(), em); + + termDao.persist(firstTerm, vocabulary); + termDao.persist(termToRemove, vocabulary); + + firstChanges.forEach(r -> changeRecordDao.persist(r, firstTerm)); + termToRemoveChanges.forEach(r -> changeRecordDao.persist(r, termToRemove)); + changeRecordDao.persist(deleteChangeRecord, termToRemove); + + termToRemove.setVocabulary(vocabulary.getUri()); + termDao.remove(termToRemove); + }); + + final VocabularyContentChangeFilterDto filter = new VocabularyContentChangeFilterDto(); + filter.setTermName(needle); + + final int recordsCount = termToRemoveChanges.size() + 1; // +1 for the delete record + final Pageable pageable = Pageable.ofSize(recordsCount * 2); + final List contentChanges = sut.getDetailedHistoryOfContent(vocabulary, filter, pageable); + + assertEquals(recordsCount, contentChanges.size()); + final long persistCount = contentChanges.stream().filter(ch -> ch instanceof PersistChangeRecord).count(); + final long updatesCount = contentChanges.stream().filter(ch -> ch instanceof UpdateChangeRecord).count(); + final long deleteCount = contentChanges.stream().filter(ch -> ch instanceof DeleteChangeRecord).count(); + assertEquals(1, persistCount); + assertEquals(recordsCount - 2, updatesCount); // -1 persist record -1 delete record + assertEquals(1, deleteCount); + } } diff --git a/src/test/java/cz/cvut/kbss/termit/service/changetracking/ChangeTrackingTest.java b/src/test/java/cz/cvut/kbss/termit/service/changetracking/ChangeTrackingTest.java index 05069c385..d570a1f85 100644 --- a/src/test/java/cz/cvut/kbss/termit/service/changetracking/ChangeTrackingTest.java +++ b/src/test/java/cz/cvut/kbss/termit/service/changetracking/ChangeTrackingTest.java @@ -27,6 +27,7 @@ import cz.cvut.kbss.termit.model.User; import cz.cvut.kbss.termit.model.Vocabulary; import cz.cvut.kbss.termit.model.changetracking.AbstractChangeRecord; +import cz.cvut.kbss.termit.model.changetracking.DeleteChangeRecord; import cz.cvut.kbss.termit.model.changetracking.PersistChangeRecord; import cz.cvut.kbss.termit.model.changetracking.UpdateChangeRecord; import cz.cvut.kbss.termit.model.resource.File; @@ -52,6 +53,8 @@ import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class ChangeTrackingTest extends BaseServiceTestRunner { @@ -141,7 +144,7 @@ void updatingVocabularyReferenceAndLiteralAttributesCreatesTwoUpdateRecords() { assertEquals(vocabulary.getUri(), chr.getChangedEntity()); assertThat(result.get(0), instanceOf(UpdateChangeRecord.class)); assertThat(((UpdateChangeRecord) chr).getChangedAttribute().toString(), anyOf(equalTo(DC.Terms.TITLE), - equalTo(cz.cvut.kbss.termit.util.Vocabulary.s_p_importuje_slovnik))); + equalTo(cz.cvut.kbss.termit.util.Vocabulary.s_p_importuje_slovnik))); }); } @@ -214,7 +217,7 @@ void updatingTermLiteralAttributesCreatesChangeRecordWithOriginalAndNewValue() { final List result = changeRecordDao.findAll(term); assertEquals(1, result.size()); assertEquals(Collections.singleton(originalDefinition), - ((UpdateChangeRecord) result.get(0)).getOriginalValue()); + ((UpdateChangeRecord) result.get(0)).getOriginalValue()); assertEquals(Collections.singleton(newDefinition), ((UpdateChangeRecord) result.get(0)).getNewValue()); } @@ -271,4 +274,24 @@ void updatingTermStateCreatesUpdateChangeRecord() { assertEquals(URI.create(cz.cvut.kbss.termit.util.Vocabulary.s_p_ma_stav_pojmu), ((UpdateChangeRecord) result.get(0)).getChangedAttribute()); } + + @Test + void deletingTermCreatesDeleteChangeRecord() { + enableRdfsInference(em); + final Term term = Generator.generateTermWithId(vocabulary.getUri()); + transactional(()-> { + em.persist(vocabulary, descriptorFactory.vocabularyDescriptor(vocabulary)); + term.setGlossary(vocabulary.getGlossary().getUri()); + em.persist(term, descriptorFactory.termDescriptor(vocabulary)); + Generator.addTermInVocabularyRelationship(term, vocabulary.getUri(), em); + }); + + termService.remove(term); + final List result = changeRecordDao.findAll(term); + assertEquals(1, result.size()); + final DeleteChangeRecord record = assertInstanceOf(DeleteChangeRecord.class, result.get(0)); + assertEquals(term.getUri(), record.getChangedEntity()); + assertNotNull(record.getLabel()); + assertEquals(term.getLabel(), record.getLabel()); + } }