From 4d21184fad154adbe4e3a703b8ed70550c4c0af3 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Thu, 8 Jun 2023 13:09:12 +0200 Subject: [PATCH 01/12] [kbss-cvut/termit-ui#326] Define REST API for faceted term search. --- .../dto/search/FacetedSearchResult.java | 149 ++++++++++++++++++ .../kbss/termit/dto/search/MatchType.java | 19 +++ .../kbss/termit/dto/search/SearchParam.java | 50 ++++++ .../UnsupportedSearchFacetException.java | 11 ++ .../kbss/termit/rest/SearchController.java | 19 ++- .../rest/handler/RestExceptionHandler.java | 7 + 6 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 src/main/java/cz/cvut/kbss/termit/dto/search/FacetedSearchResult.java create mode 100644 src/main/java/cz/cvut/kbss/termit/dto/search/MatchType.java create mode 100644 src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java create mode 100644 src/main/java/cz/cvut/kbss/termit/exception/UnsupportedSearchFacetException.java diff --git a/src/main/java/cz/cvut/kbss/termit/dto/search/FacetedSearchResult.java b/src/main/java/cz/cvut/kbss/termit/dto/search/FacetedSearchResult.java new file mode 100644 index 000000000..431a2fb11 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/termit/dto/search/FacetedSearchResult.java @@ -0,0 +1,149 @@ +package cz.cvut.kbss.termit.dto.search; + +import cz.cvut.kbss.jopa.model.MultilingualString; +import cz.cvut.kbss.jopa.model.annotations.*; +import cz.cvut.kbss.jopa.vocabulary.SKOS; +import cz.cvut.kbss.termit.model.Asset; +import cz.cvut.kbss.termit.model.util.HasTypes; +import cz.cvut.kbss.termit.util.Utils; + +import java.net.URI; +import java.util.Objects; +import java.util.Set; + +/** + * Represents the result of a faceted term search. + *

+ * Contains only basic SKOS properties. + */ +@OWLClass(iri = SKOS.CONCEPT) +public class FacetedSearchResult extends Asset implements HasTypes { + + @OWLAnnotationProperty(iri = SKOS.PREF_LABEL) + private MultilingualString label; + + @OWLAnnotationProperty(iri = SKOS.DEFINITION) + private MultilingualString definition; + + @OWLAnnotationProperty(iri = SKOS.ALT_LABEL) + private Set altLabels; + + @OWLAnnotationProperty(iri = SKOS.HIDDEN_LABEL) + private Set hiddenLabels; + + @OWLAnnotationProperty(iri = SKOS.SCOPE_NOTE) + private MultilingualString description; + + @OWLDataProperty(iri = SKOS.NOTATION, simpleLiteral = true) + private Set notations; + + @OWLAnnotationProperty(iri = SKOS.EXAMPLE) + private Set examples; + + @Inferred + @OWLObjectProperty(iri = cz.cvut.kbss.termit.util.Vocabulary.s_p_je_pojmem_ze_slovniku) + private URI vocabulary; + + @Types + private Set types; + + @Override + public MultilingualString getLabel() { + return label; + } + + @Override + public void setLabel(MultilingualString label) { + this.label = label; + } + + public MultilingualString getDefinition() { + return definition; + } + + public void setDefinition(MultilingualString definition) { + this.definition = definition; + } + + public Set getAltLabels() { + return altLabels; + } + + public void setAltLabels(Set altLabels) { + this.altLabels = altLabels; + } + + public Set getHiddenLabels() { + return hiddenLabels; + } + + public void setHiddenLabels(Set hiddenLabels) { + this.hiddenLabels = hiddenLabels; + } + + public MultilingualString getDescription() { + return description; + } + + public void setDescription(MultilingualString description) { + this.description = description; + } + + public Set getNotations() { + return notations; + } + + public void setNotations(Set notations) { + this.notations = notations; + } + + public Set getExamples() { + return examples; + } + + public void setExamples(Set examples) { + this.examples = examples; + } + + public URI getVocabulary() { + return vocabulary; + } + + public void setVocabulary(URI vocabulary) { + this.vocabulary = vocabulary; + } + + public Set getTypes() { + return types; + } + + public void setTypes(Set types) { + this.types = types; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FacetedSearchResult)) { + return false; + } + FacetedSearchResult that = (FacetedSearchResult) o; + return Objects.equals(getUri(), that.getUri()); + } + + @Override + public int hashCode() { + return Objects.hash(getUri()); + } + + @Override + public String toString() { + return "FacetedSearchResult{" + + getLabel() + ' ' + + Utils.uriToString(getUri()) + + ", types=" + getTypes() + + '}'; + } +} diff --git a/src/main/java/cz/cvut/kbss/termit/dto/search/MatchType.java b/src/main/java/cz/cvut/kbss/termit/dto/search/MatchType.java new file mode 100644 index 000000000..7e0016c6d --- /dev/null +++ b/src/main/java/cz/cvut/kbss/termit/dto/search/MatchType.java @@ -0,0 +1,19 @@ +package cz.cvut.kbss.termit.dto.search; + +/** + * Describes how the property value should be matched in the data. + */ +public enum MatchType { + /** + * Matches resource identifier in the repository. + */ + IRI, + /** + * Matches the specified value as a substring of the string representation of a property value in the repository. + */ + SUBSTRING, + /** + * Matches the specified value exactly to the string representation of a property value in the repository. + */ + EXACT_MATCH +} diff --git a/src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java b/src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java new file mode 100644 index 000000000..6e29716aa --- /dev/null +++ b/src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java @@ -0,0 +1,50 @@ +package cz.cvut.kbss.termit.dto.search; + +import cz.cvut.kbss.termit.util.Utils; + +import java.net.URI; + +/** + * Parameter of the faceted term search. + */ +public class SearchParam { + + private URI property; + + private String value; + + private MatchType matchType; + + public URI getProperty() { + return property; + } + + public void setProperty(URI property) { + this.property = property; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public MatchType getMatchType() { + return matchType; + } + + public void setMatchType(MatchType matchType) { + this.matchType = matchType; + } + + @Override + public String toString() { + return "SearchParam{" + + "property=" + Utils.uriToString(property) + + ", value='" + value + '\'' + + ", matchType=" + matchType + + '}'; + } +} diff --git a/src/main/java/cz/cvut/kbss/termit/exception/UnsupportedSearchFacetException.java b/src/main/java/cz/cvut/kbss/termit/exception/UnsupportedSearchFacetException.java new file mode 100644 index 000000000..1a58c04ec --- /dev/null +++ b/src/main/java/cz/cvut/kbss/termit/exception/UnsupportedSearchFacetException.java @@ -0,0 +1,11 @@ +package cz.cvut.kbss.termit.exception; + +/** + * Indicates that an unsupported facet was provided to faceted search. + */ +public class UnsupportedSearchFacetException extends TermItException { + + public UnsupportedSearchFacetException(String message) { + super(message); + } +} diff --git a/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java b/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java index 204fb5180..213aea9bc 100644 --- a/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java +++ b/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java @@ -16,6 +16,8 @@ import cz.cvut.kbss.jsonld.JsonLd; import cz.cvut.kbss.termit.dto.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.FacetedSearchResult; +import cz.cvut.kbss.termit.dto.search.SearchParam; import cz.cvut.kbss.termit.service.IdentifierResolver; import cz.cvut.kbss.termit.service.business.SearchService; import cz.cvut.kbss.termit.util.Configuration; @@ -27,12 +29,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.net.URI; +import java.util.Collection; import java.util.List; import java.util.Set; @@ -69,4 +69,15 @@ public List fullTextSearchTerms( @RequestParam(name = "vocabulary", required = false) Set vocabularies) { return searchService.fullTextSearchOfTerms(searchString, Utils.emptyIfNull(vocabularies)); } + + @Operation(description = "Runs a faceted search using the specified search parameters over all terms.") + @ApiResponse(responseCode = "200", description = "Search results.") + @PreAuthorize("permitAll()") + @PostMapping(value = "/faceted/terms", produces = {MediaType.APPLICATION_JSON_VALUE, JsonLd.MEDIA_TYPE}, + consumes = {MediaType.APPLICATION_JSON_VALUE}) + public List facetedTermSearch(@Parameter(description = "Search parameters.") + @RequestBody Collection searchParams) { + // TODO + return null; + } } diff --git a/src/main/java/cz/cvut/kbss/termit/rest/handler/RestExceptionHandler.java b/src/main/java/cz/cvut/kbss/termit/rest/handler/RestExceptionHandler.java index c40311a3f..aff5a30b3 100644 --- a/src/main/java/cz/cvut/kbss/termit/rest/handler/RestExceptionHandler.java +++ b/src/main/java/cz/cvut/kbss/termit/rest/handler/RestExceptionHandler.java @@ -195,4 +195,11 @@ public ResponseEntity snapshotNotEditableException(HttpServletRequest return new ResponseEntity<>(ErrorInfo.createWithMessage(e.getMessage(), request.getRequestURI()), HttpStatus.CONFLICT); } + + @ExceptionHandler + public ResponseEntity unsupportedSearchFacetException(HttpServletRequest request, + UnsupportedSearchFacetException e) { + logException(e); + return new ResponseEntity<>(errorInfo(request, e), HttpStatus.BAD_REQUEST); + } } From ff19f6915e4054951083659196e2fe7a301df29c Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Thu, 8 Jun 2023 13:15:05 +0200 Subject: [PATCH 02/12] [Ref] Move FullTextSearchResult into the search package in dtos. --- .../cvut/kbss/termit/dto/{ => search}/FullTextSearchResult.java | 2 +- .../java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java | 2 +- .../kbss/termit/persistence/dao/lucene/LuceneSearchDao.java | 2 +- src/main/java/cz/cvut/kbss/termit/rest/SearchController.java | 2 +- .../cz/cvut/kbss/termit/service/business/SearchService.java | 2 +- .../security/authorization/SearchAuthorizationService.java | 2 +- .../java/cz/cvut/kbss/termit/dto/FullTextSearchResultTest.java | 1 + .../java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java | 2 +- .../kbss/termit/persistence/dao/lucene/LuceneSearchDaoTest.java | 2 +- .../java/cz/cvut/kbss/termit/rest/SearchControllerTest.java | 2 +- .../cz/cvut/kbss/termit/service/business/SearchServiceTest.java | 2 +- .../security/authorization/SearchAuthorizationServiceTest.java | 2 +- 12 files changed, 12 insertions(+), 11 deletions(-) rename src/main/java/cz/cvut/kbss/termit/dto/{ => search}/FullTextSearchResult.java (99%) diff --git a/src/main/java/cz/cvut/kbss/termit/dto/FullTextSearchResult.java b/src/main/java/cz/cvut/kbss/termit/dto/search/FullTextSearchResult.java similarity index 99% rename from src/main/java/cz/cvut/kbss/termit/dto/FullTextSearchResult.java rename to src/main/java/cz/cvut/kbss/termit/dto/search/FullTextSearchResult.java index 87c9b3bf5..a773b84e0 100644 --- a/src/main/java/cz/cvut/kbss/termit/dto/FullTextSearchResult.java +++ b/src/main/java/cz/cvut/kbss/termit/dto/search/FullTextSearchResult.java @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with this program. If not, see * . */ -package cz.cvut.kbss.termit.dto; +package cz.cvut.kbss.termit.dto.search; import cz.cvut.kbss.jopa.model.annotations.*; import cz.cvut.kbss.jopa.vocabulary.RDFS; diff --git a/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java b/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java index 11d788eae..e84114e10 100644 --- a/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java +++ b/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java @@ -17,7 +17,7 @@ import cz.cvut.kbss.jopa.model.EntityManager; import cz.cvut.kbss.jopa.model.query.Query; import cz.cvut.kbss.jopa.vocabulary.SKOS; -import cz.cvut.kbss.termit.dto.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.util.Configuration; import cz.cvut.kbss.termit.util.Utils; import cz.cvut.kbss.termit.util.Vocabulary; diff --git a/src/main/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDao.java b/src/main/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDao.java index 89b21d895..7e0fbbcef 100644 --- a/src/main/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDao.java +++ b/src/main/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDao.java @@ -15,7 +15,7 @@ package cz.cvut.kbss.termit.persistence.dao.lucene; import cz.cvut.kbss.jopa.model.EntityManager; -import cz.cvut.kbss.termit.dto.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.persistence.dao.SearchDao; import cz.cvut.kbss.termit.util.Configuration; import cz.cvut.kbss.termit.util.Vocabulary; diff --git a/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java b/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java index 213aea9bc..f918caeaa 100644 --- a/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java +++ b/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java @@ -15,7 +15,7 @@ package cz.cvut.kbss.termit.rest; import cz.cvut.kbss.jsonld.JsonLd; -import cz.cvut.kbss.termit.dto.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.dto.search.FacetedSearchResult; import cz.cvut.kbss.termit.dto.search.SearchParam; import cz.cvut.kbss.termit.service.IdentifierResolver; diff --git a/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java b/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java index 5da9b66c3..4aa4b09ec 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java +++ b/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java @@ -15,7 +15,7 @@ package cz.cvut.kbss.termit.service.business; import cz.cvut.kbss.jopa.vocabulary.SKOS; -import cz.cvut.kbss.termit.dto.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.persistence.dao.SearchDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PostFilter; diff --git a/src/main/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationService.java b/src/main/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationService.java index c4f099393..66f6db516 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationService.java +++ b/src/main/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationService.java @@ -1,7 +1,7 @@ package cz.cvut.kbss.termit.service.security.authorization; import cz.cvut.kbss.jopa.vocabulary.SKOS; -import cz.cvut.kbss.termit.dto.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.model.Vocabulary; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; diff --git a/src/test/java/cz/cvut/kbss/termit/dto/FullTextSearchResultTest.java b/src/test/java/cz/cvut/kbss/termit/dto/FullTextSearchResultTest.java index 8d97fdaf6..3ba578717 100644 --- a/src/test/java/cz/cvut/kbss/termit/dto/FullTextSearchResultTest.java +++ b/src/test/java/cz/cvut/kbss/termit/dto/FullTextSearchResultTest.java @@ -1,6 +1,7 @@ package cz.cvut.kbss.termit.dto; import cz.cvut.kbss.jopa.vocabulary.SKOS; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.environment.Generator; import org.junit.jupiter.api.Test; diff --git a/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java b/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java index dc0e353bc..1ce7ed4bd 100644 --- a/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java +++ b/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java @@ -15,7 +15,7 @@ package cz.cvut.kbss.termit.persistence.dao; import cz.cvut.kbss.jopa.model.EntityManager; -import cz.cvut.kbss.termit.dto.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.environment.Environment; import cz.cvut.kbss.termit.environment.Generator; import cz.cvut.kbss.termit.model.Term; diff --git a/src/test/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDaoTest.java b/src/test/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDaoTest.java index 8a4b46b14..e92b4ef4e 100644 --- a/src/test/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDaoTest.java +++ b/src/test/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDaoTest.java @@ -19,7 +19,7 @@ import cz.cvut.kbss.jopa.model.EntityManager; import cz.cvut.kbss.jopa.model.query.Query; -import cz.cvut.kbss.termit.dto.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.util.Configuration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java b/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java index 28795cfea..b65f177c7 100644 --- a/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java +++ b/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java @@ -15,7 +15,7 @@ package cz.cvut.kbss.termit.rest; import com.fasterxml.jackson.core.type.TypeReference; -import cz.cvut.kbss.termit.dto.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.environment.Generator; import cz.cvut.kbss.termit.service.business.SearchService; import cz.cvut.kbss.termit.util.Vocabulary; diff --git a/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java b/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java index 6ab47bd92..77bc9f961 100644 --- a/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java +++ b/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java @@ -1,7 +1,7 @@ package cz.cvut.kbss.termit.service.business; import cz.cvut.kbss.jopa.vocabulary.SKOS; -import cz.cvut.kbss.termit.dto.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.environment.Generator; import cz.cvut.kbss.termit.persistence.dao.SearchDao; import org.junit.jupiter.api.Test; diff --git a/src/test/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationServiceTest.java b/src/test/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationServiceTest.java index 21f76001f..10020f95a 100644 --- a/src/test/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationServiceTest.java +++ b/src/test/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationServiceTest.java @@ -1,7 +1,7 @@ package cz.cvut.kbss.termit.service.security.authorization; import cz.cvut.kbss.jopa.vocabulary.SKOS; -import cz.cvut.kbss.termit.dto.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.environment.Generator; import cz.cvut.kbss.termit.model.Vocabulary; import org.junit.jupiter.api.Test; From 055995967acdc93bc013d7d42d7b9f66045a0957 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Fri, 9 Jun 2023 09:47:18 +0200 Subject: [PATCH 03/12] [kbss-cvut/termit-ui#326] Implement faceted search REST endpoint and service. --- .../kbss/termit/dto/search/SearchParam.java | 54 +++++++++++++++++-- .../termit/persistence/dao/SearchDao.java | 21 +++++++- .../dao/lucene/LuceneSearchDao.java | 5 +- .../kbss/termit/rest/SearchController.java | 5 +- .../service/business/SearchService.java | 20 +++++++ .../SearchAuthorizationService.java | 14 +++++ .../termit/dto/search/SearchParamTest.java | 40 ++++++++++++++ .../kbss/termit/environment/Generator.java | 11 +++- .../termit/rest/SearchControllerTest.java | 28 ++++++++++ .../service/business/SearchServiceTest.java | 39 ++++++++++++-- .../SearchAuthorizationServiceTest.java | 11 ++++ 11 files changed, 232 insertions(+), 16 deletions(-) create mode 100644 src/test/java/cz/cvut/kbss/termit/dto/search/SearchParamTest.java diff --git a/src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java b/src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java index 6e29716aa..be34ed44b 100644 --- a/src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java +++ b/src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java @@ -1,8 +1,11 @@ package cz.cvut.kbss.termit.dto.search; +import cz.cvut.kbss.termit.exception.ValidationException; import cz.cvut.kbss.termit.util.Utils; import java.net.URI; +import java.util.Objects; +import java.util.Set; /** * Parameter of the faceted term search. @@ -11,9 +14,19 @@ public class SearchParam { private URI property; - private String value; + private Set value; - private MatchType matchType; + private MatchType matchType = MatchType.EXACT_MATCH; + + public SearchParam() { + } + + // For test purposes + public SearchParam(URI property, String value, MatchType matchType) { + this.property = Objects.requireNonNull(property); + this.value = Set.of(value); + this.matchType = Objects.requireNonNull(matchType); + } public URI getProperty() { return property; @@ -23,11 +36,11 @@ public void setProperty(URI property) { this.property = property; } - public String getValue() { + public Set getValue() { return value; } - public void setValue(String value) { + public void setValue(Set value) { this.value = value; } @@ -39,6 +52,39 @@ public void setMatchType(MatchType matchType) { this.matchType = matchType; } + /** + * Validates this search parameter. + *

+ * This mainly means checking that the values correspond to the match type, e.g., that a single value is provided + * for string-matching types. + */ + public void validate() { + if (Utils.emptyIfNull(value).isEmpty() || property == null) { + throw new ValidationException("Must provide a property and value to search by!"); + } + if (matchType != MatchType.IRI && Utils.emptyIfNull(value).size() != 1) { + throw new ValidationException("Exactly one value must be provided for match type " + matchType); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SearchParam)) { + return false; + } + SearchParam that = (SearchParam) o; + return Objects.equals(property, that.property) + && Objects.equals(value, that.value) && matchType == that.matchType; + } + + @Override + public int hashCode() { + return Objects.hash(property, value, matchType); + } + @Override public String toString() { return "SearchParam{" + diff --git a/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java b/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java index e84114e10..370a68dd5 100644 --- a/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java +++ b/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java @@ -17,7 +17,9 @@ import cz.cvut.kbss.jopa.model.EntityManager; import cz.cvut.kbss.jopa.model.query.Query; import cz.cvut.kbss.jopa.vocabulary.SKOS; +import cz.cvut.kbss.termit.dto.search.FacetedSearchResult; import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.SearchParam; import cz.cvut.kbss.termit.util.Configuration; import cz.cvut.kbss.termit.util.Utils; import cz.cvut.kbss.termit.util.Vocabulary; @@ -25,10 +27,12 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Repository; import javax.annotation.PostConstruct; import java.net.URI; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -70,7 +74,7 @@ private void loadQueries() { * @return List of matching results * @see #fullTextSearchIncludingSnapshots(String) */ - public List fullTextSearch(String searchString) { + public List fullTextSearch(@NonNull String searchString) { Objects.requireNonNull(searchString); if (searchString.isBlank()) { return Collections.emptyList(); @@ -93,7 +97,7 @@ public List fullTextSearch(String searchString) { * @return List of matching results * @see #fullTextSearchIncludingSnapshots(String) */ - public List fullTextSearchIncludingSnapshots(String searchString) { + public List fullTextSearchIncludingSnapshots(@NonNull String searchString) { Objects.requireNonNull(searchString); if (searchString.isBlank()) { return Collections.emptyList(); @@ -117,4 +121,17 @@ protected String queryIncludingSnapshots() { // This string has to match the filter string in the query return ftsQuery.replace("FILTER NOT EXISTS { ?entity a ?snapshot . }", ""); } + + /** + * Executes a faceted search among terms using the specified search parameters. + *

+ * Only current versions of terms are searched. + * + * @param searchParams Search parameters (facets) + * @return List of matching terms, ordered by label + */ + public List facetedTermSearch(@NonNull Collection searchParams) { + // TODO + return null; + } } diff --git a/src/main/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDao.java b/src/main/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDao.java index 7e0fbbcef..a6ac830d5 100644 --- a/src/main/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDao.java +++ b/src/main/java/cz/cvut/kbss/termit/persistence/dao/lucene/LuceneSearchDao.java @@ -22,6 +22,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Repository; import java.net.URI; @@ -49,7 +50,7 @@ public LuceneSearchDao(EntityManager em, Configuration config) { } @Override - public List fullTextSearch(String searchString) { + public List fullTextSearch(@NonNull String searchString) { Objects.requireNonNull(searchString); if (searchString.isBlank()) { return Collections.emptyList(); @@ -83,7 +84,7 @@ private static String splitExactMatch(String searchString) { } @Override - public List fullTextSearchIncludingSnapshots(String searchString) { + public List fullTextSearchIncludingSnapshots(@NonNull String searchString) { Objects.requireNonNull(searchString); if (searchString.isBlank()) { return Collections.emptyList(); diff --git a/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java b/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java index f918caeaa..7676aa6eb 100644 --- a/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java +++ b/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java @@ -15,8 +15,8 @@ package cz.cvut.kbss.termit.rest; import cz.cvut.kbss.jsonld.JsonLd; -import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.dto.search.FacetedSearchResult; +import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.dto.search.SearchParam; import cz.cvut.kbss.termit.service.IdentifierResolver; import cz.cvut.kbss.termit.service.business.SearchService; @@ -77,7 +77,6 @@ public List fullTextSearchTerms( consumes = {MediaType.APPLICATION_JSON_VALUE}) public List facetedTermSearch(@Parameter(description = "Search parameters.") @RequestBody Collection searchParams) { - // TODO - return null; + return searchService.facetedTermSearch(searchParams); } } diff --git a/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java b/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java index 4aa4b09ec..2c2d85a7f 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java +++ b/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java @@ -15,13 +15,17 @@ package cz.cvut.kbss.termit.service.business; import cz.cvut.kbss.jopa.vocabulary.SKOS; +import cz.cvut.kbss.termit.dto.search.FacetedSearchResult; import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.SearchParam; import cz.cvut.kbss.termit.persistence.dao.SearchDao; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; import org.springframework.security.access.prepost.PostFilter; import org.springframework.stereotype.Service; import java.net.URI; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Set; @@ -64,4 +68,20 @@ public List fullTextSearchOfTerms(String searchString, Set .filter(r -> vocabularies.contains(r.getVocabulary())) .collect(Collectors.toList()); } + + /** + * Executes a faceted search of terms based on the specified search parameters. + *

+ * The search parameters define facets by which terms should be searched together with corresponding search values. + * The search treats the parameters as conjunction, so the result has to match all the search parameters. + * + * @param searchParams Search parameters + * @return List of matching terms, sorted by label + */ + @PostFilter("@searchAuthorizationService.canRead(filterObject)") + public List facetedTermSearch(@NonNull Collection searchParams) { + Objects.requireNonNull(searchParams); + searchParams.forEach(SearchParam::validate); + return searchDao.facetedTermSearch(searchParams); + } } diff --git a/src/main/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationService.java b/src/main/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationService.java index 66f6db516..3b31fdeea 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationService.java +++ b/src/main/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationService.java @@ -1,6 +1,7 @@ package cz.cvut.kbss.termit.service.security.authorization; import cz.cvut.kbss.jopa.vocabulary.SKOS; +import cz.cvut.kbss.termit.dto.search.FacetedSearchResult; import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.model.Vocabulary; import org.springframework.lang.NonNull; @@ -38,4 +39,17 @@ public boolean canRead(@NonNull FullTextSearchResult instance) { return vocabularyAuthorizationService.canRead(new Vocabulary(instance.getUri())); } } + + /** + * Checks if the current user has read access to the specified faceted term search result. + *

+ * Authorization is based on vocabulary ACL. + * + * @param instance Faceted search result to authorize access to + * @return {@code true} if the current user can read the specified instance, {@code false} otherwise + */ + public boolean canRead(@NonNull FacetedSearchResult instance) { + Objects.requireNonNull(instance); + return vocabularyAuthorizationService.canRead(new Vocabulary(instance.getVocabulary())); + } } diff --git a/src/test/java/cz/cvut/kbss/termit/dto/search/SearchParamTest.java b/src/test/java/cz/cvut/kbss/termit/dto/search/SearchParamTest.java new file mode 100644 index 000000000..be508ff64 --- /dev/null +++ b/src/test/java/cz/cvut/kbss/termit/dto/search/SearchParamTest.java @@ -0,0 +1,40 @@ +package cz.cvut.kbss.termit.dto.search; + +import cz.cvut.kbss.jopa.vocabulary.RDF; +import cz.cvut.kbss.jopa.vocabulary.SKOS; +import cz.cvut.kbss.termit.exception.ValidationException; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class SearchParamTest { + + @Test + void validateThrowsValidationExceptionWhenMatchTypeIsSubStringAndParamsHasMultipleValues() { + final SearchParam sut = new SearchParam(); + sut.setProperty(URI.create(SKOS.NOTATION)); + sut.setValue(Set.of("one", "two", "three")); + sut.setMatchType(MatchType.SUBSTRING); + assertThrows(ValidationException.class, sut::validate); + } + + @Test + void validateThrowsValidationExceptionWhenMatchTypeIsExactMatchAndParamsHasMultipleValues() { + final SearchParam sut = new SearchParam(); + sut.setProperty(URI.create(SKOS.NOTATION)); + sut.setValue(Set.of("one", "two")); + sut.setMatchType(MatchType.EXACT_MATCH); + assertThrows(ValidationException.class, sut::validate); + } + + @Test + void validateThrowsValidationExceptionWhenNoValueIsProvided() { + final SearchParam sut = new SearchParam(); + sut.setProperty(URI.create(RDF.TYPE)); + sut.setMatchType(MatchType.IRI); + assertThrows(ValidationException.class, sut::validate); + } +} diff --git a/src/test/java/cz/cvut/kbss/termit/environment/Generator.java b/src/test/java/cz/cvut/kbss/termit/environment/Generator.java index ea986a091..2f4b29625 100644 --- a/src/test/java/cz/cvut/kbss/termit/environment/Generator.java +++ b/src/test/java/cz/cvut/kbss/termit/environment/Generator.java @@ -52,7 +52,7 @@ private Generator() { } /** - * Generates a (pseudo) random URI, usable for test individuals. + * Generates a (pseudo) random URI. * * @return Random URI */ @@ -60,6 +60,15 @@ public static URI generateUri() { return URI.create(Environment.BASE_URI + "/randomInstance" + randomInt()); } + /** + * Generates a (pseudo) random URI and returns it as a string. + * + * @return Random URI string + */ + public static String generateUriString() { + return generateUri().toString(); + } + /** * Generates a (pseudo-)random integer between the specified lower and upper bounds. * diff --git a/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java b/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java index b65f177c7..67d735f16 100644 --- a/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java +++ b/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java @@ -15,7 +15,14 @@ package cz.cvut.kbss.termit.rest; import com.fasterxml.jackson.core.type.TypeReference; +import cz.cvut.kbss.jopa.model.MultilingualString; +import cz.cvut.kbss.jopa.vocabulary.RDF; +import cz.cvut.kbss.jopa.vocabulary.SKOS; +import cz.cvut.kbss.termit.dto.search.FacetedSearchResult; import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.MatchType; +import cz.cvut.kbss.termit.dto.search.SearchParam; +import cz.cvut.kbss.termit.environment.Environment; import cz.cvut.kbss.termit.environment.Generator; import cz.cvut.kbss.termit.service.business.SearchService; import cz.cvut.kbss.termit.util.Vocabulary; @@ -25,6 +32,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; import java.net.URI; @@ -33,9 +41,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(MockitoExtension.class) @@ -87,4 +97,22 @@ void fullTextSearchOfTermsWithoutVocabularySpecificationExecutesSearchOnService( .andExpect(status().isOk()).andReturn(); verify(searchServiceMock).fullTextSearchOfTerms(searchString, Collections.singleton(vocabularyIri)); } + + @Test + void facetedTermSearchPassesSearchParametersToSearchService() throws Exception { + final FacetedSearchResult term = new FacetedSearchResult(); + term.setUri(Generator.generateUri()); + term.setLabel(MultilingualString.create("Test term", Environment.LANGUAGE)); + when(searchServiceMock.facetedTermSearch(anyCollection())).thenReturn(List.of(term)); + final List searchParams = List.of( + new SearchParam(URI.create(SKOS.NOTATION), "LA_", MatchType.EXACT_MATCH), + new SearchParam(URI.create(RDF.TYPE), Generator.generateUri().toString(), MatchType.IRI)); + final MvcResult mvcResult = mockMvc.perform( + post(PATH + "/faceted/terms").content(toJson(searchParams)).contentType( + MediaType.APPLICATION_JSON)).andReturn(); + final List result = readValue(mvcResult, new TypeReference>() { + }); + assertEquals(List.of(term), result); + verify(searchServiceMock).facetedTermSearch(searchParams); + } } diff --git a/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java b/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java index 77bc9f961..ef2e40610 100644 --- a/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java +++ b/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java @@ -1,8 +1,15 @@ package cz.cvut.kbss.termit.service.business; +import cz.cvut.kbss.jopa.model.MultilingualString; +import cz.cvut.kbss.jopa.vocabulary.RDF; import cz.cvut.kbss.jopa.vocabulary.SKOS; +import cz.cvut.kbss.termit.dto.search.FacetedSearchResult; import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.MatchType; +import cz.cvut.kbss.termit.dto.search.SearchParam; +import cz.cvut.kbss.termit.environment.Environment; import cz.cvut.kbss.termit.environment.Generator; +import cz.cvut.kbss.termit.exception.ValidationException; import cz.cvut.kbss.termit.persistence.dao.SearchDao; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,12 +19,12 @@ import java.net.URI; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class SearchServiceTest { @@ -67,4 +74,28 @@ void fullTextSearchReturnsResultsFromMatchingVocabularies() { assertEquals(Collections.singletonList(ftsr), result); verify(searchDao).fullTextSearchIncludingSnapshots(searchString); } + + @Test + void facetedTermSearchValidatesEachSearchParamBeforeInvokingSearch() { + final SearchParam spOne = new SearchParam(URI.create(RDF.TYPE), Generator.generateUriString(), MatchType.IRI); + final SearchParam spTwo = new SearchParam(URI.create(SKOS.NOTATION), "will be removed", MatchType.SUBSTRING); + spTwo.setValue(null); + assertThrows(ValidationException.class, () -> sut.facetedTermSearch(List.of(spOne, spTwo))); + verify(searchDao, never()).facetedTermSearch(anyCollection()); + } + + @Test + void facetedTermSearchExecutesSearchOnDaoAndReturnsResults() { + final SearchParam spOne = new SearchParam(URI.create(RDF.TYPE), Generator.generateUriString(), MatchType.IRI); + final FacetedSearchResult item = new FacetedSearchResult(); + item.setUri(Generator.generateUri()); + item.setLabel(MultilingualString.create("Test term", Environment.LANGUAGE)); + item.setVocabulary(Generator.generateUri()); + item.setTypes(new HashSet<>(spOne.getValue())); + when(searchDao.facetedTermSearch(anyCollection())).thenReturn(List.of(item)); + + final List result = sut.facetedTermSearch(Set.of(spOne)); + assertEquals(List.of(item), result); + verify(searchDao).facetedTermSearch(Set.of(spOne)); + } } diff --git a/src/test/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationServiceTest.java b/src/test/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationServiceTest.java index 10020f95a..5d3e9fafd 100644 --- a/src/test/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationServiceTest.java +++ b/src/test/java/cz/cvut/kbss/termit/service/security/authorization/SearchAuthorizationServiceTest.java @@ -1,6 +1,7 @@ package cz.cvut.kbss.termit.service.security.authorization; import cz.cvut.kbss.jopa.vocabulary.SKOS; +import cz.cvut.kbss.termit.dto.search.FacetedSearchResult; import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.environment.Generator; import cz.cvut.kbss.termit.model.Vocabulary; @@ -46,4 +47,14 @@ void canReadChecksIfVocabularyIsReadableForVocabularyResult() { assertFalse(sut.canRead(res)); verify(vocabularyAuthorizationService).canRead(new Vocabulary(res.getUri())); } + + @Test + void canReadChecksIfVocabularyIsReadableForFacetedSearchTermResult() { + final FacetedSearchResult res = new FacetedSearchResult(); + res.setUri(Generator.generateUri()); + res.setVocabulary(Generator.generateUri()); + when(vocabularyAuthorizationService.canRead(any(Vocabulary.class))).thenReturn(true); + assertTrue(sut.canRead(res)); + verify(vocabularyAuthorizationService).canRead(new Vocabulary(res.getVocabulary())); + } } From bdd0bfd6e37e667fa1442f689db6f732e71e3d50 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Fri, 9 Jun 2023 12:18:59 +0200 Subject: [PATCH 04/12] [Perf #194] Refactor SearchDaoTest to reuse test data between tests. The tests only query data, so there should be no interference between tests. --- .../dto/search/FullTextSearchResult.java | 5 +- .../termit/persistence/dao/SearchDaoTest.java | 97 +++++++++---------- 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/termit/dto/search/FullTextSearchResult.java b/src/main/java/cz/cvut/kbss/termit/dto/search/FullTextSearchResult.java index a773b84e0..3565b96df 100644 --- a/src/main/java/cz/cvut/kbss/termit/dto/search/FullTextSearchResult.java +++ b/src/main/java/cz/cvut/kbss/termit/dto/search/FullTextSearchResult.java @@ -16,6 +16,7 @@ import cz.cvut.kbss.jopa.model.annotations.*; import cz.cvut.kbss.jopa.vocabulary.RDFS; +import cz.cvut.kbss.termit.model.util.HasIdentifier; import cz.cvut.kbss.termit.model.util.HasTypes; import cz.cvut.kbss.termit.util.Vocabulary; @@ -38,7 +39,7 @@ type = String.class), @VariableResult(name = "score", type = Double.class) })}) -public class FullTextSearchResult implements HasTypes, Serializable { +public class FullTextSearchResult implements HasIdentifier, HasTypes, Serializable { @Id private URI uri; @@ -80,10 +81,12 @@ public FullTextSearchResult(URI uri, String label, URI vocabulary, Boolean draft this.score = score; } + @Override public URI getUri() { return uri; } + @Override public void setUri(URI uri) { this.uri = uri; } diff --git a/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java b/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java index 1ce7ed4bd..b45b20473 100644 --- a/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java +++ b/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java @@ -15,6 +15,7 @@ package cz.cvut.kbss.termit.persistence.dao; import cz.cvut.kbss.jopa.model.EntityManager; +import cz.cvut.kbss.jopa.vocabulary.SKOS; import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.environment.Environment; import cz.cvut.kbss.termit.environment.Generator; @@ -22,6 +23,7 @@ import cz.cvut.kbss.termit.model.User; import cz.cvut.kbss.termit.model.Vocabulary; import cz.cvut.kbss.termit.model.util.HasIdentifier; +import cz.cvut.kbss.termit.persistence.context.DescriptorFactory; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.vocabulary.RDF; import org.eclipse.rdf4j.repository.Repository; @@ -29,7 +31,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; import java.util.ArrayList; import java.util.Collection; @@ -38,59 +39,67 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import static cz.cvut.kbss.termit.model.util.EntityToOwlClassMapper.getOwlClassForEntity; -import static org.junit.jupiter.api.Assertions.*; +import static cz.cvut.kbss.termit.environment.util.ContainsSameEntities.containsSameEntities; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * This class tests the default full text search functionality. *

* Repository-tailored queries stored in corresponding profiles should be used in production. */ -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class SearchDaoTest extends BaseDaoTestRunner { @Autowired private EntityManager em; + @Autowired + private DescriptorFactory descriptorFactory; + @Autowired private SearchDao sut; + private static boolean initialized = false; + private User user; private Vocabulary vocabulary; + private static List terms; + private static List vocabularies; + @BeforeEach void setUp() { this.user = Generator.generateUserWithId(); - transactional(() -> em.persist(user)); + this.vocabulary = Generator.generateVocabularyWithId(); + if (!initialized) { + transactional(() -> { + em.persist(user); + em.persist(vocabulary, descriptorFactory.vocabularyDescriptor(vocabulary)); + terms = generateTerms(); + terms.forEach(t -> em.persist(t, descriptorFactory.termDescriptor(t))); + vocabularies = generateVocabularies(); + vocabularies.forEach(v -> em.persist(v, descriptorFactory.vocabularyDescriptor(v))); + }); + initialized = true; + } Environment.setCurrentUser(user); } @Test void defaultFullTextSearchFindsTermsWithMatchingLabel() { - final List terms = generateAndPersistTerms(); final Collection matching = terms.stream().filter(t -> t.getPrimaryLabel().contains("Matching")) .collect(Collectors.toList()); - final List result = sut.fullTextSearch("matching"); - assertEquals(matching.size(), result.size()); - for (FullTextSearchResult item : result) { - assertTrue(item.getTypes().contains(getOwlClassForEntity(Term.class))); - assertTrue(matching.stream().anyMatch(t -> t.getUri().equals(item.getUri()))); - } - } - - private List generateAndPersistTerms() { - final List terms = generateTerms(); - transactional(() -> { - em.persist(vocabulary); - terms.forEach(em::persist); - }); - return terms; + final List allResults = sut.fullTextSearch("matching"); + final List termResults = allResults.stream().filter(r -> r.hasType(SKOS.CONCEPT)).collect( + Collectors.toList()); + assertEquals(matching.size(), termResults.size()); + assertThat(termResults, containsSameEntities(matching)); } private List generateTerms() { - this.vocabulary = Generator.generateVocabularyWithId(); final List terms = new ArrayList<>(10); for (int i = 0; i < Generator.randomInt(5, 10); i++) { final Term term = new Term(); @@ -98,6 +107,7 @@ private List generateTerms() { term.setPrimaryLabel(Generator.randomBoolean() ? "Matching label " + i : "Unknown label " + i); vocabulary.getGlossary().addRootTerm(term); term.setVocabulary(vocabulary.getUri()); + term.setDraft(Generator.randomBoolean()); terms.add(term); } return terms; @@ -105,16 +115,15 @@ private List generateTerms() { @Test void defaultFullTextSearchFindsVocabulariesWithMatchingLabel() { - final List vocabularies = generateVocabularies(); - transactional(() -> vocabularies.forEach(em::persist)); final Collection matching = vocabularies.stream().filter(v -> v.getLabel().contains("Matching")) .collect(Collectors.toList()); - final List result = sut.fullTextSearch("matching"); - assertEquals(matching.size(), result.size()); - for (FullTextSearchResult item : result) { - assertTrue(item.getTypes().contains(getOwlClassForEntity(Vocabulary.class))); - assertTrue(matching.stream().anyMatch(t -> t.getUri().equals(item.getUri()))); - } + final List allResults = sut.fullTextSearch("matching"); + final List vocabularyResults = allResults.stream() + .filter(r -> r.hasType( + cz.cvut.kbss.termit.util.Vocabulary.s_c_slovnik)) + .collect(Collectors.toList()); + assertEquals(matching.size(), vocabularyResults.size()); + assertThat(vocabularyResults, containsSameEntities(matching)); } private List generateVocabularies() { @@ -131,13 +140,6 @@ private List generateVocabularies() { @Test void defaultFullTextSearchFindsVocabulariesAndTermsWithMatchingLabel() { - final List terms = generateTerms(); - final List vocabularies = generateVocabularies(); - transactional(() -> { - em.persist(vocabulary); - terms.forEach(em::persist); - vocabularies.forEach(em::persist); - }); final Collection matchingTerms = terms.stream().filter(t -> t.getPrimaryLabel().contains("Matching")) .collect(Collectors.toList()); final Collection matchingVocabularies = vocabularies.stream() @@ -146,7 +148,7 @@ void defaultFullTextSearchFindsVocabulariesAndTermsWithMatchingLabel() { final List result = sut.fullTextSearch("matching"); assertEquals(matchingTerms.size() + matchingVocabularies.size(), result.size()); for (FullTextSearchResult item : result) { - if (item.getTypes().contains(getOwlClassForEntity(Term.class))) { + if (item.hasType(SKOS.CONCEPT)) { assertTrue(matchingTerms.stream().anyMatch(t -> t.getUri().equals(item.getUri()))); } else { assertTrue(matchingVocabularies.stream().anyMatch(v -> v.getUri().equals(item.getUri()))); @@ -156,20 +158,14 @@ void defaultFullTextSearchFindsVocabulariesAndTermsWithMatchingLabel() { @Test void defaultFullTextSearchIncludesDraftStatusInResult() { - final List terms = generateTerms(); - transactional(() -> { - em.persist(vocabulary); - terms.forEach(t -> { - t.setDraft(Generator.randomBoolean()); - em.persist(t); - }); - }); final Collection matching = terms.stream().filter(t -> t.getPrimaryLabel().contains("Matching")) .collect(Collectors.toList()); - final List result = sut.fullTextSearch("matching"); - assertEquals(matching.size(), result.size()); - for (FullTextSearchResult ftsResult : result) { + final List allResults = sut.fullTextSearch("matching"); + final List termResults = allResults.stream() + .filter(r -> r.hasType(SKOS.CONCEPT)) + .collect(Collectors.toList()); + for (FullTextSearchResult ftsResult : termResults) { final Optional term = matching.stream().filter(t -> t.getUri().equals(ftsResult.getUri())) .findFirst(); assertTrue(term.isPresent()); @@ -179,14 +175,13 @@ void defaultFullTextSearchIncludesDraftStatusInResult() { @Test void defaultFullTextSearchReturnsEmptyListForEmptyInputString() { - generateAndPersistTerms(); final List result = sut.fullTextSearch(""); assertTrue(result.isEmpty()); } @Test void defaultFullTextSearchSkipsSnapshots() { - final String matchingLabel = "Matching"; + final String matchingLabel = "Snapshot"; final Vocabulary v = Generator.generateVocabularyWithId(); v.setLabel(matchingLabel + " 0"); final Vocabulary snapshot = Generator.generateVocabularyWithId(); From d8cf8259c4906a63199bd9bb066dcdcfe28c716a Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Fri, 9 Jun 2023 16:45:44 +0200 Subject: [PATCH 05/12] [kbss-cvut/termit-ui#326] Implement faceted term search. --- .../kbss/termit/dto/search/MatchType.java | 2 + .../kbss/termit/dto/search/SearchParam.java | 4 +- .../termit/persistence/dao/SearchDao.java | 34 +++++++- .../service/init/AdminAccountGenerator.java | 4 +- .../cz/cvut/kbss/termit/util/Constants.java | 5 ++ .../termit/persistence/dao/SearchDaoTest.java | 80 +++++++++++++++++-- .../termit/rest/SearchControllerTest.java | 5 +- .../service/business/SearchServiceTest.java | 6 +- 8 files changed, 121 insertions(+), 19 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/termit/dto/search/MatchType.java b/src/main/java/cz/cvut/kbss/termit/dto/search/MatchType.java index 7e0016c6d..0031d07b0 100644 --- a/src/main/java/cz/cvut/kbss/termit/dto/search/MatchType.java +++ b/src/main/java/cz/cvut/kbss/termit/dto/search/MatchType.java @@ -10,6 +10,8 @@ public enum MatchType { IRI, /** * Matches the specified value as a substring of the string representation of a property value in the repository. + * + * Note that this match is not case-sensitive. */ SUBSTRING, /** diff --git a/src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java b/src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java index be34ed44b..19a131421 100644 --- a/src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java +++ b/src/main/java/cz/cvut/kbss/termit/dto/search/SearchParam.java @@ -22,9 +22,9 @@ public SearchParam() { } // For test purposes - public SearchParam(URI property, String value, MatchType matchType) { + public SearchParam(URI property, Set value, MatchType matchType) { this.property = Objects.requireNonNull(property); - this.value = Set.of(value); + this.value = value; this.matchType = Objects.requireNonNull(matchType); } diff --git a/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java b/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java index 370a68dd5..8201d8738 100644 --- a/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java +++ b/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java @@ -36,6 +36,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; @Repository @Profile("!lucene") @@ -131,7 +132,36 @@ protected String queryIncludingSnapshots() { * @return List of matching terms, ordered by label */ public List facetedTermSearch(@NonNull Collection searchParams) { - // TODO - return null; + Objects.requireNonNull(searchParams); + LOG.trace("Running faceted term search for search parameters: {}", searchParams); + final StringBuilder queryStr = new StringBuilder( + "SELECT DISTINCT ?t WHERE { ?t a ?term ; ?hasLabel ?label .\n"); + int i = 0; + for (SearchParam p : searchParams) { + final String variable = "?v" + i++; + queryStr.append("?t ").append(Utils.uriToString(p.getProperty())).append(" ").append(variable) + .append(" . "); + switch (p.getMatchType()) { + case IRI: + queryStr.append("FILTER (").append(variable).append(" IN (") + .append(p.getValue().stream().map(v -> Utils.uriToString(URI.create(v))).collect( + Collectors.joining(","))).append("))\n"); + break; + case EXACT_MATCH: + queryStr.append("FILTER (STR(").append(variable).append(") = \"") + .append(p.getValue().iterator().next()).append("\")\n"); + break; + case SUBSTRING: + queryStr.append("FILTER (CONTAINS(LCASE(STR(").append(variable).append(")), LCASE(\"") + .append(p.getValue().iterator().next()).append("\")))\n"); + break; + } + } + queryStr.append("FILTER NOT EXISTS { ?t a ?snapshot . }} ORDER BY ?label"); + return em.createNativeQuery(queryStr.toString(), FacetedSearchResult.class) + .setParameter("term", URI.create(SKOS.CONCEPT)) + .setParameter("hasLabel", URI.create(SKOS.PREF_LABEL)) + .setParameter("snapshot", URI.create(Vocabulary.s_c_verze_objektu)) + .getResultList(); } } diff --git a/src/main/java/cz/cvut/kbss/termit/service/init/AdminAccountGenerator.java b/src/main/java/cz/cvut/kbss/termit/service/init/AdminAccountGenerator.java index 7f5917ef4..4d5258175 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/init/AdminAccountGenerator.java +++ b/src/main/java/cz/cvut/kbss/termit/service/init/AdminAccountGenerator.java @@ -3,6 +3,7 @@ import cz.cvut.kbss.termit.model.UserAccount; import cz.cvut.kbss.termit.service.repository.UserRepositoryService; import cz.cvut.kbss.termit.util.Configuration; +import cz.cvut.kbss.termit.util.Constants; import cz.cvut.kbss.termit.util.Vocabulary; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,7 +23,6 @@ public class AdminAccountGenerator { private static final Logger LOG = LoggerFactory.getLogger(AdminAccountGenerator.class); - private static final String LETTERS = "abcdefghijklmnopqrstuvwxyz"; private static final int PASSWORD_LENGTH = 8; private final UserRepositoryService userService; @@ -101,7 +101,7 @@ private static String generatePassword() { if (random.nextBoolean()) { sb.append(random.nextInt(10)); } else { - char c = LETTERS.charAt(random.nextInt(LETTERS.length())); + char c = Constants.LETTERS.charAt(random.nextInt(Constants.LETTERS.length())); sb.append(random.nextBoolean() ? c : Character.toUpperCase(c)); } } diff --git a/src/main/java/cz/cvut/kbss/termit/util/Constants.java b/src/main/java/cz/cvut/kbss/termit/util/Constants.java index 63ae6b116..e01d993ec 100644 --- a/src/main/java/cz/cvut/kbss/termit/util/Constants.java +++ b/src/main/java/cz/cvut/kbss/termit/util/Constants.java @@ -33,6 +33,11 @@ */ public class Constants { + /** + * Letters of the (English) alphabet. + */ + public static final String LETTERS = "abcdefghijklmnopqrstuvwxyz"; + /** * URL path to the application's REST API. */ diff --git a/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java b/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java index b45b20473..7975a6769 100644 --- a/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java +++ b/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java @@ -15,8 +15,12 @@ package cz.cvut.kbss.termit.persistence.dao; import cz.cvut.kbss.jopa.model.EntityManager; +import cz.cvut.kbss.jopa.model.MultilingualString; import cz.cvut.kbss.jopa.vocabulary.SKOS; +import cz.cvut.kbss.termit.dto.search.FacetedSearchResult; import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; +import cz.cvut.kbss.termit.dto.search.MatchType; +import cz.cvut.kbss.termit.dto.search.SearchParam; import cz.cvut.kbss.termit.environment.Environment; import cz.cvut.kbss.termit.environment.Generator; import cz.cvut.kbss.termit.model.Term; @@ -24,6 +28,7 @@ import cz.cvut.kbss.termit.model.Vocabulary; import cz.cvut.kbss.termit.model.util.HasIdentifier; import cz.cvut.kbss.termit.persistence.context.DescriptorFactory; +import cz.cvut.kbss.termit.util.Constants; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.vocabulary.RDF; import org.eclipse.rdf4j.repository.Repository; @@ -32,17 +37,16 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; +import java.net.URI; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; import static cz.cvut.kbss.termit.environment.util.ContainsSameEntities.containsSameEntities; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.hasItem; +import static org.junit.jupiter.api.Assertions.*; /** * This class tests the default full text search functionality. @@ -51,6 +55,10 @@ */ class SearchDaoTest extends BaseDaoTestRunner { + private static final String[] TYPES = {"http://onto.fel.cvut.cz/ontologies/ufo/event", + "http://onto.fel.cvut.cz/ontologies/ufo/object", + "http://onto.fel.cvut.cz/ontologies/ufo/relator"}; + @Autowired private EntityManager em; @@ -102,12 +110,18 @@ void defaultFullTextSearchFindsTermsWithMatchingLabel() { private List generateTerms() { final List terms = new ArrayList<>(10); for (int i = 0; i < Generator.randomInt(5, 10); i++) { + final boolean b = Generator.randomBoolean(); final Term term = new Term(); term.setUri(Generator.generateUri()); - term.setPrimaryLabel(Generator.randomBoolean() ? "Matching label " + i : "Unknown label " + i); + term.setPrimaryLabel(b ? "Matching label " + i : "Unknown label " + i); vocabulary.getGlossary().addRootTerm(term); term.setVocabulary(vocabulary.getUri()); - term.setDraft(Generator.randomBoolean()); + term.setDraft(b); + term.addType(TYPES[Generator.randomIndex(TYPES)]); + term.setNotations(Set.of(String.valueOf( + Constants.LETTERS.charAt(Generator.randomInt(0, Constants.LETTERS.length()))))); + term.setExamples(Set.of(MultilingualString.create(b ? "Matching" : "Unknown" + " example " + i, + Environment.LANGUAGE))); terms.add(term); } return terms; @@ -205,4 +219,54 @@ private void insertSnapshotType(HasIdentifier asset) { cz.cvut.kbss.termit.util.Vocabulary.s_c_verze_objektu)); } } + + @Test + void facetedTermSearchReturnsTermsMatchingIriSearchParamWithSpecifiedTypes() { + final SearchParam param = new SearchParam(URI.create(RDF.TYPE.stringValue()), Set.of(TYPES[0], TYPES[1]), + MatchType.IRI); + final List result = sut.facetedTermSearch(Set.of(param)); + assertFalse(result.isEmpty()); + result.forEach(r -> assertThat(r.getTypes(), anyOf(hasItem(TYPES[0]), hasItem(TYPES[1])))); + } + + @Test + void facetedTermSearchReturnsTermsMatchingExactMatchSearchParamWithSpecifiedValue() { + final SearchParam param = new SearchParam(URI.create(SKOS.NOTATION), + Set.of(terms.get(0).getNotations().iterator().next()), + MatchType.EXACT_MATCH); + final List matchingTerms = terms.stream() + .filter(t -> !Collections.disjoint(t.getNotations(), param.getValue())) + .collect(Collectors.toList()); + final List result = sut.facetedTermSearch(Set.of(param)); + assertFalse(result.isEmpty()); + assertThat(result, containsSameEntities(matchingTerms)); + } + + @Test + void facetedTermSearchReturnsTermsMatchingSubstringSearchParamWithSpecifiedValue() { + final SearchParam param = new SearchParam(URI.create(SKOS.EXAMPLE), + Set.of("matching"), + MatchType.SUBSTRING); + final List matchingTerms = terms.stream().filter(t -> t.getExamples().iterator().next().get() + .startsWith("Matching")) + .collect(Collectors.toList()); + final List result = sut.facetedTermSearch(Set.of(param)); + assertFalse(result.isEmpty()); + assertThat(result, containsSameEntities(matchingTerms)); + } + + @Test + void facetedTermSearchReturnsResultsMatchingMultipleSearchParameters() { + final SearchParam typeParam = new SearchParam(URI.create(RDF.TYPE.stringValue()), Set.of(TYPES[0], TYPES[1]), + MatchType.IRI); + final SearchParam substringParam = new SearchParam(URI.create(SKOS.EXAMPLE), + Set.of("matching"), + MatchType.SUBSTRING); + final List matchingTerms = terms.stream().filter(t -> t.getExamples().iterator().next().get() + .startsWith("Matching") && (t.hasType( + TYPES[0]) || t.hasType(TYPES[1]))) + .collect(Collectors.toList()); + final List result = sut.facetedTermSearch(Set.of(typeParam, substringParam)); + assertThat(result, containsSameEntities(matchingTerms)); + } } diff --git a/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java b/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java index 67d735f16..7f18f871b 100644 --- a/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java +++ b/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java @@ -38,6 +38,7 @@ import java.net.URI; import java.util.Collections; import java.util.List; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -105,8 +106,8 @@ void facetedTermSearchPassesSearchParametersToSearchService() throws Exception { term.setLabel(MultilingualString.create("Test term", Environment.LANGUAGE)); when(searchServiceMock.facetedTermSearch(anyCollection())).thenReturn(List.of(term)); final List searchParams = List.of( - new SearchParam(URI.create(SKOS.NOTATION), "LA_", MatchType.EXACT_MATCH), - new SearchParam(URI.create(RDF.TYPE), Generator.generateUri().toString(), MatchType.IRI)); + new SearchParam(URI.create(SKOS.NOTATION), Set.of("LA_"), MatchType.EXACT_MATCH), + new SearchParam(URI.create(RDF.TYPE), Set.of(Generator.generateUri().toString()), MatchType.IRI)); final MvcResult mvcResult = mockMvc.perform( post(PATH + "/faceted/terms").content(toJson(searchParams)).contentType( MediaType.APPLICATION_JSON)).andReturn(); diff --git a/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java b/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java index ef2e40610..3e7785ca8 100644 --- a/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java +++ b/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java @@ -77,8 +77,8 @@ void fullTextSearchReturnsResultsFromMatchingVocabularies() { @Test void facetedTermSearchValidatesEachSearchParamBeforeInvokingSearch() { - final SearchParam spOne = new SearchParam(URI.create(RDF.TYPE), Generator.generateUriString(), MatchType.IRI); - final SearchParam spTwo = new SearchParam(URI.create(SKOS.NOTATION), "will be removed", MatchType.SUBSTRING); + final SearchParam spOne = new SearchParam(URI.create(RDF.TYPE), Set.of(Generator.generateUriString()), MatchType.IRI); + final SearchParam spTwo = new SearchParam(URI.create(SKOS.NOTATION), Set.of("will be removed"), MatchType.SUBSTRING); spTwo.setValue(null); assertThrows(ValidationException.class, () -> sut.facetedTermSearch(List.of(spOne, spTwo))); verify(searchDao, never()).facetedTermSearch(anyCollection()); @@ -86,7 +86,7 @@ void facetedTermSearchValidatesEachSearchParamBeforeInvokingSearch() { @Test void facetedTermSearchExecutesSearchOnDaoAndReturnsResults() { - final SearchParam spOne = new SearchParam(URI.create(RDF.TYPE), Generator.generateUriString(), MatchType.IRI); + final SearchParam spOne = new SearchParam(URI.create(RDF.TYPE), Set.of(Generator.generateUriString()), MatchType.IRI); final FacetedSearchResult item = new FacetedSearchResult(); item.setUri(Generator.generateUri()); item.setLabel(MultilingualString.create("Test term", Environment.LANGUAGE)); From 097cf5697e02aa18d41accc8be37a711f829ee2c Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Fri, 9 Jun 2023 16:46:37 +0200 Subject: [PATCH 06/12] [Upd] Update to JOPA 0.22.1, fix broken tests. --- pom.xml | 2 +- .../export/SKOSVocabularyExporterTest.java | 59 +++++++++++-------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/pom.xml b/pom.xml index 433b66114..824aa5e3a 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ 2.7.0 1.5.3.Final 1.7.0 - 0.21.1 + 0.22.1 0.12.3 1.9.7 diff --git a/src/test/java/cz/cvut/kbss/termit/service/export/SKOSVocabularyExporterTest.java b/src/test/java/cz/cvut/kbss/termit/service/export/SKOSVocabularyExporterTest.java index 1770fd400..743537dce 100644 --- a/src/test/java/cz/cvut/kbss/termit/service/export/SKOSVocabularyExporterTest.java +++ b/src/test/java/cz/cvut/kbss/termit/service/export/SKOSVocabularyExporterTest.java @@ -1,6 +1,7 @@ package cz.cvut.kbss.termit.service.export; import cz.cvut.kbss.jopa.model.EntityManager; +import cz.cvut.kbss.termit.dto.TermInfo; import cz.cvut.kbss.termit.environment.Environment; import cz.cvut.kbss.termit.environment.Generator; import cz.cvut.kbss.termit.model.Asset; @@ -298,9 +299,7 @@ void exportGlossaryExportsRelatedTerms() throws Exception { final List terms = generateTerms(vocabulary); final Term withRelated = terms.get(0); final Term related = terms.get(terms.size() - 1); - withRelated.setProperties(Collections - .singletonMap(SKOS.RELATED.stringValue(), - Collections.singleton(related.getUri().toString()))); + withRelated.setRelated(Set.of(new TermInfo(related))); // This is normally inferred withRelated.setVocabulary(vocabulary.getUri()); transactional(() -> em.merge(withRelated, descriptorFactory.termDescriptor(withRelated))); @@ -359,11 +358,11 @@ void exportGlossaryExportsSuperClassesAsBroader() throws Exception { final List terms = generateTerms(vocabulary); final Term rdfsSubclass = terms.get(0); final String supertype = Generator.generateUri().toString(); - rdfsSubclass.setProperties( - Collections.singletonMap(RDFS.SUBCLASSOF.stringValue(), Collections.singleton(supertype))); - // This is normally inferred - rdfsSubclass.setVocabulary(vocabulary.getUri()); - transactional(() -> em.merge(rdfsSubclass, descriptorFactory.termDescriptor(rdfsSubclass))); + transactional(() -> { + insertPropertyAssertion(rdfsSubclass, RDFS.SUBCLASSOF.stringValue(), supertype); + // Simulate inference + Generator.addTermInVocabularyRelationship(rdfsSubclass, vocabulary.getUri(), em); + }); final TypeAwareResource result = sut.exportGlossary(vocabulary, exportConfig()); final Model model = loadAsModel(result); @@ -372,17 +371,26 @@ void exportGlossaryExportsSuperClassesAsBroader() throws Exception { vf.createIRI(supertype)))); } + private void insertPropertyAssertion(Term subject, String property, String value) { + final Repository repo = em.unwrap(Repository.class); + final ValueFactory vf = repo.getValueFactory(); + try (final RepositoryConnection conn = repo.getConnection()) { + conn.add(vf.createIRI(subject.getUri().toString()), vf.createIRI(property), vf.createIRI(value), + vf.createIRI(vocabulary.getUri().toString())); + } + } + @Test void exportGlossaryExportsPartOfAsBroader() throws Exception { final List terms = generateTerms(vocabulary); final Term partOf = terms.get(0); final Term hasPart = terms.get(1); - hasPart.setProperties( - Collections.singletonMap(cz.cvut.kbss.termit.util.Vocabulary.s_p_has_part, - Collections.singleton(partOf.getUri().toString()))); - // This is normally inferred - hasPart.setVocabulary(vocabulary.getUri()); - transactional(() -> em.merge(hasPart, descriptorFactory.termDescriptor(hasPart))); + transactional(() -> { + insertPropertyAssertion(hasPart, cz.cvut.kbss.termit.util.Vocabulary.s_p_has_part, + partOf.getUri().toString()); + // Simulate inference + Generator.addTermInVocabularyRelationship(hasPart, vocabulary.getUri(), em); + }); final TypeAwareResource result = sut.exportGlossary(vocabulary, exportConfig()); final Model model = loadAsModel(result); @@ -396,12 +404,12 @@ void exportGlossaryExportsParticipationAsBroader() throws Exception { final List terms = generateTerms(vocabulary); final Term participant = terms.get(0); final Term parent = terms.get(1); - parent.setProperties( - Collections.singletonMap(cz.cvut.kbss.termit.util.Vocabulary.s_p_has_participant, - Collections.singleton(participant.getUri().toString()))); - // This is normally inferred - parent.setVocabulary(vocabulary.getUri()); - transactional(() -> em.merge(parent, descriptorFactory.termDescriptor(parent))); + transactional(() -> { + insertPropertyAssertion(parent, cz.cvut.kbss.termit.util.Vocabulary.s_p_has_participant, + participant.getUri().toString()); + // Simulate inference + Generator.addTermInVocabularyRelationship(parent, vocabulary.getUri(), em); + }); final TypeAwareResource result = sut.exportGlossary(vocabulary, exportConfig()); final Model model = loadAsModel(result); @@ -430,12 +438,11 @@ void exportGlossarySkipsOwlConstructsUsedToClassifyTerms() throws Exception { void exportGlossarySkipsOwlConstructsUsedAsTermSuperTypes() throws Exception { final List terms = generateTerms(vocabulary); final Term withOwl = terms.get(Generator.randomIndex(terms)); - withOwl.setProperties(Collections - .singletonMap(RDFS.SUBCLASSOF.stringValue(), - Collections.singleton(OWL.OBJECTPROPERTY.stringValue()))); - // This is normally inferred - withOwl.setVocabulary(vocabulary.getUri()); - transactional(() -> em.merge(withOwl, descriptorFactory.termDescriptor(withOwl))); + transactional(() -> { + insertPropertyAssertion(withOwl, RDFS.SUBCLASSOF.stringValue(), OWL.OBJECTPROPERTY.stringValue()); + // Simulate inference + Generator.addTermInVocabularyRelationship(withOwl, vocabulary.getUri(), em); + }); final TypeAwareResource result = sut.exportGlossary(vocabulary, exportConfig()); final Model model = loadAsModel(result); From fc718a1a1df4279f46a85766b858d368b4ebf497 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 19 Jun 2023 08:37:43 +0200 Subject: [PATCH 07/12] [kbss-cvut/termit-ui#326] Extend FacetedSearchResult with draft attribute. --- .../kbss/termit/dto/search/FacetedSearchResult.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/cz/cvut/kbss/termit/dto/search/FacetedSearchResult.java b/src/main/java/cz/cvut/kbss/termit/dto/search/FacetedSearchResult.java index 431a2fb11..e71386d87 100644 --- a/src/main/java/cz/cvut/kbss/termit/dto/search/FacetedSearchResult.java +++ b/src/main/java/cz/cvut/kbss/termit/dto/search/FacetedSearchResult.java @@ -6,6 +6,7 @@ import cz.cvut.kbss.termit.model.Asset; import cz.cvut.kbss.termit.model.util.HasTypes; import cz.cvut.kbss.termit.util.Utils; +import cz.cvut.kbss.termit.util.Vocabulary; import java.net.URI; import java.util.Objects; @@ -44,6 +45,9 @@ public class FacetedSearchResult extends Asset implements Ha @OWLObjectProperty(iri = cz.cvut.kbss.termit.util.Vocabulary.s_p_je_pojmem_ze_slovniku) private URI vocabulary; + @OWLDataProperty(iri = Vocabulary.s_p_je_draft) + private Boolean draft; + @Types private Set types; @@ -113,6 +117,14 @@ public void setVocabulary(URI vocabulary) { this.vocabulary = vocabulary; } + public Boolean isDraft() { + return draft == null || draft; + } + + public void setDraft(Boolean draft) { + this.draft = draft; + } + public Set getTypes() { return types; } From 619718a38a5198892a31c8562fc9c9057a538328 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 20 Jun 2023 08:30:41 +0200 Subject: [PATCH 08/12] [Code style] Prevent star imports in .editorconfig. --- .editorconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.editorconfig b/.editorconfig index a4bec8154..1415ada02 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,3 +20,5 @@ ij_any_array_initializer_wrap = normal ij_any_align_multiline_array_initializer_expression = true ij_java_annotation_parameter_wrap = normal ij_java_align_multiline_annotation_parameters = true +ij_java_class_count_to_use_import_on_demand = 100 +ij_java_names_count_to_use_import_on_demand = 100 From 4cbe07b6413e73e7a67e8b5c485b484f74029054 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 20 Jun 2023 09:32:37 +0200 Subject: [PATCH 09/12] [kbss-cvut/termit-ui#326] Support paging in faceted search. --- .../termit/persistence/dao/SearchDao.java | 8 +++- .../kbss/termit/rest/SearchController.java | 20 ++++++++-- .../service/business/SearchService.java | 8 +++- .../termit/persistence/dao/SearchDaoTest.java | 37 ++++++++++++++----- .../termit/rest/SearchControllerTest.java | 28 +++++++++++++- .../service/business/SearchServiceTest.java | 14 ++++--- 6 files changed, 93 insertions(+), 22 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java b/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java index 8201d8738..033baa454 100644 --- a/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java +++ b/src/main/java/cz/cvut/kbss/termit/persistence/dao/SearchDao.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.Pageable; import org.springframework.lang.NonNull; import org.springframework.stereotype.Repository; @@ -129,10 +130,13 @@ protected String queryIncludingSnapshots() { * Only current versions of terms are searched. * * @param searchParams Search parameters (facets) + * @param pageSpec Specification of the page of results to return * @return List of matching terms, ordered by label */ - public List facetedTermSearch(@NonNull Collection searchParams) { + public List facetedTermSearch(@NonNull Collection searchParams, + @NonNull Pageable pageSpec) { Objects.requireNonNull(searchParams); + Objects.requireNonNull(pageSpec); LOG.trace("Running faceted term search for search parameters: {}", searchParams); final StringBuilder queryStr = new StringBuilder( "SELECT DISTINCT ?t WHERE { ?t a ?term ; ?hasLabel ?label .\n"); @@ -162,6 +166,8 @@ public List facetedTermSearch(@NonNull Collection fullTextSearchTerms( @PreAuthorize("permitAll()") @PostMapping(value = "/faceted/terms", produces = {MediaType.APPLICATION_JSON_VALUE, JsonLd.MEDIA_TYPE}, consumes = {MediaType.APPLICATION_JSON_VALUE}) - public List facetedTermSearch(@Parameter(description = "Search parameters.") + public List facetedTermSearch(@Parameter(description = ApiDocConstants.PAGE_SIZE_DESCRIPTION) + @RequestParam(name = Constants.QueryParams.PAGE_SIZE, + required = false) Integer pageSize, + @Parameter(description = ApiDocConstants.PAGE_NO_DESCRIPTION) + @RequestParam(name = Constants.QueryParams.PAGE, + required = false) Integer pageNo, + @Parameter(description = "Search parameters.") @RequestBody Collection searchParams) { - return searchService.facetedTermSearch(searchParams); + return searchService.facetedTermSearch(searchParams, RestUtils.createPageRequest(pageSize, pageNo)); } } diff --git a/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java b/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java index 2c2d85a7f..47f36412d 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java +++ b/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java @@ -20,6 +20,7 @@ import cz.cvut.kbss.termit.dto.search.SearchParam; import cz.cvut.kbss.termit.persistence.dao.SearchDao; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; import org.springframework.lang.NonNull; import org.springframework.security.access.prepost.PostFilter; import org.springframework.stereotype.Service; @@ -76,12 +77,15 @@ public List fullTextSearchOfTerms(String searchString, Set * The search treats the parameters as conjunction, so the result has to match all the search parameters. * * @param searchParams Search parameters + * @param pageSpec Page specifying result number and position * @return List of matching terms, sorted by label */ @PostFilter("@searchAuthorizationService.canRead(filterObject)") - public List facetedTermSearch(@NonNull Collection searchParams) { + public List facetedTermSearch(@NonNull Collection searchParams, + @NonNull Pageable pageSpec) { Objects.requireNonNull(searchParams); + Objects.requireNonNull(pageSpec); searchParams.forEach(SearchParam::validate); - return searchDao.facetedTermSearch(searchParams); + return searchDao.facetedTermSearch(searchParams, pageSpec); } } diff --git a/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java b/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java index 7975a6769..0af311ca2 100644 --- a/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java +++ b/src/test/java/cz/cvut/kbss/termit/persistence/dao/SearchDaoTest.java @@ -36,6 +36,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import java.net.URI; import java.util.*; @@ -46,6 +48,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.*; /** @@ -70,18 +73,17 @@ class SearchDaoTest extends BaseDaoTestRunner { private static boolean initialized = false; - private User user; - - private Vocabulary vocabulary; + private static User user; + private static Vocabulary vocabulary; private static List terms; private static List vocabularies; @BeforeEach void setUp() { - this.user = Generator.generateUserWithId(); - this.vocabulary = Generator.generateVocabularyWithId(); if (!initialized) { + user = Generator.generateUserWithId(); + vocabulary = Generator.generateVocabularyWithId(); transactional(() -> { em.persist(user); em.persist(vocabulary, descriptorFactory.vocabularyDescriptor(vocabulary)); @@ -124,6 +126,7 @@ private List generateTerms() { Environment.LANGUAGE))); terms.add(term); } + terms.sort(Comparator.comparing(Term::getPrimaryLabel)); return terms; } @@ -224,7 +227,7 @@ private void insertSnapshotType(HasIdentifier asset) { void facetedTermSearchReturnsTermsMatchingIriSearchParamWithSpecifiedTypes() { final SearchParam param = new SearchParam(URI.create(RDF.TYPE.stringValue()), Set.of(TYPES[0], TYPES[1]), MatchType.IRI); - final List result = sut.facetedTermSearch(Set.of(param)); + final List result = sut.facetedTermSearch(Set.of(param), Constants.DEFAULT_PAGE_SPEC); assertFalse(result.isEmpty()); result.forEach(r -> assertThat(r.getTypes(), anyOf(hasItem(TYPES[0]), hasItem(TYPES[1])))); } @@ -237,7 +240,7 @@ void facetedTermSearchReturnsTermsMatchingExactMatchSearchParamWithSpecifiedValu final List matchingTerms = terms.stream() .filter(t -> !Collections.disjoint(t.getNotations(), param.getValue())) .collect(Collectors.toList()); - final List result = sut.facetedTermSearch(Set.of(param)); + final List result = sut.facetedTermSearch(Set.of(param), Constants.DEFAULT_PAGE_SPEC); assertFalse(result.isEmpty()); assertThat(result, containsSameEntities(matchingTerms)); } @@ -250,7 +253,7 @@ void facetedTermSearchReturnsTermsMatchingSubstringSearchParamWithSpecifiedValue final List matchingTerms = terms.stream().filter(t -> t.getExamples().iterator().next().get() .startsWith("Matching")) .collect(Collectors.toList()); - final List result = sut.facetedTermSearch(Set.of(param)); + final List result = sut.facetedTermSearch(Set.of(param), Constants.DEFAULT_PAGE_SPEC); assertFalse(result.isEmpty()); assertThat(result, containsSameEntities(matchingTerms)); } @@ -266,7 +269,23 @@ void facetedTermSearchReturnsResultsMatchingMultipleSearchParameters() { .startsWith("Matching") && (t.hasType( TYPES[0]) || t.hasType(TYPES[1]))) .collect(Collectors.toList()); - final List result = sut.facetedTermSearch(Set.of(typeParam, substringParam)); + final List result = sut.facetedTermSearch(Set.of(typeParam, substringParam), + Constants.DEFAULT_PAGE_SPEC); assertThat(result, containsSameEntities(matchingTerms)); } + + @Test + void facetedTermSearchReturnsMatchingPage() { + final Pageable pageOne = PageRequest.of(0, terms.size() / 2); + final Pageable pageTwo = PageRequest.of(1, terms.size() / 2); + final SearchParam searchParam = new SearchParam( + URI.create(cz.cvut.kbss.termit.util.Vocabulary.s_p_je_pojmem_ze_slovniku), + Set.of(vocabulary.getUri().toString()), MatchType.IRI); + + final List resultOne = sut.facetedTermSearch(Set.of(searchParam), pageOne); + assertEquals(terms.size() / 2, resultOne.size()); + final List resultTwo = sut.facetedTermSearch(Set.of(searchParam), pageTwo); + assertEquals(terms.size() / 2, resultTwo.size()); + assertThat(resultOne, not(containsSameEntities(resultTwo))); + } } diff --git a/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java b/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java index 7f18f871b..a46b90097 100644 --- a/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java +++ b/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java @@ -25,6 +25,7 @@ import cz.cvut.kbss.termit.environment.Environment; import cz.cvut.kbss.termit.environment.Generator; import cz.cvut.kbss.termit.service.business.SearchService; +import cz.cvut.kbss.termit.util.Constants; import cz.cvut.kbss.termit.util.Vocabulary; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,6 +33,8 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; @@ -73,6 +76,7 @@ void fullTextSearchExecutesSearchOnService() throws Exception { "test", "test", 1.0)); when(searchServiceMock.fullTextSearch(any())).thenReturn(expected); final String searchString = "test"; + final MvcResult mvcResult = mockMvc.perform(get(PATH + "/fts").param("searchString", searchString)) .andExpect(status().isOk()).andReturn(); final List result = readValue(mvcResult, new TypeReference>() { @@ -92,6 +96,7 @@ void fullTextSearchOfTermsWithoutVocabularySpecificationExecutesSearchOnService( Vocabulary.s_c_term, "test", "test", 1.0)); when(searchServiceMock.fullTextSearchOfTerms(any(), any())).thenReturn(expected); final String searchString = "test"; + mockMvc.perform(get(PATH + "/fts/terms") .param("searchString", searchString) .param("vocabulary", vocabularyIri.toString())) @@ -104,16 +109,35 @@ void facetedTermSearchPassesSearchParametersToSearchService() throws Exception { final FacetedSearchResult term = new FacetedSearchResult(); term.setUri(Generator.generateUri()); term.setLabel(MultilingualString.create("Test term", Environment.LANGUAGE)); - when(searchServiceMock.facetedTermSearch(anyCollection())).thenReturn(List.of(term)); + when(searchServiceMock.facetedTermSearch(anyCollection(), any(Pageable.class))).thenReturn(List.of(term)); final List searchParams = List.of( new SearchParam(URI.create(SKOS.NOTATION), Set.of("LA_"), MatchType.EXACT_MATCH), new SearchParam(URI.create(RDF.TYPE), Set.of(Generator.generateUri().toString()), MatchType.IRI)); + final MvcResult mvcResult = mockMvc.perform( post(PATH + "/faceted/terms").content(toJson(searchParams)).contentType( MediaType.APPLICATION_JSON)).andReturn(); final List result = readValue(mvcResult, new TypeReference>() { }); assertEquals(List.of(term), result); - verify(searchServiceMock).facetedTermSearch(searchParams); + verify(searchServiceMock).facetedTermSearch(searchParams, Constants.DEFAULT_PAGE_SPEC); + } + + @Test + void facetedSearchPassesSpecifiedPageSpecificationToService() throws Exception { + final FacetedSearchResult term = new FacetedSearchResult(); + term.setUri(Generator.generateUri()); + term.setLabel(MultilingualString.create("Test term", Environment.LANGUAGE)); + when(searchServiceMock.facetedTermSearch(anyCollection(), any(Pageable.class))).thenReturn(List.of(term)); + final List searchParams = List.of( + new SearchParam(URI.create(SKOS.NOTATION), Set.of("LA_"), MatchType.EXACT_MATCH)); + final int pageNo = Generator.randomInt(0, 5); + final int pageSize = Generator.randomInt(100, 1000); + + mockMvc.perform( + post(PATH + "/faceted/terms").content(toJson(searchParams)).contentType( + MediaType.APPLICATION_JSON).param(Constants.QueryParams.PAGE, Integer.toString(pageNo)).param( + Constants.QueryParams.PAGE_SIZE, Integer.toString(pageSize))).andExpect(status().isOk()); + verify(searchServiceMock).facetedTermSearch(searchParams, PageRequest.of(pageNo, pageSize)); } } diff --git a/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java b/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java index 3e7785ca8..c98285669 100644 --- a/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java +++ b/src/test/java/cz/cvut/kbss/termit/service/business/SearchServiceTest.java @@ -11,11 +11,14 @@ import cz.cvut.kbss.termit.environment.Generator; import cz.cvut.kbss.termit.exception.ValidationException; import cz.cvut.kbss.termit.persistence.dao.SearchDao; +import cz.cvut.kbss.termit.util.Constants; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import java.net.URI; import java.util.Collections; @@ -80,8 +83,8 @@ void facetedTermSearchValidatesEachSearchParamBeforeInvokingSearch() { final SearchParam spOne = new SearchParam(URI.create(RDF.TYPE), Set.of(Generator.generateUriString()), MatchType.IRI); final SearchParam spTwo = new SearchParam(URI.create(SKOS.NOTATION), Set.of("will be removed"), MatchType.SUBSTRING); spTwo.setValue(null); - assertThrows(ValidationException.class, () -> sut.facetedTermSearch(List.of(spOne, spTwo))); - verify(searchDao, never()).facetedTermSearch(anyCollection()); + assertThrows(ValidationException.class, () -> sut.facetedTermSearch(List.of(spOne, spTwo), Constants.DEFAULT_PAGE_SPEC)); + verify(searchDao, never()).facetedTermSearch(anyCollection(), any()); } @Test @@ -92,10 +95,11 @@ void facetedTermSearchExecutesSearchOnDaoAndReturnsResults() { item.setLabel(MultilingualString.create("Test term", Environment.LANGUAGE)); item.setVocabulary(Generator.generateUri()); item.setTypes(new HashSet<>(spOne.getValue())); - when(searchDao.facetedTermSearch(anyCollection())).thenReturn(List.of(item)); + when(searchDao.facetedTermSearch(anyCollection(), any(Pageable.class))).thenReturn(List.of(item)); + final Pageable pageSpec = PageRequest.of(2, 100); - final List result = sut.facetedTermSearch(Set.of(spOne)); + final List result = sut.facetedTermSearch(Set.of(spOne), pageSpec); assertEquals(List.of(item), result); - verify(searchDao).facetedTermSearch(Set.of(spOne)); + verify(searchDao).facetedTermSearch(Set.of(spOne), pageSpec); } } From 00b8736a84112f98f719b4369987e264101571ff Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 20 Jun 2023 13:36:22 +0200 Subject: [PATCH 10/12] [kbss-cvut/termit-ui#326] Handle empty faceted search params. --- .../java/cz/cvut/kbss/termit/rest/SearchController.java | 4 ++++ .../cz/cvut/kbss/termit/rest/SearchControllerTest.java | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java b/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java index d82226d4c..7bbc314e5 100644 --- a/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java +++ b/src/main/java/cz/cvut/kbss/termit/rest/SearchController.java @@ -18,6 +18,7 @@ import cz.cvut.kbss.termit.dto.search.FacetedSearchResult; import cz.cvut.kbss.termit.dto.search.FullTextSearchResult; import cz.cvut.kbss.termit.dto.search.SearchParam; +import cz.cvut.kbss.termit.exception.UnsupportedSearchFacetException; import cz.cvut.kbss.termit.rest.doc.ApiDocConstants; import cz.cvut.kbss.termit.rest.util.RestUtils; import cz.cvut.kbss.termit.service.IdentifierResolver; @@ -91,6 +92,9 @@ public List facetedTermSearch(@Parameter(description = ApiD required = false) Integer pageNo, @Parameter(description = "Search parameters.") @RequestBody Collection searchParams) { + if (searchParams.isEmpty()) { + throw new UnsupportedSearchFacetException("Search params must be provided for faceted search."); + } return searchService.facetedTermSearch(searchParams, RestUtils.createPageRequest(pageSize, pageNo)); } } diff --git a/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java b/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java index a46b90097..847964b88 100644 --- a/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java +++ b/src/test/java/cz/cvut/kbss/termit/rest/SearchControllerTest.java @@ -46,6 +46,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -140,4 +141,11 @@ void facetedSearchPassesSpecifiedPageSpecificationToService() throws Exception { Constants.QueryParams.PAGE_SIZE, Integer.toString(pageSize))).andExpect(status().isOk()); verify(searchServiceMock).facetedTermSearch(searchParams, PageRequest.of(pageNo, pageSize)); } + + @Test + void facetedSearchThrowsBadRequestWhenNoSearchParamsAreProvided() throws Exception { + mockMvc.perform(post(PATH + "/faceted/terms").content("[]").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + verify(searchServiceMock, never()).facetedTermSearch(anyCollection(), any()); + } } From adfd3969ff22195ef7dc8195ce52fb9e5252f401 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Wed, 21 Jun 2023 13:07:16 +0200 Subject: [PATCH 11/12] [Doc] Add explanation of why HATEAOS paging is not supported by faceted search. --- .../cz/cvut/kbss/termit/service/business/SearchService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java b/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java index 47f36412d..dfc6e4985 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java +++ b/src/main/java/cz/cvut/kbss/termit/service/business/SearchService.java @@ -75,6 +75,9 @@ public List fullTextSearchOfTerms(String searchString, Set *

* The search parameters define facets by which terms should be searched together with corresponding search values. * The search treats the parameters as conjunction, so the result has to match all the search parameters. + *

+ * Note: cannot use full paging support, as it is not supported by Spring Security - + * https://github.com/spring-projects/spring-security/issues/3410 * * @param searchParams Search parameters * @param pageSpec Page specifying result number and position From 9c125e6e802a6735e3e2263addfdf48a3ade8f28 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Thu, 29 Jun 2023 13:52:42 +0200 Subject: [PATCH 12/12] [2.18.0] Bump version, update readme. --- README.md | 25 +++++++++++++------------ pom.xml | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index df0e92821..a94172b1e 100644 --- a/README.md +++ b/README.md @@ -32,16 +32,14 @@ This section briefly lists the main technologies and principles used (or planned - Spring Boot 2, Spring Framework 5, Spring Security, Spring Data (paging, filtering) - Jackson 2.13 -- [JB4JSON-LD](https://github.com/kbss-cvut/jb4jsonld-jackson)* - Java - JSON-LD (de)serialization library +- [JB4JSON-LD](https://github.com/kbss-cvut/jb4jsonld-jackson) - Java - JSON-LD (de)serialization library - [JOPA](https://github.com/kbss-cvut/jopa) - persistence library for the Semantic Web -- JUnit 5* (RT used 4), Mockito 4* (RT used 1), Hamcrest 2* (RT used 1) -- Servlet API 4* (RT used 3.0.1) -- JSON Web Tokens* (CSRF protection not necessary for JWT) +- JUnit 5 (RT used 4), Mockito 4 (RT used 1), Hamcrest 2 (RT used 1) +- Servlet API 4 (RT used 3.0.1) +- JSON Web Tokens (CSRF protection not necessary for JWT) - SLF4J + Logback -- CORS* (for separate frontend) -- Java bean validation (JSR 380)* - -_* Technology not used in [INBAS RT](https://github.com/kbss-cvut/reporting-tool)_ +- CORS (for separate frontend) +- Java bean validation (JSR 380) ## Ontology @@ -63,14 +61,15 @@ the [JavaMelody Spring Boot Starter docs](https://github.com/javamelody/javamelo ## Documentation -TermIt REST API is tentatively documented on [SwaggerHub](https://app.swaggerhub.com/apis/ledvima1/TermIt/) under the -appropriate version. +TermIt REST API is available for each instance via [Swagger UI](https://swagger.io/tools/swagger-ui/). It is accessible +at `http://SERVER_URL/PATH/swagger-ui/index.html`, where `SERVER_URL` is the URL of the server at which TermIt backend +is running and `PATH` is the context path. A link to the API documentation is also available in the footer of the TermIt UI. Build configuration and deployment is described in [setup.md](doc/setup.md). -## Dockerization +## Docker -The docker image of TermIt backend can be built by +The Docker image of TermIt backend alone can be built by `docker build -t termit-server .` Then, TermIt can be run and exposed at the port 8080 as @@ -78,6 +77,8 @@ Then, TermIt can be run and exposed at the port 8080 as An optional argument is `` pointing to the RDF4J/GraphDB repository. +TermIt Docker images are also build and published to [DockerHub](https://hub.docker.com/r/kbsscvut/termit). + ## Links - [TermIt UI](https://github.com/kbss-cvut/termit-ui) - repository with TermIt frontend source code diff --git a/pom.xml b/pom.xml index 0d6f9384b..7c8073d77 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ termit - 2.17.0 + 2.18.0 TermIt Terminology manager based on Semantic Web technologies. ${packaging}