From 29adbdb0d05507d3255475340ef15dfc5ef3c977 Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 3 Oct 2024 15:40:20 +0200 Subject: [PATCH 01/18] FAIRSPC-81: migrated views endpoint to spring mvc --- projects/saturn/build.gradle | 8 +- .../saturn/config/SparkFilterFactory.java | 3 +- .../saturn/controller/ViewController.java | 58 ++++++ .../saturn/controller/dto/ColumnDto.java | 5 + .../saturn/controller/dto/CountDto.java | 3 + .../saturn/controller/dto/FacetDto.java | 19 ++ .../saturn/controller/dto/FacetsDto.java | 5 + .../saturn/controller/dto/ValueDto.java | 8 + .../saturn/controller/dto/ViewDto.java | 11 ++ .../dto/ViewPageDto.java} | 6 +- .../saturn/controller/dto/ViewsDto.java | 5 + .../dto/request}/CountRequest.java | 10 +- .../dto/request}/ViewRequest.java | 10 +- .../saturn/services/views/ColumnDTO.java | 13 -- .../saturn/services/views/CountDTO.java | 9 - .../saturn/services/views/FacetDTO.java | 22 --- .../saturn/services/views/FacetsDTO.java | 10 -- .../services/views/JdbcQueryService.java | 19 +- .../saturn/services/views/QueryService.java | 9 +- .../services/views/SparqlQueryService.java | 29 +-- .../saturn/services/views/ValueDTO.java | 14 -- .../saturn/services/views/ViewApp.java | 48 ----- .../saturn/services/views/ViewDTO.java | 16 -- .../saturn/services/views/ViewRow.java | 16 +- .../saturn/services/views/ViewService.java | 44 ++--- .../services/views/ViewStoreReader.java | 27 +-- .../saturn/services/views/ViewsDTO.java | 10 -- .../src/main/resources/application.yaml | 1 + .../saturn/controller/ViewControllerTest.java | 167 ++++++++++++++++++ .../services/views/JdbcQueryServiceTest.java | 41 +++-- .../views/SparqlQueryServiceTest.java | 33 ++-- .../services/views/ViewServiceTest.java | 16 +- 32 files changed, 425 insertions(+), 270 deletions(-) create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ColumnDto.java create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/CountDto.java create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetDto.java create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetsDto.java create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ValueDto.java create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewDto.java rename projects/saturn/src/main/java/io/fairspace/saturn/{services/views/ViewPageDTO.java => controller/dto/ViewPageDto.java} (70%) create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewsDto.java rename projects/saturn/src/main/java/io/fairspace/saturn/{services/views => controller/dto/request}/CountRequest.java (57%) rename projects/saturn/src/main/java/io/fairspace/saturn/{services/views => controller/dto/request}/ViewRequest.java (75%) delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/views/ColumnDTO.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountDTO.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetDTO.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetsDTO.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/views/ValueDTO.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewApp.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewDTO.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewsDTO.java create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java diff --git a/projects/saturn/build.gradle b/projects/saturn/build.gradle index 30211520da..39ed8c3032 100644 --- a/projects/saturn/build.gradle +++ b/projects/saturn/build.gradle @@ -75,16 +75,13 @@ dependencies { testImplementation "junit:junit:4.13.2" testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.junit.vintage:junit-vintage-engine:5.10.2' testImplementation "org.testcontainers:postgresql:1.19.6" testImplementation('com.github.stefanbirkner:system-rules:1.19.0') { exclude group: 'junit', module:'junit-dep' } - constraints { -// implementation('com.fasterxml.jackson.core:jackson-databind:2.9.10.1') { -// because 'previous versions have security vulnerabilities' -// } - } } jacocoTestReport { @@ -117,4 +114,5 @@ test { // They also blocks usage of testing library mocking environment variables, // That is why this additional arg for tests is needed jvmArgs = ['--add-opens', 'java.base/java.util=ALL-UNNAMED'] + useJUnitPlatform() } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java index 8930e455b4..b43d4b0cfe 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java @@ -9,7 +9,6 @@ import io.fairspace.saturn.services.search.SearchApp; import io.fairspace.saturn.services.users.LogoutApp; import io.fairspace.saturn.services.users.UserApp; -import io.fairspace.saturn.services.views.ViewApp; import io.fairspace.saturn.services.workspaces.WorkspaceApp; public class SparkFilterFactory { @@ -22,7 +21,7 @@ public static SaturnSparkFilter createSparkFilter( return new SaturnSparkFilter( new WorkspaceApp(apiPathPrefix + "/workspaces", svc.getWorkspaceService()), new MetadataApp(apiPathPrefix + "/metadata", svc.getMetadataService()), - new ViewApp(apiPathPrefix + "/views", svc.getViewService(), svc.getQueryService()), + // new ViewApp(apiPathPrefix + "/views", svc.getViewService(), svc.getQueryService()), new SearchApp(apiPathPrefix + "/search", svc.getSearchService(), svc.getFileSearchService()), new VocabularyApp(apiPathPrefix + "/vocabulary"), new UserApp(apiPathPrefix + "/users", svc.getUserService()), diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java new file mode 100644 index 0000000000..be70285fc6 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java @@ -0,0 +1,58 @@ +package io.fairspace.saturn.controller; + +import jakarta.validation.Valid; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.config.Services; +import io.fairspace.saturn.controller.dto.CountDto; +import io.fairspace.saturn.controller.dto.FacetsDto; +import io.fairspace.saturn.controller.dto.ViewPageDto; +import io.fairspace.saturn.controller.dto.ViewsDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; + +@RestController +@RequestMapping("${application.basePath}/views") +@Validated +public class ViewController { + + private final Services services; + + public ViewController(Services services) { + this.services = services; + } + + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + public ViewsDto getViews() { + var views = services.getViewService().getViews(); + return new ViewsDto(views); + } + + @PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createView(@Valid @RequestBody ViewRequest requestBody) { + var result = services.getQueryService().retrieveViewPage(requestBody); + return ResponseEntity.ok(result); + } + + @GetMapping(value = "/facets", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getFacets() { + var facets = services.getViewService().getFacets(); + return ResponseEntity.ok(new FacetsDto(facets)); + } + + @PostMapping( + value = "/count", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity count(@Valid @RequestBody CountRequest requestBody) { + var result = services.getQueryService().count(requestBody); + return ResponseEntity.ok(result); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ColumnDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ColumnDto.java new file mode 100644 index 0000000000..9b9a981115 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ColumnDto.java @@ -0,0 +1,5 @@ +package io.fairspace.saturn.controller.dto; + +import io.fairspace.saturn.config.ViewsConfig; + +public record ColumnDto(String name, String title, ViewsConfig.ColumnType type, Integer displayIndex) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/CountDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/CountDto.java new file mode 100644 index 0000000000..6056f40290 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/CountDto.java @@ -0,0 +1,3 @@ +package io.fairspace.saturn.controller.dto; + +public record CountDto(long count, boolean timeout) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetDto.java new file mode 100644 index 0000000000..469f9d533b --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetDto.java @@ -0,0 +1,19 @@ +package io.fairspace.saturn.controller.dto; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import io.fairspace.saturn.config.ViewsConfig; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +@JsonInclude(NON_NULL) +public record FacetDto( + String name, + String title, + ViewsConfig.ColumnType type, + List values, + Boolean booleanValue, + Object min, + Object max) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetsDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetsDto.java new file mode 100644 index 0000000000..93c1797406 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetsDto.java @@ -0,0 +1,5 @@ +package io.fairspace.saturn.controller.dto; + +import java.util.List; + +public record FacetsDto(List facets) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ValueDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ValueDto.java new file mode 100644 index 0000000000..209f450245 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ValueDto.java @@ -0,0 +1,8 @@ +package io.fairspace.saturn.controller.dto; + +public record ValueDto(String label, Object value) implements Comparable { + @Override + public int compareTo(ValueDto o) { + return label.compareTo(o.label); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewDto.java new file mode 100644 index 0000000000..23a23843e9 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewDto.java @@ -0,0 +1,11 @@ +package io.fairspace.saturn.controller.dto; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +public record ViewDto( + String name, + String title, + List columns, + @JsonInclude(JsonInclude.Include.NON_NULL) Long maxDisplayCount) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewPageDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewPageDto.java similarity index 70% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewPageDTO.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewPageDto.java index 1d9eb16d91..3e89f0a43d 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewPageDTO.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewPageDto.java @@ -1,4 +1,4 @@ -package io.fairspace.saturn.services.views; +package io.fairspace.saturn.controller.dto; import java.util.List; import java.util.Map; @@ -8,12 +8,12 @@ @Value @Builder -public class ViewPageDTO { +public class ViewPageDto { /** * The key of every row is `${view}_${column}`. */ @NonNull - List>> rows; + List>> rows; boolean hasNext; boolean timeout; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewsDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewsDto.java new file mode 100644 index 0000000000..7ceb437c4e --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewsDto.java @@ -0,0 +1,5 @@ +package io.fairspace.saturn.controller.dto; + +import java.util.List; + +public record ViewsDto(List views) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountRequest.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/CountRequest.java similarity index 57% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountRequest.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/CountRequest.java index 6915209678..d2a443ec38 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountRequest.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/CountRequest.java @@ -1,13 +1,13 @@ -package io.fairspace.saturn.services.views; +package io.fairspace.saturn.controller.dto.request; import java.util.List; import jakarta.validation.constraints.NotBlank; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; -@Getter -@Setter +import io.fairspace.saturn.services.views.ViewFilter; + +@Data public class CountRequest { @NotBlank private String view; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRequest.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/ViewRequest.java similarity index 75% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRequest.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/ViewRequest.java index d3801c2ee7..6ea4eb716f 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRequest.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/ViewRequest.java @@ -1,11 +1,11 @@ -package io.fairspace.saturn.services.views; +package io.fairspace.saturn.controller.dto.request; import jakarta.validation.constraints.Min; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +import lombok.EqualsAndHashCode; -@Getter -@Setter +@EqualsAndHashCode(callSuper = true) +@Data public class ViewRequest extends CountRequest { @Min(1) private Integer page; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ColumnDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ColumnDTO.java deleted file mode 100644 index f79f1efd4d..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ColumnDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.fairspace.saturn.services.views; - -import lombok.Value; - -import io.fairspace.saturn.config.*; - -@Value -public class ColumnDTO { - String name; - String title; - ViewsConfig.ColumnType type; - Integer displayIndex; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountDTO.java deleted file mode 100644 index 89fac90c6f..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountDTO.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.fairspace.saturn.services.views; - -import lombok.Data; - -@Data -public class CountDTO { - private final long count; - private final boolean timeout; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetDTO.java deleted file mode 100644 index 80afb37f68..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.fairspace.saturn.services.views; - -import java.util.List; - -import com.fasterxml.jackson.annotation.*; -import lombok.Value; - -import io.fairspace.saturn.config.*; - -import static com.fasterxml.jackson.annotation.JsonInclude.Include.*; - -@Value -@JsonInclude(NON_NULL) -public class FacetDTO { - String name; - String title; - ViewsConfig.ColumnType type; - List values; - Boolean booleanValue; - Object min; - Object max; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetsDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetsDTO.java deleted file mode 100644 index 358ba6eb9a..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetsDTO.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.fairspace.saturn.services.views; - -import java.util.List; - -import lombok.Value; - -@Value -public class FacetsDTO { - List facets; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/JdbcQueryService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/JdbcQueryService.java index ddf37b1aae..8ebca24d22 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/JdbcQueryService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/JdbcQueryService.java @@ -15,6 +15,11 @@ import io.fairspace.saturn.config.ViewsConfig; import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.controller.dto.CountDto; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.ViewPageDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; import io.fairspace.saturn.rdf.transactions.Transactions; import io.fairspace.saturn.rdf.transactions.TxnIndexDatasetGraph; @@ -83,7 +88,7 @@ protected void applyCollectionsFilterIfRequired(String view, List fi } @SneakyThrows - public ViewPageDTO retrieveViewPage(ViewRequest request) { + public ViewPageDto retrieveViewPage(ViewRequest request) { int page = (request.getPage() != null && request.getPage() >= 1) ? request.getPage() : 1; int size = (request.getSize() != null && request.getSize() >= 1) ? request.getSize() : 20; var filters = new ArrayList(); @@ -92,9 +97,9 @@ public ViewPageDTO retrieveViewPage(ViewRequest request) { } applyCollectionsFilterIfRequired(request.getView(), filters); try (var viewStoreReader = getViewStoreReader()) { - List>> rows = viewStoreReader.retrieveRows( + List>> rows = viewStoreReader.retrieveRows( request.getView(), filters, (page - 1) * size, size + 1, request.includeJoinedViews()); - var pageBuilder = ViewPageDTO.builder() + var pageBuilder = ViewPageDto.builder() .rows(rows.subList(0, min(size, rows.size()))) .hasNext(rows.size() > size); if (request.includeCounts()) { @@ -103,7 +108,7 @@ public ViewPageDTO retrieveViewPage(ViewRequest request) { } return pageBuilder.build(); } catch (SQLTimeoutException e) { - return ViewPageDTO.builder() + return ViewPageDto.builder() .rows(Collections.emptyList()) .timeout(true) .build(); @@ -111,16 +116,16 @@ public ViewPageDTO retrieveViewPage(ViewRequest request) { } @SneakyThrows - public CountDTO count(CountRequest request) { + public CountDto count(CountRequest request) { var filters = request.getFilters(); if (filters == null) { filters = new ArrayList<>(); } applyCollectionsFilterIfRequired(request.getView(), filters); try (var viewStoreReader = getViewStoreReader()) { - return new CountDTO(viewStoreReader.countRows(request.getView(), filters), false); + return new CountDto(viewStoreReader.countRows(request.getView(), filters), false); } catch (SQLTimeoutException e) { - return new CountDTO(0, true); + return new CountDto(0, true); } } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/QueryService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/QueryService.java index eb49521400..a1f3294d72 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/QueryService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/QueryService.java @@ -1,5 +1,10 @@ package io.fairspace.saturn.services.views; +import io.fairspace.saturn.controller.dto.CountDto; +import io.fairspace.saturn.controller.dto.ViewPageDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; + /** * High-level interface for fetching metadata view pages and counts. * Implemented using Sparql queries on the RDF database directly @@ -14,7 +19,7 @@ * collections the user has access to. */ public interface QueryService { - ViewPageDTO retrieveViewPage(ViewRequest request); + ViewPageDto retrieveViewPage(ViewRequest request); - CountDTO count(CountRequest request); + CountDto count(CountRequest request); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/SparqlQueryService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/SparqlQueryService.java index 67bec54e38..3fedfde88f 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/SparqlQueryService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/SparqlQueryService.java @@ -42,6 +42,11 @@ import io.fairspace.saturn.config.ViewsConfig.ColumnType; import io.fairspace.saturn.config.ViewsConfig.View; import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.controller.dto.CountDto; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.ViewPageDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; import io.fairspace.saturn.rdf.transactions.Transactions; import io.fairspace.saturn.vocabulary.FS; @@ -97,7 +102,7 @@ public String executeQuery(String sparqlQuery) { }); } - public ViewPageDTO retrieveViewPage(ViewRequest request) { + public ViewPageDto retrieveViewPage(ViewRequest request) { var query = getQuery(request, false); log.debug("Executing query:\n{}", query); @@ -133,7 +138,7 @@ public ViewPageDTO retrieveViewPage(ViewRequest request) { .map(resource -> fetch(resource, request.getView())) .collect(toList()); - return ViewPageDTO.builder() + return ViewPageDto.builder() .rows(rows) .hasNext(hasNext) .timeout(timeout) @@ -142,10 +147,10 @@ public ViewPageDTO retrieveViewPage(ViewRequest request) { } } - private Map> fetch(Resource resource, String viewName) { + private Map> fetch(Resource resource, String viewName) { var view = getView(viewName); - var result = new HashMap>(); + var result = new HashMap>(); result.put(view.name, Set.of(toValueDTO(resource))); for (var c : view.columns) { @@ -183,7 +188,7 @@ private Map> fetch(Resource resource, String viewName) { return result; } - private Set getValues(Resource resource, View.Column column) { + private Set getValues(Resource resource, View.Column column) { return new TreeSet<>(resource.listProperties(createProperty(column.source)) .mapWith(Statement::getObject) .mapWith(this::toValueDTO) @@ -196,13 +201,13 @@ private View getView(String viewName) { .orElseThrow(() -> new IllegalArgumentException("Unknown view: " + viewName)); } - private ValueDTO toValueDTO(RDFNode node) { + private ValueDto toValueDTO(RDFNode node) { if (node.isLiteral()) { var value = node.asLiteral().getValue(); if (value instanceof XSDDateTime) { value = ofEpochMilli(((XSDDateTime) value).asCalendar().getTimeInMillis()); } - return new ValueDTO(value.toString(), value); + return new ValueDto(value.toString(), value); } var resource = node.asResource(); var label = resource.listProperties(RDFS.label) @@ -210,7 +215,7 @@ private ValueDTO toValueDTO(RDFNode node) { .map(Statement::getString) .orElseGet(resource::getLocalName); - return new ValueDTO(label, resource.getURI()); + return new ValueDto(label, resource.getURI()); } private Query getQuery(CountRequest request, boolean isCount) { @@ -424,7 +429,7 @@ private static boolean convertBooleanValue(String value) { return Boolean.getBoolean(value); } - public CountDTO count(CountRequest request) { + public CountDto count(CountRequest request) { var query = getQuery(request, true); log.debug("Querying the total number of matches: \n{}", query); @@ -440,13 +445,13 @@ public CountDTO count(CountRequest request) { if (queryResult.hasNext()) { var row = queryResult.next(); var count = row.getLiteral("count").getLong(); - return new CountDTO(count, false); + return new CountDto(count, false); } else { - return new CountDTO(0, false); + return new CountDto(0, false); } }); } catch (QueryCancelledException e) { - return new CountDTO(0, true); + return new CountDto(0, true); } } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ValueDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ValueDTO.java deleted file mode 100644 index c355f2a6ad..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ValueDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.fairspace.saturn.services.views; - -import lombok.Value; - -@Value -public class ValueDTO implements Comparable { - String label; - Object value; - - @Override - public int compareTo(ValueDTO o) { - return label.compareTo(o.label); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewApp.java deleted file mode 100644 index 4c9a564cd4..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewApp.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.fairspace.saturn.services.views; - -import lombok.extern.slf4j.Slf4j; - -import io.fairspace.saturn.services.BaseApp; - -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.get; -import static spark.Spark.post; - -@Slf4j -public class ViewApp extends BaseApp { - - private final ViewService viewService; - private final QueryService queryService; - - public ViewApp(String basePath, ViewService viewService, QueryService queryService) { - super(basePath); - this.viewService = viewService; - this.queryService = queryService; - } - - @Override - protected void initApp() { - get("/", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(new ViewsDTO(viewService.getViews())); - }); - - post("/", (req, res) -> { - var requestBody = mapper.readValue(req.body(), ViewRequest.class); - var result = queryService.retrieveViewPage(requestBody); - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(result); - }); - - get("/facets", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(new FacetsDTO(viewService.getFacets())); - }); - - post("/count", (req, res) -> { - var result = queryService.count(mapper.readValue(req.body(), CountRequest.class)); - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(result); - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewDTO.java deleted file mode 100644 index 6ba67f863e..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewDTO.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.fairspace.saturn.services.views; - -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Value; - -@Value -public class ViewDTO { - String name; - String title; - List columns; - - @JsonInclude(JsonInclude.Include.NON_NULL) - Long maxDisplayCount; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRow.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRow.java index a5502031d0..791ec3e0c6 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRow.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRow.java @@ -9,25 +9,27 @@ import com.google.common.collect.Sets; +import io.fairspace.saturn.controller.dto.ValueDto; + public class ViewRow { - private final Map> data; + private final Map> data; public ViewRow() { this.data = new HashMap<>(); } - public ViewRow(Map> data) { + public ViewRow(Map> data) { this.data = data; } public static ViewRow viewSetOf(ResultSet resultSet, List columnsNames, String viewName) throws SQLException { - var data = new HashMap>(); + var data = new HashMap>(); for (String columnName : columnsNames) { String label = resultSet.getString(columnName); var key = viewName + "_" + columnName; - var value = Sets.newHashSet(new ValueDTO(label, label)); + var value = Sets.newHashSet(new ValueDto(label, label)); data.put(key, value); } return new ViewRow(data); @@ -35,11 +37,11 @@ public static ViewRow viewSetOf(ResultSet resultSet, List columnsNames, // TODO, make obsolete by ViewStoreReader refactor // TODO: return unmodifiable map - public Map> getRawData() { + public Map> getRawData() { return data; } - public void put(String key, Set value) { + public void put(String key, Set value) { data.put(key, value); } @@ -48,7 +50,7 @@ public ViewRow merge(ViewRow anotherViewRow) { return this; } - private static Set addElementsAndReturn(Set set, Set elements) { + private static Set addElementsAndReturn(Set set, Set elements) { set.addAll(elements); return set; } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewService.java index 6d02b1a1bf..4d81f32c56 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewService.java @@ -24,6 +24,10 @@ import io.fairspace.saturn.config.ViewsConfig; import io.fairspace.saturn.config.properties.CacheProperties; import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.controller.dto.ColumnDto; +import io.fairspace.saturn.controller.dto.FacetDto; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.ViewDto; import io.fairspace.saturn.rdf.search.FilteredDatasetGraph; import io.fairspace.saturn.services.AccessDeniedException; import io.fairspace.saturn.services.metadata.MetadataPermissions; @@ -102,8 +106,8 @@ public class ViewService { private final Dataset ds; private final ViewStoreClientFactory viewStoreClientFactory; private final MetadataPermissions metadataPermissions; - private final LoadingCache> facetsCache; - private final LoadingCache> viewsCache; + private final LoadingCache> facetsCache; + private final LoadingCache> viewsCache; public ViewService( SearchProperties searchProperties, @@ -134,7 +138,7 @@ public void refreshCaches() { log.info("Caches refreshing/warming up successfully finished"); } - public List getFacets() { + public List getFacets() { if (!metadataPermissions.canReadFacets()) { // this check is needed for cached data only as, otherwise, // the check will be performed during retrieving data from Jena @@ -147,7 +151,7 @@ public List getFacets() { } } - public List getViews() { + public List getViews() { try { return viewsCache.get(Boolean.TRUE); } catch (ExecutionException e) { @@ -155,22 +159,22 @@ public List getViews() { } } - protected List fetchViews() { + protected List fetchViews() { return viewsConfig.views.stream() .map(v -> { - var columns = new ArrayList(); + var columns = new ArrayList(); // The entity label is the first column displayed, // if you want a column before this label, assign a negative displayIndex value in views.yaml final int ENTITY_LABEL_INDEX = 0; - columns.add(new ColumnDTO( + columns.add(new ColumnDto( v.name, v.itemName == null ? v.name : v.itemName, ColumnType.Identifier, ENTITY_LABEL_INDEX)); for (var c : v.columns) { - columns.add(new ColumnDTO(v.name + "_" + c.name, c.title, c.type, c.displayIndex)); + columns.add(new ColumnDto(v.name + "_" + c.name, c.title, c.type, c.displayIndex)); } for (var j : v.join) { var joinView = viewsConfig.getViewConfig(j.view).orElse(null); @@ -178,34 +182,34 @@ protected List fetchViews() { continue; } if (j.include.contains("id")) { - columns.add(new ColumnDTO( + columns.add(new ColumnDto( joinView.name, joinView.title, ColumnType.Identifier, j.displayIndex)); } for (var c : joinView.columns) { if (!j.include.contains(c.name)) { continue; } - columns.add(new ColumnDTO(joinView.name + "_" + c.name, c.title, c.type, j.displayIndex)); + columns.add(new ColumnDto(joinView.name + "_" + c.name, c.title, c.type, j.displayIndex)); } } - return new ViewDTO(v.name, v.title, columns, v.maxDisplayCount); + return new ViewDto(v.name, v.title, columns, v.maxDisplayCount); }) .collect(toList()); } - protected List fetchFacets() { + protected List fetchFacets() { return calculateRead(ds, () -> viewsConfig.views.stream() .flatMap(view -> view.columns.stream() .map(column -> getFacetInfo(view, column)) - .filter(f -> (f.getMin() != null - || f.getMax() != null - || (f.getValues() != null && f.getValues().size() > 1) - || f.getBooleanValue() != null))) + .filter(f -> (f.min() != null + || f.max() != null + || (f.values() != null && f.values().size() > 1) + || f.booleanValue() != null))) .collect(toList())); } - private FacetDTO getFacetInfo(View view, View.Column column) { - List values = null; + private FacetDto getFacetInfo(View view, View.Column column) { + List values = null; Object min = null; Object max = null; Boolean booleanValue = null; @@ -225,7 +229,7 @@ private FacetDTO getFacetInfo(View view, View.Column column) { for (var row : (Iterable) execution::execSelect) { var resource = row.getResource("value"); var label = row.getLiteral("label").getString(); - values.add(new ValueDTO(label, resource.getURI())); + values.add(new ValueDto(label, resource.getURI())); } } } @@ -269,7 +273,7 @@ private FacetDTO getFacetInfo(View view, View.Column column) { } } - return new FacetDTO(view.name + "_" + column.name, column.title, column.type, values, booleanValue, min, max); + return new FacetDto(view.name + "_" + column.name, column.title, column.type, values, booleanValue, min, max); } private Object convertLiteralValue(Object value) { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java index 459ca38414..986ede449d 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java @@ -30,6 +30,7 @@ import io.fairspace.saturn.config.ViewsConfig.ColumnType; import io.fairspace.saturn.config.ViewsConfig.View; import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.controller.dto.ValueDto; import io.fairspace.saturn.services.search.FileSearchRequest; import io.fairspace.saturn.services.search.SearchResultDTO; import io.fairspace.saturn.vocabulary.FS; @@ -88,14 +89,14 @@ String iriForLabel(String type, String label) throws SQLException { return null; } - Map> transformRow(View viewConfig, ResultSet result) throws SQLException { - Map> row = new HashMap<>(); + Map> transformRow(View viewConfig, ResultSet result) throws SQLException { + Map> row = new HashMap<>(); row.put( viewConfig.name, - Collections.singleton(new ValueDTO(result.getString("label"), result.getString("id")))); + Collections.singleton(new ValueDto(result.getString("label"), result.getString("id")))); if (viewConfig.name.equalsIgnoreCase("Collection")) { var collection = result.getString("collection"); - row.put(viewConfig.name + "_collection", Collections.singleton(new ValueDTO(collection, collection))); + row.put(viewConfig.name + "_collection", Collections.singleton(new ValueDto(collection, collection))); } for (var viewColumn : viewConfig.columns) { if (viewColumn.type.isSet()) { @@ -106,23 +107,23 @@ Map> transformRow(View viewConfig, ResultSet result) throw if (column.type == ColumnType.Number) { var value = result.getBigDecimal(column.name); if (value != null) { - row.put(columnName, Collections.singleton(new ValueDTO(value.toString(), value.floatValue()))); + row.put(columnName, Collections.singleton(new ValueDto(value.toString(), value.floatValue()))); } } else if (column.type == Date) { var value = result.getTimestamp(column.name); if (value != null) { row.put( columnName, - Collections.singleton(new ValueDTO(value.toInstant().toString(), value.toInstant()))); + Collections.singleton(new ValueDto(value.toInstant().toString(), value.toInstant()))); } } else { var value = result.getString(column.name); if (viewColumn.type == ColumnType.Term) { row.put( columnName, - Collections.singleton(new ValueDTO(value, iriForLabel(viewColumn.rdfType, value)))); + Collections.singleton(new ValueDto(value, iriForLabel(viewColumn.rdfType, value)))); } else { - row.put(columnName, Collections.singleton(new ValueDTO(value, value))); + row.put(columnName, Collections.singleton(new ValueDto(value, value))); } } } @@ -476,7 +477,7 @@ private ViewRow buildJoinRows( if (joinViewId != null) { // could be null as we do the left join for join views row.put( joinView.view, - Sets.newHashSet(new ValueDTO( + Sets.newHashSet(new ValueDto( result.getString(joinView.view + "_label"), result.getString(joinViewIdName)))); for (var column : projectionColumns) { var columnDefinition = Optional.ofNullable( @@ -495,19 +496,19 @@ private static void parseAndSetValueForColumn( if (columnDefinition.type == ColumnType.Number) { var value = result.getBigDecimal(columnDefinition.name); if (value != null) { - row.put(columnDefinition.name, Sets.newHashSet(new ValueDTO(value.toString(), value))); + row.put(columnDefinition.name, Sets.newHashSet(new ValueDto(value.toString(), value))); } } else if (columnDefinition.type == Date) { var value = result.getTimestamp(columnDefinition.name); if (value != null) { row.put( columnDefinition.name, - Sets.newHashSet(new ValueDTO(value.toInstant().toString(), value.toString()))); + Sets.newHashSet(new ValueDto(value.toInstant().toString(), value.toString()))); } } else { var label = result.getString(columnDefinition.name); if (label != null) { - row.put(columnDefinition.name, Sets.newHashSet(new ValueDTO(label, label))); + row.put(columnDefinition.name, Sets.newHashSet(new ValueDto(label, label))); } } } @@ -570,7 +571,7 @@ public Range aggregate(String view, String column) { * @param includeJoinedViews if true, include joined views in the resulting rows. * @return the list of rows. */ - public List>> retrieveRows( + public List>> retrieveRows( String view, List filters, int offset, int limit, boolean includeJoinedViews) { try { var viewConfig = configuration.viewConfig.get(view); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewsDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewsDTO.java deleted file mode 100644 index ccecb3e7a7..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewsDTO.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.fairspace.saturn.services.views; - -import java.util.List; - -import lombok.Value; - -@Value -public class ViewsDTO { - List views; -} diff --git a/projects/saturn/src/main/resources/application.yaml b/projects/saturn/src/main/resources/application.yaml index 9da9993202..368506cfb5 100644 --- a/projects/saturn/src/main/resources/application.yaml +++ b/projects/saturn/src/main/resources/application.yaml @@ -35,6 +35,7 @@ jwt: principal-attribute: preferred_username application: + basePath: /api publicUrl: ${PUBLIC_URL:http://localhost:8080} jena: # Base IRI for all metadata entities diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java new file mode 100644 index 0000000000..cff345e25b --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java @@ -0,0 +1,167 @@ +package io.fairspace.saturn.controller; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.auth.JwtAuthConverterProperties; +import io.fairspace.saturn.config.Services; +import io.fairspace.saturn.config.ViewsConfig; +import io.fairspace.saturn.controller.dto.CountDto; +import io.fairspace.saturn.controller.dto.FacetDto; +import io.fairspace.saturn.controller.dto.FacetsDto; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.ViewDto; +import io.fairspace.saturn.controller.dto.ViewPageDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; +import io.fairspace.saturn.services.views.QueryService; +import io.fairspace.saturn.services.views.ViewService; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ViewController.class) +@ImportAutoConfiguration(exclude = {SecurityAutoConfiguration.class, OAuth2ResourceServerAutoConfiguration.class}) +public class ViewControllerTest { + + private static final String VIEWS_URL_TEMPLATE = "/api/views/"; + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ViewService viewService; + + @MockBean + private QueryService queryService; + + @MockBean + private JwtAuthConverterProperties jwtAuthConverterProperties; + + @MockBean + private Services services; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeEach + public void setUp() { + when(services.getViewService()).thenReturn(viewService); + when(services.getQueryService()).thenReturn(queryService); + } + + @Test + public void testGetViewsSuccess() throws Exception { + // Mock data for getViews + var viewDto = new ViewDto("view1", "View 1", List.of(), 100L); + + when(viewService.getViews()).thenReturn(List.of(viewDto)); + + mockMvc.perform(get(VIEWS_URL_TEMPLATE).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.views", hasSize(1))) + .andExpect(jsonPath("$.views[0].name", is("view1"))) + .andExpect(jsonPath("$.views[0].title", is("View 1"))) + .andExpect(jsonPath("$.views[0].columns", hasSize(0))) + .andExpect(jsonPath("$.views[0].maxDisplayCount", is(100))); // Max display count is null + } + + @Test + public void testCreateViewSuccess() throws Exception { + // Mock request body and response + var viewRequest = new ViewRequest(); + viewRequest.setView("view1"); + viewRequest.setPage(1); + viewRequest.setSize(10); + + var row = Map.of("view1_column1", Set.of(new ValueDto("label1", "value1"))); + var viewPageDto = ViewPageDto.builder() + .rows(List.of(row)) + .hasNext(false) + .timeout(false) + .totalCount(100L) + .totalPages(10L) + .build(); + + when(queryService.retrieveViewPage(viewRequest)).thenReturn(viewPageDto); + + mockMvc.perform(post(VIEWS_URL_TEMPLATE) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(viewRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.rows", hasSize(1))) + .andExpect(jsonPath("$.rows[0]['view1_column1']", hasSize(1))) + .andExpect(jsonPath("$.rows[0]['view1_column1'][0].value", is("value1"))) + .andExpect(jsonPath("$.hasNext", is(false))) + .andExpect(jsonPath("$.timeout", is(false))) + .andExpect(jsonPath("$.totalCount", is(100))) + .andExpect(jsonPath("$.totalPages", is(10))); + } + + @Test + public void testGetFacetsSuccess() throws Exception { + // Mock data for getFacets + var facetDto = new FacetDto("facet1", "Facet 1", ViewsConfig.ColumnType.Set, List.of(), null, null, null); + var mockFacetsDto = new FacetsDto(List.of(facetDto)); + + when(viewService.getFacets()).thenReturn(List.of(facetDto)); + + mockMvc.perform(get(VIEWS_URL_TEMPLATE + "facets").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.facets", hasSize(1))) + .andExpect(jsonPath("$.facets[0].name", is("facet1"))) + .andExpect(jsonPath("$.facets[0].title", is("Facet 1"))) + .andExpect( + jsonPath("$.facets[0].type", is(ViewsConfig.ColumnType.Set.getName()))); // Empty options list + } + + @Test + public void testCountSuccess() throws Exception { + // Mock request body and response + var countRequest = new CountRequest(); + countRequest.setView("view1"); + countRequest.setFilters(List.of()); + + var countDto = new CountDto(100, false); + + when(queryService.count(countRequest)).thenReturn(countDto); + + mockMvc.perform(post(VIEWS_URL_TEMPLATE + "count") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(countRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.count", is(100))) + .andExpect(jsonPath("$.timeout", is(false))); + } + + @Test + public void testCreateViewValidationFailure() throws Exception { + // Test validation error (e.g., invalid request body) + var invalidRequestBody = new ViewRequest(); + invalidRequestBody.setPage(0); // Invalid page (must be >= 1) + invalidRequestBody.setSize(0); // Invalid size (must be >= 1) + + mockMvc.perform(post(VIEWS_URL_TEMPLATE) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequestBody))) + .andExpect(status().isBadRequest()); + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/JdbcQueryServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/JdbcQueryServiceTest.java index d404bc9fe0..06e51ecc47 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/JdbcQueryServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/JdbcQueryServiceTest.java @@ -29,6 +29,9 @@ import io.fairspace.saturn.config.properties.JenaProperties; import io.fairspace.saturn.config.properties.SearchProperties; import io.fairspace.saturn.config.properties.WebDavProperties; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.transactions.SimpleTransactions; import io.fairspace.saturn.rdf.transactions.Transactions; @@ -207,19 +210,19 @@ public void testRetrieveSamplePage() { row.keySet()); Assert.assertEquals( "Sample A for subject 1", - row.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals( "Blood", - row.get("Sample_nature").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample_nature").stream().findFirst().orElseThrow().label()); Assert.assertEquals( "Liver", - row.get("Sample_topography").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample_topography").stream().findFirst().orElseThrow().label()); Assert.assertEquals( 45.2f, ((Number) row.get("Sample_tumorCellularity").stream() .findFirst() .orElseThrow() - .getValue()) + .value()) .floatValue(), 0.01); } @@ -245,19 +248,19 @@ public void testRetrieveSamplePageAfterReindexing() { row.keySet()); Assert.assertEquals( "Sample A for subject 1", - row.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals( "Blood", - row.get("Sample_nature").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample_nature").stream().findFirst().orElseThrow().label()); Assert.assertEquals( "Liver", - row.get("Sample_topography").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample_topography").stream().findFirst().orElseThrow().label()); Assert.assertEquals( 45.2f, ((Number) row.get("Sample_tumorCellularity").stream() .findFirst() .orElseThrow() - .getValue()) + .value()) .floatValue(), 0.01); } @@ -316,17 +319,15 @@ public void testRetrieveSamplePageIncludeJoin() { var row1 = page.getRows().getFirst(); Assert.assertEquals( "Sample A for subject 1", - row1.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row1.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals(1, row1.get("Subject").size()); var row2 = page.getRows().get(1); Assert.assertEquals( "Sample B for subject 2", - row2.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row2.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals( Set.of("RNA-seq", "Whole genome sequencing"), - row2.get("Resource_analysisType").stream() - .map(ValueDTO::getLabel) - .collect(Collectors.toSet())); + row2.get("Resource_analysisType").stream().map(ValueDto::label).collect(Collectors.toSet())); } @Test @@ -342,17 +343,15 @@ public void testRetrieveSamplePageIncludeJoinAfterReindexing() { var row1 = page.getRows().getFirst(); Assert.assertEquals( "Sample A for subject 1", - row1.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row1.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals(1, row1.get("Subject").size()); var row2 = page.getRows().get(1); Assert.assertEquals( "Sample B for subject 2", - row2.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row2.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals( Set.of("RNA-seq", "Whole genome sequencing"), - row2.get("Resource_analysisType").stream() - .map(ValueDTO::getLabel) - .collect(Collectors.toSet())); + row2.get("Resource_analysisType").stream().map(ValueDto::label).collect(Collectors.toSet())); } @Test @@ -361,7 +360,7 @@ public void testCountSamplesWithoutMaxDisplayCount() { var requestParams = new CountRequest(); requestParams.setView("Sample"); var result = sut.count(requestParams); - assertEquals(2, result.getCount()); + assertEquals(2, result.count()); } @Test @@ -369,7 +368,7 @@ public void testCountSubjectWithMaxDisplayCountLimitLessThanTotalCount() { var request = new CountRequest(); request.setView("Subject"); var result = sut.count(request); - Assert.assertEquals(1, result.getCount()); + Assert.assertEquals(1, result.count()); } @Test @@ -377,6 +376,6 @@ public void testCountResourceWithMaxDisplayCountLimitMoreThanTotalCount() { var request = new CountRequest(); request.setView("Resource"); var result = sut.count(request); - Assert.assertEquals(4, result.getCount()); + Assert.assertEquals(4, result.count()); } } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/SparqlQueryServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/SparqlQueryServiceTest.java index e63725bffa..644e882f8a 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/SparqlQueryServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/SparqlQueryServiceTest.java @@ -26,6 +26,8 @@ import io.fairspace.saturn.config.properties.JenaProperties; import io.fairspace.saturn.config.properties.SearchProperties; import io.fairspace.saturn.config.properties.WebDavProperties; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.search.FilteredDatasetGraph; import io.fairspace.saturn.rdf.transactions.SimpleTransactions; @@ -187,24 +189,19 @@ public void testRetrieveSamplePage() { assertEquals(2, page.getRows().size()); // The implementation does not sort results. Probably deterministic, // but no certain order is guaranteed. - var row = page.getRows() - .get(0) - .get("Sample") - .iterator() - .next() - .getValue() - .equals("http://example.com/samples#s1-a") - ? page.getRows().get(0) - : page.getRows().get(1); + var row = + page.getRows().get(0).get("Sample").iterator().next().value().equals("http://example.com/samples#s1-a") + ? page.getRows().get(0) + : page.getRows().get(1); assertEquals( - "Sample A for subject 1", row.get("Sample").iterator().next().getLabel()); + "Sample A for subject 1", row.get("Sample").iterator().next().label()); assertEquals( - SAMPLE_NATURE_BLOOD, row.get("Sample_nature").iterator().next().getValue()); - assertEquals("Blood", row.get("Sample_nature").iterator().next().getLabel()); - assertEquals("Liver", row.get("Sample_topography").iterator().next().getLabel()); + SAMPLE_NATURE_BLOOD, row.get("Sample_nature").iterator().next().value()); + assertEquals("Blood", row.get("Sample_nature").iterator().next().label()); + assertEquals("Liver", row.get("Sample_topography").iterator().next().label()); assertEquals( 45.2f, - ((Number) row.get("Sample_tumorCellularity").iterator().next().getValue()).floatValue(), + ((Number) row.get("Sample_tumorCellularity").iterator().next().value()).floatValue(), 0.01); } @@ -214,7 +211,7 @@ public void testCountSamplesWithoutMaxDisplayCount() { var requestParams = new CountRequest(); requestParams.setView("Sample"); var result = queryService.count(requestParams); - assertEquals(2, result.getCount()); + assertEquals(2, result.count()); } @Test @@ -222,7 +219,7 @@ public void testCountSubjectWithMaxDisplayCountLimitLessThanTotalCount() { var request = new CountRequest(); request.setView("Subject"); var result = queryService.count(request); - Assert.assertEquals(1, result.getCount()); + Assert.assertEquals(1, result.count()); } @Test @@ -232,7 +229,7 @@ public void testCountResourceWithAccess() { request.setView("Resource"); var result = queryService.count(request); - Assert.assertEquals(3, result.getCount()); + Assert.assertEquals(3, result.count()); } @Test @@ -241,7 +238,7 @@ public void testCountSamplesWithoutViewAccess() { var countRequest = new CountRequest(); countRequest.setView("Sample"); var result = queryService.count(countRequest); - assertEquals(0, result.getCount()); + assertEquals(0, result.count()); } @Test diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/ViewServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/ViewServiceTest.java index ea1eb00d20..d4ce392182 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/ViewServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/ViewServiceTest.java @@ -98,12 +98,12 @@ public void testFetchViewConfig() { when(permissions.canReadFacets()).thenReturn(true); var facets = viewService.getFacets(); var dateFacets = facets.stream() - .filter(facet -> facet.getType() == ViewsConfig.ColumnType.Date) + .filter(facet -> facet.type() == ViewsConfig.ColumnType.Date) .toList(); Assert.assertEquals(2, dateFacets.size()); var boolFacets = facets.stream() - .filter(facet -> facet.getType() == ViewsConfig.ColumnType.Boolean) + .filter(facet -> facet.type() == ViewsConfig.ColumnType.Boolean) .toList(); Assert.assertEquals(1, boolFacets.size()); } @@ -121,23 +121,23 @@ public void testNoAccessExceptionFetchingFacetsWhenUserHasNoPermissions() { @Test public void testDisplayIndex_IsSet() { var views = viewService.getViews(); - var columns = views.get(1).getColumns().stream().toList(); + var columns = views.get(1).columns().stream().toList(); var selectedColumn = columns.stream() - .filter(c -> c.getTitle().equals("Morphology")) + .filter(c -> c.title().equals("Morphology")) .toList() .getFirst(); - Assert.assertEquals(Integer.valueOf(1), selectedColumn.getDisplayIndex()); + Assert.assertEquals(Integer.valueOf(1), selectedColumn.displayIndex()); } @Test public void testDisplayIndex_IsNotSet() { var views = viewService.getViews(); - var columns = views.get(1).getColumns().stream().toList(); + var columns = views.get(1).columns().stream().toList(); var selectedColumn = columns.stream() - .filter(c -> c.getTitle().equals("Laterality")) + .filter(c -> c.title().equals("Laterality")) .toList() .getFirst(); - Assert.assertEquals(Integer.valueOf(Integer.MAX_VALUE), selectedColumn.getDisplayIndex()); + Assert.assertEquals(Integer.valueOf(Integer.MAX_VALUE), selectedColumn.displayIndex()); } @Test From d3f11cd28afc1e4ad4b08c00fd8ff68584db62e4 Mon Sep 17 00:00:00 2001 From: anton Date: Fri, 4 Oct 2024 16:40:37 +0200 Subject: [PATCH 02/18] FAIRSPC-81: migrated workspace endpoints to spring mvc --- .../saturn/config/ServiceConfig.java | 15 ++ .../saturn/config/SparkFilterFactory.java | 3 +- .../controller/WorkspaceController.java | 70 +++++++++ .../GlobalExceptionHandler.java | 2 +- .../services/workspaces/WorkspaceService.java | 80 +++++----- .../controller/WorkspaceControllerTest.java | 148 ++++++++++++++++++ 6 files changed, 280 insertions(+), 38 deletions(-) create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java rename projects/saturn/src/main/java/io/fairspace/saturn/controller/{ => exception}/GlobalExceptionHandler.java (92%) create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/ServiceConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/ServiceConfig.java index 102d423ed7..9692f0a1f5 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/ServiceConfig.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/ServiceConfig.java @@ -2,6 +2,8 @@ import java.sql.SQLException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import lombok.RequiredArgsConstructor; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.Keycloak; @@ -20,12 +22,16 @@ import io.fairspace.saturn.config.properties.ViewDatabaseProperties; import io.fairspace.saturn.config.properties.WebDavProperties; import io.fairspace.saturn.rdf.SaturnDatasetFactory; +import io.fairspace.saturn.services.IRIModule; import io.fairspace.saturn.services.users.UserService; import io.fairspace.saturn.services.views.SparqlQueryService; import io.fairspace.saturn.services.views.ViewStoreClientFactory; import static io.fairspace.saturn.config.ConfigLoader.VIEWS_CONFIG; +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS; + /** * Configuration for the Spark filter to enable the Saturn API. */ @@ -93,4 +99,13 @@ public Keycloak getKeycloak(KeycloakClientProperties keycloakClientProperties) { .password(keycloakClientProperties.getClientSecret()) .build(); } + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new IRIModule()) + .registerModule(new JavaTimeModule()) + .configure(WRITE_DATES_AS_TIMESTAMPS, false) + .configure(FAIL_ON_UNKNOWN_PROPERTIES, false); + } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java index b43d4b0cfe..df9f62d200 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java @@ -9,7 +9,6 @@ import io.fairspace.saturn.services.search.SearchApp; import io.fairspace.saturn.services.users.LogoutApp; import io.fairspace.saturn.services.users.UserApp; -import io.fairspace.saturn.services.workspaces.WorkspaceApp; public class SparkFilterFactory { public static SaturnSparkFilter createSparkFilter( @@ -19,7 +18,7 @@ public static SaturnSparkFilter createSparkFilter( FeatureProperties featureProperties, String publicUrl) { return new SaturnSparkFilter( - new WorkspaceApp(apiPathPrefix + "/workspaces", svc.getWorkspaceService()), + // new WorkspaceApp(apiPathPrefix + "/workspaces", svc.getWorkspaceService()), new MetadataApp(apiPathPrefix + "/metadata", svc.getMetadataService()), // new ViewApp(apiPathPrefix + "/views", svc.getViewService(), svc.getQueryService()), new SearchApp(apiPathPrefix + "/search", svc.getSearchService(), svc.getFileSearchService()), diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java new file mode 100644 index 0000000000..2853326686 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java @@ -0,0 +1,70 @@ +package io.fairspace.saturn.controller; + +import java.util.List; +import java.util.Map; + +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.config.Services; +import io.fairspace.saturn.services.workspaces.UserRoleDto; +import io.fairspace.saturn.services.workspaces.Workspace; +import io.fairspace.saturn.services.workspaces.WorkspaceRole; + +@RestController +@RequestMapping("${application.basePath}/workspaces") +@Validated +public class WorkspaceController { + + private final Services services; + + public WorkspaceController(Services services) { + this.services = services; + } + + @PutMapping(value = "/") + public ResponseEntity createWorkspace(@RequestBody Workspace workspace) { + var createdWorkspace = services.getWorkspaceService().createWorkspace(workspace); + return ResponseEntity.ok( + createdWorkspace); // it should return HTTP 201 CREATED - tobe analyzed across the codebase + } + + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> listWorkspaces() { + var workspaces = services.getWorkspaceService().listWorkspaces(); + return ResponseEntity.ok(workspaces); + } + + @DeleteMapping(value = "/") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteWorkspace(@RequestParam("workspace") String workspaceUri) { + services.getWorkspaceService().deleteWorkspace(NodeFactory.createURI(workspaceUri)); + } + + @GetMapping(value = "/users/") + public ResponseEntity> getUsers(@RequestParam("workspace") String workspaceUri) { + var uri = NodeFactory.createURI(workspaceUri); + var users = services.getWorkspaceService().getUsers(uri); + return ResponseEntity.ok(users); + } + + @PatchMapping(value = "/users/", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void setUserRole(@RequestBody UserRoleDto userRoleDto) { + services.getWorkspaceService() + .setUserRole(userRoleDto.getWorkspace(), userRoleDto.getUser(), userRoleDto.getRole()); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/GlobalExceptionHandler.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java similarity index 92% rename from projects/saturn/src/main/java/io/fairspace/saturn/controller/GlobalExceptionHandler.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java index 4f8b4a0199..95aaf8c240 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/GlobalExceptionHandler.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package io.fairspace.saturn.controller; +package io.fairspace.saturn.controller.exception; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java index aca6bf19a0..701fc3a591 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java @@ -38,41 +38,51 @@ public WorkspaceService(Transactions tx, UserService userService) { public List listWorkspaces() { return tx.calculateRead(m -> { - var user = m.wrapAsResource(getUserURI()); - return new DAO(m) - .list(Workspace.class).stream() - .peek(ws -> { - var res = m.wrapAsResource(ws.getIri()); - ws.setCanManage( - userService.currentUser().isAdmin() || user.hasProperty(FS.isManagerOf, res)); - ws.setCanCollaborate(ws.isCanManage() || user.hasProperty(FS.isMemberOf, res)); - var workspaceCollections = m.listSubjectsWithProperty(FS.ownedBy, res) - .filterKeep(r -> r.hasProperty(RDF.type, FS.Collection)) - .toList(); - var totalCollectionCount = workspaceCollections.size(); - var nonDeletedCollectionCount = (int) workspaceCollections.stream() - .filter(collection -> !collection.hasProperty(FS.dateDeleted)) - .count(); - var memberCount = m.listSubjectsWithProperty(RDF.type, FS.User) - .filterKeep(u -> u.hasProperty(FS.isMemberOf, res)) - .toList() - .size(); - var managers = new DAO(m) - .list(User.class).stream() - .filter(u -> m.wrapAsResource(u.getIri()) - .hasProperty(FS.isManagerOf, res)) - .collect(toList()); - ws.setSummary(WorkspaceSummary.builder() - .totalCollectionCount(totalCollectionCount) - .nonDeletedCollectionCount(nonDeletedCollectionCount) - .memberCount(memberCount + managers.size()) - .build()); - ws.setManagers(managers); - }) - .filter(ws -> userService.currentUser().isCanViewPublicMetadata() - || ws.isCanManage() - || ws.isCanCollaborate()) - .collect(toList()); + try { + var user = m.wrapAsResource(getUserURI()); + return new DAO(m) + .list(Workspace.class).stream() + .peek(ws -> { + var res = m.wrapAsResource(ws.getIri()); + ws.setCanManage(userService.currentUser().isAdmin() + || user.hasProperty(FS.isManagerOf, res)); + ws.setCanCollaborate(ws.isCanManage() || user.hasProperty(FS.isMemberOf, res)); + var workspaceCollections = m.listSubjectsWithProperty(FS.ownedBy, res) + .filterKeep(r -> r.hasProperty(RDF.type, FS.Collection)) + .toList(); + var totalCollectionCount = workspaceCollections.size(); + var nonDeletedCollectionCount = (int) workspaceCollections.stream() + .filter(collection -> !collection.hasProperty(FS.dateDeleted)) + .count(); + var memberCount = m.listSubjectsWithProperty(RDF.type, FS.User) + .filterKeep(u -> u.hasProperty(FS.isMemberOf, res)) + .toList() + .size(); + var managers = new DAO(m) + .list(User.class).stream() + .filter(u -> m.wrapAsResource(u.getIri()) + .hasProperty(FS.isManagerOf, res)) + .collect(toList()); + ws.setSummary(WorkspaceSummary.builder() + .totalCollectionCount(totalCollectionCount) + .nonDeletedCollectionCount(nonDeletedCollectionCount) + .memberCount(memberCount + managers.size()) + .build()); + ws.setManagers(managers); + }) + .filter(ws -> userService.currentUser().isCanViewPublicMetadata() + || ws.isCanManage() + || ws.isCanCollaborate()) + .collect(toList()); + } catch (Throwable e) { + log.error("+++++++++++++++++++++++++++++"); + log.error("+++++++++++++++++++++++++++++"); + log.error("+++++++++++++++++++++++++++++"); + log.error("+++++++++++++++++++++++++++++"); + log.error("+++++++++++++++++++++++++++++"); + log.error("Error listing workspaces", e); + throw e; + } }); } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java new file mode 100644 index 0000000000..c794ae410e --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java @@ -0,0 +1,148 @@ +package io.fairspace.saturn.controller; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.auth.JwtAuthConverterProperties; +import io.fairspace.saturn.config.Services; +import io.fairspace.saturn.services.IRIModule; +import io.fairspace.saturn.services.workspaces.Workspace; +import io.fairspace.saturn.services.workspaces.WorkspaceRole; +import io.fairspace.saturn.services.workspaces.WorkspaceService; + +import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(WorkspaceController.class) +@ImportAutoConfiguration(exclude = {SecurityAutoConfiguration.class, OAuth2ResourceServerAutoConfiguration.class}) +@Import(WorkspaceControllerTest.CustomObjectMapperConfig.class) +class WorkspaceControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private JwtAuthConverterProperties jwtAuthConverterProperties; + + @MockBean + private Services services; + + @MockBean + private WorkspaceService workspaceService; + + @BeforeEach + void setUp() { + when(services.getWorkspaceService()).thenReturn(workspaceService); + } + + @TestConfiguration + static class CustomObjectMapperConfig { + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new IRIModule()) + .findAndRegisterModules(); // Automatically registers JavaTimeModule, etc. + } + } + + @Test + void createWorkspace_shouldReturnCreatedWorkspace() throws Exception { + Workspace workspace = new Workspace(); + workspace.setCode("WS001"); + + when(workspaceService.createWorkspace(any(Workspace.class))).thenReturn(workspace); + + mockMvc.perform(put("/api/workspaces/") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"code\": \"WS001\", \"title\": \"New Workspace\"}")) + .andExpect(status().isOk()) // Use isCreated() if HTTP 201 Created is implemented + .andExpect(jsonPath("$.code").value("WS001")); + } + + @Test + void listWorkspaces_shouldReturnListOfWorkspaces() throws Exception { + Workspace workspace = new Workspace(); + workspace.setCode("WS001"); + + when(workspaceService.listWorkspaces()).thenReturn(List.of(workspace)); + + mockMvc.perform(get("/api/workspaces/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].code").value("WS001")); + } + + @Test + void deleteWorkspace_shouldDeleteWorkspace() throws Exception { + String workspaceUri = "http://example.com/workspace/1"; + + mockMvc.perform(delete("/api/workspaces/").param("workspace", workspaceUri)) + .andExpect(status().isNoContent()); + + Mockito.verify(workspaceService).deleteWorkspace(NodeFactory.createURI(workspaceUri)); + } + + @Test + void getUsers_shouldReturnWorkspaceUsers() throws Exception { + String workspaceUri = "http://example.com/workspace/1"; + var users = Map.of(NodeFactory.createURI("http://example.com/user/1"), WorkspaceRole.Member); + + when(workspaceService.getUsers(any())).thenReturn(users); + + mockMvc.perform(get("/api/workspaces/users/").param("workspace", workspaceUri)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$['http://example.com/user/1']").value("Member")); + } + + @Test + void setUserRole_shouldUpdateUserRole() throws Exception { + mockMvc.perform( + patch("/api/workspaces/users/") + .contentType(MediaType.APPLICATION_JSON) + .content( + "{\"workspace\": \"http://example.com/workspace/1\", \"user\": \"http://example.com/user/1\", \"role\": \"Manager\"}")) + .andExpect(status().isNoContent()); + + // Use ArgumentCaptor to capture the arguments passed to the method + ArgumentCaptor workspaceCaptor = ArgumentCaptor.forClass(Node.class); + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(Node.class); + ArgumentCaptor roleCaptor = ArgumentCaptor.forClass(WorkspaceRole.class); + + // Verify that setUserRole was called once and capture the arguments + Mockito.verify(workspaceService, times(1)) + .setUserRole(workspaceCaptor.capture(), userCaptor.capture(), roleCaptor.capture()); + + // Now you can assert that the captured arguments are what you expect + assertEquals(NodeFactory.createURI("http://example.com/workspace/1"), workspaceCaptor.getValue()); + assertEquals(NodeFactory.createURI("http://example.com/user/1"), userCaptor.getValue()); + assertEquals(WorkspaceRole.Manager, roleCaptor.getValue()); // Make sure the role is Manager + } +} From 51b3ce9725ed9067ef3b2f462fe607786937611c Mon Sep 17 00:00:00 2001 From: anton Date: Fri, 4 Oct 2024 16:49:21 +0200 Subject: [PATCH 03/18] FAIRSPC-81: refactoring of controller tests --- .../saturn/config/SparkFilterFactory.java | 2 - .../services/workspaces/WorkspaceApp.java | 49 ------------------- .../saturn/controller/BaseControllerTest.java | 39 +++++++++++++++ .../saturn/controller/ViewControllerTest.java | 18 ++----- .../controller/WorkspaceControllerTest.java | 32 +----------- 5 files changed, 44 insertions(+), 96 deletions(-) delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceApp.java create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/BaseControllerTest.java diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java index df9f62d200..79feaa599b 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java @@ -18,9 +18,7 @@ public static SaturnSparkFilter createSparkFilter( FeatureProperties featureProperties, String publicUrl) { return new SaturnSparkFilter( - // new WorkspaceApp(apiPathPrefix + "/workspaces", svc.getWorkspaceService()), new MetadataApp(apiPathPrefix + "/metadata", svc.getMetadataService()), - // new ViewApp(apiPathPrefix + "/views", svc.getViewService(), svc.getQueryService()), new SearchApp(apiPathPrefix + "/search", svc.getSearchService(), svc.getFileSearchService()), new VocabularyApp(apiPathPrefix + "/vocabulary"), new UserApp(apiPathPrefix + "/users", svc.getUserService()), diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceApp.java deleted file mode 100644 index 145c7404e1..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceApp.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.fairspace.saturn.services.workspaces; - -import io.fairspace.saturn.services.BaseApp; - -import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT; -import static org.apache.jena.graph.NodeFactory.createURI; -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.*; - -public class WorkspaceApp extends BaseApp { - private final WorkspaceService workspaceService; - - public WorkspaceApp(String basePath, WorkspaceService workspaceService) { - super(basePath); - this.workspaceService = workspaceService; - } - - @Override - protected void initApp() { - put("/", (req, res) -> { - var ws = workspaceService.createWorkspace(mapper.readValue(req.body(), Workspace.class)); - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(ws); - }); - - get("/", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(workspaceService.listWorkspaces()); - }); - - delete("/", (req, res) -> { - workspaceService.deleteWorkspace(createURI(req.queryParams("workspace"))); - res.status(SC_NO_CONTENT); - return ""; - }); - - get("/users/", (req, res) -> { - var users = workspaceService.getUsers(createURI(req.queryParams("workspace"))); - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(users); - }); - - patch("/users/", (req, res) -> { - var dto = mapper.readValue(req.body(), UserRoleDto.class); - workspaceService.setUserRole(dto.getWorkspace(), dto.getUser(), dto.getRole()); - return ""; - }); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/BaseControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/BaseControllerTest.java new file mode 100644 index 0000000000..4c843499f6 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/BaseControllerTest.java @@ -0,0 +1,39 @@ +package io.fairspace.saturn.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +import io.fairspace.saturn.auth.JwtAuthConverterProperties; +import io.fairspace.saturn.config.Services; +import io.fairspace.saturn.services.IRIModule; + +@ImportAutoConfiguration(exclude = {SecurityAutoConfiguration.class, OAuth2ResourceServerAutoConfiguration.class}) +@Import(BaseControllerTest.CustomObjectMapperConfig.class) +class BaseControllerTest { + + @MockBean + private JwtAuthConverterProperties jwtAuthConverterProperties; + + @MockBean + private Services services; + + @TestConfiguration + static class CustomObjectMapperConfig { + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new IRIModule()) + .findAndRegisterModules(); // Automatically registers JavaTimeModule, etc. + } + } + + protected Services getService() { + return services; + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java index cff345e25b..445546bd17 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java @@ -8,16 +8,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import io.fairspace.saturn.auth.JwtAuthConverterProperties; -import io.fairspace.saturn.config.Services; import io.fairspace.saturn.config.ViewsConfig; import io.fairspace.saturn.controller.dto.CountDto; import io.fairspace.saturn.controller.dto.FacetDto; @@ -39,8 +34,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(ViewController.class) -@ImportAutoConfiguration(exclude = {SecurityAutoConfiguration.class, OAuth2ResourceServerAutoConfiguration.class}) -public class ViewControllerTest { +public class ViewControllerTest extends BaseControllerTest { private static final String VIEWS_URL_TEMPLATE = "/api/views/"; @@ -53,19 +47,13 @@ public class ViewControllerTest { @MockBean private QueryService queryService; - @MockBean - private JwtAuthConverterProperties jwtAuthConverterProperties; - - @MockBean - private Services services; - @Autowired private ObjectMapper objectMapper; @BeforeEach public void setUp() { - when(services.getViewService()).thenReturn(viewService); - when(services.getQueryService()).thenReturn(queryService); + when(getService().getViewService()).thenReturn(viewService); + when(getService().getQueryService()).thenReturn(queryService); } @Test diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java index c794ae410e..af944fe9db 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java @@ -3,7 +3,6 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; import org.junit.jupiter.api.BeforeEach; @@ -11,20 +10,11 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import io.fairspace.saturn.auth.JwtAuthConverterProperties; -import io.fairspace.saturn.config.Services; -import io.fairspace.saturn.services.IRIModule; import io.fairspace.saturn.services.workspaces.Workspace; import io.fairspace.saturn.services.workspaces.WorkspaceRole; import io.fairspace.saturn.services.workspaces.WorkspaceService; @@ -42,35 +32,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(WorkspaceController.class) -@ImportAutoConfiguration(exclude = {SecurityAutoConfiguration.class, OAuth2ResourceServerAutoConfiguration.class}) -@Import(WorkspaceControllerTest.CustomObjectMapperConfig.class) -class WorkspaceControllerTest { +class WorkspaceControllerTest extends BaseControllerTest { @Autowired private MockMvc mockMvc; - @MockBean - private JwtAuthConverterProperties jwtAuthConverterProperties; - - @MockBean - private Services services; - @MockBean private WorkspaceService workspaceService; @BeforeEach void setUp() { - when(services.getWorkspaceService()).thenReturn(workspaceService); - } - - @TestConfiguration - static class CustomObjectMapperConfig { - @Bean - public ObjectMapper objectMapper() { - return new ObjectMapper() - .registerModule(new IRIModule()) - .findAndRegisterModules(); // Automatically registers JavaTimeModule, etc. - } + when(getService().getWorkspaceService()).thenReturn(workspaceService); } @Test From ea2e4d6c1f18017862498dc2d1e5f3c1d8af6ed3 Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 7 Oct 2024 22:00:31 +0200 Subject: [PATCH 04/18] FAIRSPC-81: migrated metadata API to spring mvc --- .../saturn/config/SparkFilterFactory.java | 2 - .../saturn/controller/MetadataController.java | 94 +++++++++++++ .../controller/enums/CustomMediaType.java | 11 ++ .../exception/GlobalExceptionHandler.java | 51 ++++++- .../controller/validation/IriValidator.java | 30 ++++ .../controller/validation/ValidIri.java | 21 +++ .../saturn/services/metadata/MetadataApp.java | 108 --------------- .../controller/MetadataControllerTest.java | 129 ++++++++++++++++++ .../validation/IriValidatorTest.java | 56 ++++++++ 9 files changed, 389 insertions(+), 113 deletions(-) create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/IriValidator.java create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/ValidIri.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataApp.java create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/validation/IriValidatorTest.java diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java index 79feaa599b..cf6ef08f8e 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java @@ -4,7 +4,6 @@ import io.fairspace.saturn.config.properties.KeycloakClientProperties; import io.fairspace.saturn.services.features.FeaturesApp; import io.fairspace.saturn.services.maintenance.MaintenanceApp; -import io.fairspace.saturn.services.metadata.MetadataApp; import io.fairspace.saturn.services.metadata.VocabularyApp; import io.fairspace.saturn.services.search.SearchApp; import io.fairspace.saturn.services.users.LogoutApp; @@ -18,7 +17,6 @@ public static SaturnSparkFilter createSparkFilter( FeatureProperties featureProperties, String publicUrl) { return new SaturnSparkFilter( - new MetadataApp(apiPathPrefix + "/metadata", svc.getMetadataService()), new SearchApp(apiPathPrefix + "/search", svc.getSearchService(), svc.getFileSearchService()), new VocabularyApp(apiPathPrefix + "/vocabulary"), new UserApp(apiPathPrefix + "/users", svc.getUserService()), diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java new file mode 100644 index 0000000000..08510b37ea --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java @@ -0,0 +1,94 @@ +package io.fairspace.saturn.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ResourceFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.config.Services; +import io.fairspace.saturn.controller.validation.ValidIri; + +import static io.fairspace.saturn.controller.enums.CustomMediaType.APPLICATION_LD_JSON; +import static io.fairspace.saturn.controller.enums.CustomMediaType.APPLICATION_N_TRIPLES; +import static io.fairspace.saturn.controller.enums.CustomMediaType.TEXT_TURTLE; +import static io.fairspace.saturn.services.metadata.Serialization.deserialize; +import static io.fairspace.saturn.services.metadata.Serialization.getFormat; +import static io.fairspace.saturn.services.metadata.Serialization.serialize; + +@Log4j2 +@RestController +@RequestMapping("/api/metadata/") +@RequiredArgsConstructor +@Validated +public class MetadataController { + + private static final String DO_VIEWS_UPDATE = "doViewsUpdate"; + + public static final String DO_VIEWS_UPDATE_DEFAULT_VALUE = "true"; + + private final Services services; + + @GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) + public String getMetadata( + @RequestParam(required = false) String subject, + @RequestParam(name = "withValueProperties", defaultValue = "false") boolean withValueProperties, + @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader) { + Model model = services.getMetadataService().get(subject, withValueProperties); + var format = getFormat(acceptHeader); + return serialize(model, format); + } + + @PutMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void putMetadata( + @RequestBody String body, + @RequestHeader(value = HttpHeaders.CONTENT_TYPE, required = false) String contentType, + @RequestParam(name = DO_VIEWS_UPDATE, defaultValue = DO_VIEWS_UPDATE_DEFAULT_VALUE) + boolean doMaterializedViewsRefresh) { + Model model = deserialize(body, contentType); + services.getMetadataService().put(model, doMaterializedViewsRefresh); + } + + @PatchMapping( + consumes = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void patchMetadata( + @RequestBody String body, + @RequestHeader(value = HttpHeaders.CONTENT_TYPE, required = false) String contentType, + @RequestParam(name = DO_VIEWS_UPDATE, defaultValue = DO_VIEWS_UPDATE_DEFAULT_VALUE) boolean doViewsUpdate) { + Model model = deserialize(body, contentType); + services.getMetadataService().patch(model, doViewsUpdate); + } + + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteMetadata( + @RequestParam(required = false) @ValidIri String subject, + @RequestBody(required = false) String body, + @RequestHeader(value = HttpHeaders.CONTENT_TYPE, required = false) String contentType, + @RequestParam(name = DO_VIEWS_UPDATE, defaultValue = DO_VIEWS_UPDATE_DEFAULT_VALUE) + boolean doMaterializedViewsRefresh) { + if (subject != null) { + if (!services.getMetadataService().softDelete(ResourceFactory.createResource(subject))) { + throw new IllegalArgumentException("Subject could not be deleted"); + } + } else { + Model model = deserialize(body, contentType); + services.getMetadataService().delete(model, doMaterializedViewsRefresh); + } + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java new file mode 100644 index 0000000000..8eaa0b184c --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java @@ -0,0 +1,11 @@ +package io.fairspace.saturn.controller.enums; + +public enum CustomMediaType { + ; + + public static final String APPLICATION_LD_JSON = "application/ld+json"; + + public static final String TEXT_TURTLE = "text/turtle"; + + public static final String APPLICATION_N_TRIPLES = "application/n-triples"; +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java index 95aaf8c240..55e4deab16 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java @@ -1,17 +1,62 @@ package io.fairspace.saturn.controller.exception; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import io.fairspace.saturn.services.PayloadParsingException; +import io.fairspace.saturn.services.errors.ErrorDto; +import io.fairspace.saturn.services.metadata.validation.ValidationException; + +@Slf4j @ControllerAdvice public class GlobalExceptionHandler { - // todo: add tests + // // todo: add tests + // @ExceptionHandler(AccessDeniedException.class) + // public ResponseEntity handleAccessDenied(AccessDeniedException ex) { + // return new ResponseEntity<>("Access Denied: " + ex.getMessage(), HttpStatus.FORBIDDEN); + // } + + @ExceptionHandler(PayloadParsingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handlePayloadParsingException(PayloadParsingException ex, HttpServletRequest req) { + log.error("Malformed request body for request {} {}", req.getMethod(), req.getRequestURI(), ex); + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Malformed request body"); + } + + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleValidationException(ValidationException ex, HttpServletRequest req) { + log.error("Validation error for request {} {}", req.getMethod(), req.getRequestURI(), ex); + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", ex.getViolations()); + } + + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleIllegalArgumentException( + IllegalArgumentException ex, HttpServletRequest req) { + log.error("Validation error for request {} {}", req.getMethod(), req.getRequestURI(), ex); + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", ex.getMessage()); + } + @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity handleAccessDenied(AccessDeniedException ex) { - return new ResponseEntity<>("Access Denied: " + ex.getMessage(), HttpStatus.FORBIDDEN); + @ResponseStatus(HttpStatus.FORBIDDEN) + public ResponseEntity handleAccessDeniedException(AccessDeniedException ex, HttpServletRequest req) { + log.error("Access denied for request {} {}", req.getMethod(), req.getRequestURI(), ex); + return buildErrorResponse(HttpStatus.FORBIDDEN, "Access Denied"); + } + + private ResponseEntity buildErrorResponse(HttpStatus status, String message) { + return ResponseEntity.status(status).body(new ErrorDto(status.value(), message, null)); + } + + private ResponseEntity buildErrorResponse(HttpStatus status, String message, Object info) { + return ResponseEntity.status(status).body(new ErrorDto(status.value(), message, info)); } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/IriValidator.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/IriValidator.java new file mode 100644 index 0000000000..e6bc0371ce --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/IriValidator.java @@ -0,0 +1,30 @@ +package io.fairspace.saturn.controller.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.extern.slf4j.Slf4j; + +import static org.apache.jena.riot.system.Checker.checkIRI; + +/** + * Validates that a given IRI is valid. + */ +@Slf4j +public class IriValidator implements ConstraintValidator { + @Override + public boolean isValid(String subject, ConstraintValidatorContext context) { + try { + var isValid = subject == null || checkIRI(subject); + if (!isValid) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate( + String.format(context.getDefaultConstraintMessageTemplate(), subject)) + .addConstraintViolation(); + } + return isValid; + } catch (Exception e) { + log.error("Error validating IRI", e); + return false; + } + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/ValidIri.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/ValidIri.java new file mode 100644 index 0000000000..1e3609f211 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/ValidIri.java @@ -0,0 +1,21 @@ +package io.fairspace.saturn.controller.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = IriValidator.class) +public @interface ValidIri { + + String message() default "Invalid IRI: %s"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataApp.java deleted file mode 100644 index c9eb889b54..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataApp.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.fairspace.saturn.services.metadata; - -import lombok.extern.log4j.Log4j2; -import org.apache.jena.rdf.model.Model; -import spark.Request; - -import io.fairspace.saturn.services.AccessDeniedException; -import io.fairspace.saturn.services.BaseApp; -import io.fairspace.saturn.services.PayloadParsingException; -import io.fairspace.saturn.services.metadata.validation.ValidationException; - -import static io.fairspace.saturn.services.errors.ErrorHelper.errorBody; -import static io.fairspace.saturn.services.errors.ErrorHelper.exceptionHandler; -import static io.fairspace.saturn.services.metadata.Serialization.deserialize; -import static io.fairspace.saturn.services.metadata.Serialization.getFormat; -import static io.fairspace.saturn.services.metadata.Serialization.serialize; -import static io.fairspace.saturn.util.ValidationUtils.validate; -import static io.fairspace.saturn.util.ValidationUtils.validateIRI; - -import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT; -import static java.lang.Boolean.TRUE; -import static org.apache.jena.rdf.model.ResourceFactory.createResource; -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.delete; -import static spark.Spark.get; -import static spark.Spark.patch; -import static spark.Spark.put; - -@Log4j2 -public class MetadataApp extends BaseApp { - - private static final String DO_VIEWS_UPDATE = "doViewsUpdate"; - - protected final MetadataService api; - - public MetadataApp(String basePath, MetadataService api) { - super(basePath); - this.api = api; - } - - @Override - protected void initApp() { - get("/", (req, res) -> { - var model = getMetadata(req); - var format = getFormat(req.headers("Accept")); - res.type(format.getLang().getHeaderString()); - return serialize(model, format); - }); - - put("/", (req, res) -> { - var model = deserialize(req.body(), req.contentType()); - var doMaterializedViewsRefresh = req.queryParamOrDefault(DO_VIEWS_UPDATE, TRUE.toString()); - - api.put(model, Boolean.valueOf(doMaterializedViewsRefresh)); - - res.status(SC_NO_CONTENT); - return ""; - }); - patch("/", (req, res) -> { - var model = deserialize(req.body(), req.contentType()); - var doViewsUpdate = req.queryParamOrDefault(DO_VIEWS_UPDATE, TRUE.toString()); - api.patch(model, Boolean.valueOf(doViewsUpdate)); - - res.status(SC_NO_CONTENT); - return ""; - }); - delete("/", (req, res) -> { - if (req.queryParams("subject") != null) { - var subject = req.queryParams("subject"); - validate(subject != null, "Parameter \"subject\" is required"); - validateIRI(subject); - if (!api.softDelete(createResource(subject))) { - // Subject could not be deleted. Return a 404 error response - return null; - } - } else { - var model = deserialize(req.body(), req.contentType()); - var doMaterializedViewsRefresh = req.queryParamOrDefault(DO_VIEWS_UPDATE, TRUE.toString()); - api.delete(model, Boolean.valueOf(doMaterializedViewsRefresh)); - } - - res.status(SC_NO_CONTENT); - return ""; - }); - exception(PayloadParsingException.class, exceptionHandler(SC_BAD_REQUEST, "Malformed request body")); - exception(ValidationException.class, (e, req, res) -> { - log.error("400 Error handling request {} {}", req.requestMethod(), req.uri()); - e.getViolations().forEach(v -> log.error("{}", v)); - - res.type(APPLICATION_JSON.asString()); - res.status(SC_BAD_REQUEST); - res.body(errorBody(SC_BAD_REQUEST, "Validation Error", e.getViolations())); - }); - exception(AccessDeniedException.class, (e, req, res) -> { - log.error("401 Access denied {} {} {}", e.getMessage(), req.requestMethod(), req.uri()); - - res.type(APPLICATION_JSON.asString()); - res.status(SC_FORBIDDEN); - res.body(errorBody(SC_FORBIDDEN, "Access denied", e.getMessage())); - }); - } - - private Model getMetadata(Request req) { - return api.get(req.queryParams("subject"), req.queryParams().contains("withValueProperties")); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java new file mode 100644 index 0000000000..21cfe4058c --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java @@ -0,0 +1,129 @@ +package io.fairspace.saturn.controller; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.services.metadata.MetadataService; + +import static io.fairspace.saturn.controller.enums.CustomMediaType.TEXT_TURTLE; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(MetadataController.class) +public class MetadataControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private MetadataService metadataService; + + @BeforeEach + void setUp() { + when(getService().getMetadataService()).thenReturn(metadataService); + } + + @Test + public void testGetMetadata() throws Exception { + Model mockModel = ModelFactory.createDefaultModel(); // Create an empty Jena model for testing + mockModel.add( + mockModel.createResource("http://example.com"), + mockModel.createProperty("http://example.com/property"), + "test-value"); + Mockito.when(metadataService.get(eq("http://example.com"), eq(false))).thenReturn(mockModel); + + mockMvc.perform(get("/api/metadata/") + .param("subject", "http://example.com") + .param("withValueProperties", "false") + .header("Accept", TEXT_TURTLE)) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("http://example.com"))) + .andExpect(header().string("Content-Type", TEXT_TURTLE + ";charset=UTF-8")); + } + + @Test + public void testPutMetadata() throws Exception { + String body = + """ + @prefix ex: . + ex:subject ex:property "value" . + """; + + mockMvc.perform(put("/api/metadata/") + .content(body) + .contentType(TEXT_TURTLE) + .param("doViewsUpdate", "true")) + .andExpect(status().isNoContent()); + + Mockito.verify(metadataService).put(any(Model.class), eq(true)); + } + + @Test + public void testPatchMetadata() throws Exception { + String body = + """ + @prefix ex: . + ex:subject ex:property "updated-value" . + """; + + mockMvc.perform(patch("/api/metadata/") + .content(body) + .contentType(TEXT_TURTLE) + .param("doViewsUpdate", "false")) + .andExpect(status().isNoContent()); + + Mockito.verify(metadataService).patch(any(Model.class), eq(false)); + } + + @Test + public void testDeleteMetadataBySubject() throws Exception { + Mockito.when(metadataService.softDelete(any())).thenReturn(true); + + mockMvc.perform(delete("/api/metadata/").param("subject", "http://example.com")) + .andExpect(status().isNoContent()); + + Mockito.verify(metadataService).softDelete(any()); + } + + @Test + public void testDeleteMetadataByModel() throws Exception { + String body = + """ + @prefix ex: . + ex:subject ex:property "value" . + """; + + mockMvc.perform(delete("/api/metadata/") + .content(body) + .contentType(TEXT_TURTLE) + .param("doViewsUpdate", "true")) + .andExpect(status().isNoContent()); + + Mockito.verify(metadataService).delete(any(Model.class), eq(true)); + } + + @Test + public void testDeleteMetadataSubjectNotFound() throws Exception { + Mockito.when(metadataService.softDelete(any())).thenReturn(false); + + mockMvc.perform(delete("/api/metadata/").param("subject", "http://example.com")) + .andExpect(status().isBadRequest()); + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/validation/IriValidatorTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/validation/IriValidatorTest.java new file mode 100644 index 0000000000..a2d2887c11 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/validation/IriValidatorTest.java @@ -0,0 +1,56 @@ +package io.fairspace.saturn.controller.validation; + +import jakarta.validation.ConstraintValidatorContext; +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 static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class IriValidatorTest { + + @InjectMocks + private IriValidator iriValidator; + + @Mock + private ConstraintValidatorContext constraintValidatorContext; + + @Mock + private ConstraintValidatorContext.ConstraintViolationBuilder constraintViolationBuilder; + + @Test + void testValidIri() { + String validIri = "http://example.com/resource/123"; + + // Test that a valid IRI returns true + assertTrue(iriValidator.isValid(validIri, constraintValidatorContext)); + + // No violations should be added for valid IRI + verify(constraintValidatorContext, never()).buildConstraintViolationWithTemplate(anyString()); + } + + @Test + void testInvalidIri() { + String invalidIri = " fd "; + + // Set up mocking behavior for invalid IRI case + when(constraintValidatorContext.getDefaultConstraintMessageTemplate()).thenReturn("Invalid IRI: %s"); + when(constraintValidatorContext.buildConstraintViolationWithTemplate(anyString())) + .thenReturn(constraintViolationBuilder); + + // Test that an invalid IRI returns false + assertFalse(iriValidator.isValid(invalidIri, constraintValidatorContext)); + + // Verify that a violation was added + verify(constraintValidatorContext).disableDefaultConstraintViolation(); + verify(constraintValidatorContext).buildConstraintViolationWithTemplate(anyString()); + } +} From e070d9d6ee7db02b0088f78bd4b050e58de8841c Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 7 Oct 2024 23:00:33 +0200 Subject: [PATCH 05/18] FAIRSPC-81: migrated vocabulary API to spring mvc --- .../saturn/config/SparkFilterFactory.java | 2 -- .../saturn/controller/MetadataController.java | 8 +++-- .../controller/VocabularyController.java | 30 +++++++++++++++++++ .../services/metadata/VocabularyApp.java | 24 --------------- .../controller/VocabularyControllerTest.java | 25 ++++++++++++++++ 5 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/VocabularyApp.java create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/VocabularyControllerTest.java diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java index cf6ef08f8e..e2acff1618 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java @@ -4,7 +4,6 @@ import io.fairspace.saturn.config.properties.KeycloakClientProperties; import io.fairspace.saturn.services.features.FeaturesApp; import io.fairspace.saturn.services.maintenance.MaintenanceApp; -import io.fairspace.saturn.services.metadata.VocabularyApp; import io.fairspace.saturn.services.search.SearchApp; import io.fairspace.saturn.services.users.LogoutApp; import io.fairspace.saturn.services.users.UserApp; @@ -18,7 +17,6 @@ public static SaturnSparkFilter createSparkFilter( String publicUrl) { return new SaturnSparkFilter( new SearchApp(apiPathPrefix + "/search", svc.getSearchService(), svc.getFileSearchService()), - new VocabularyApp(apiPathPrefix + "/vocabulary"), new UserApp(apiPathPrefix + "/users", svc.getUserService()), new FeaturesApp(apiPathPrefix + "/features", featureProperties.getFeatures()), new MaintenanceApp(apiPathPrefix + "/maintenance", svc.getMaintenanceService()), diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java index 08510b37ea..f678e9a3b9 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java @@ -7,6 +7,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -43,13 +44,14 @@ public class MetadataController { private final Services services; @GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) - public String getMetadata( + public ResponseEntity getMetadata( @RequestParam(required = false) String subject, @RequestParam(name = "withValueProperties", defaultValue = "false") boolean withValueProperties, @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader) { - Model model = services.getMetadataService().get(subject, withValueProperties); + var model = services.getMetadataService().get(subject, withValueProperties); var format = getFormat(acceptHeader); - return serialize(model, format); + var metadata = serialize(model, format); + return ResponseEntity.ok(metadata); } @PutMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java new file mode 100644 index 0000000000..a0eb1c61df --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java @@ -0,0 +1,30 @@ +package io.fairspace.saturn.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static io.fairspace.saturn.services.metadata.Serialization.getFormat; +import static io.fairspace.saturn.services.metadata.Serialization.serialize; +import static io.fairspace.saturn.vocabulary.Vocabularies.VOCABULARY; + +@RestController +@RequestMapping("/api/vocabulary/") +public class VocabularyController { + + @GetMapping + public ResponseEntity getVocabulary( + @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader) { + var format = getFormat(acceptHeader); + var contentType = format.getLang().getHeaderString(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType(contentType)); + var vocabulary = serialize(VOCABULARY, format); + return new ResponseEntity<>(vocabulary, headers, HttpStatus.OK); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/VocabularyApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/VocabularyApp.java deleted file mode 100644 index 9585f46a1a..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/VocabularyApp.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.fairspace.saturn.services.metadata; - -import io.fairspace.saturn.services.BaseApp; - -import static io.fairspace.saturn.services.metadata.Serialization.getFormat; -import static io.fairspace.saturn.services.metadata.Serialization.serialize; -import static io.fairspace.saturn.vocabulary.Vocabularies.VOCABULARY; - -import static spark.Spark.get; - -public class VocabularyApp extends BaseApp { - public VocabularyApp(String basePath) { - super(basePath); - } - - @Override - protected void initApp() { - get("/", (req, res) -> { - var format = getFormat(req.headers("Accept")); - res.type(format.getLang().getHeaderString()); - return serialize(VOCABULARY, format); - }); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/VocabularyControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/VocabularyControllerTest.java new file mode 100644 index 0000000000..6945fae945 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/VocabularyControllerTest.java @@ -0,0 +1,25 @@ +package io.fairspace.saturn.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(VocabularyController.class) +class VocabularyControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void testGetVocabularyWithJsonLd() throws Exception { + mockMvc.perform(get("/api/vocabulary/").header(HttpHeaders.ACCEPT, "application/ld+json")) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, "application/ld+json")); + } +} From 0e2a77bca950bb9e3e6966a2e11ae119ced1e12d Mon Sep 17 00:00:00 2001 From: anton Date: Tue, 8 Oct 2024 12:21:05 +0200 Subject: [PATCH 06/18] FAIRSPC-81: migrated features endpoint to spring mvc --- .../saturn/config/SparkFilterConfig.java | 4 +- .../saturn/config/SparkFilterFactory.java | 9 +--- .../saturn/controller/FeaturesController.java | 25 +++++++++++ .../saturn/services/features/FeaturesApp.java | 26 ------------ .../saturn/config/SparkFilterFactoryTest.java | 4 +- .../controller/FeaturesControllerTest.java | 41 +++++++++++++++++++ 6 files changed, 69 insertions(+), 40 deletions(-) create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/features/FeaturesApp.java create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterConfig.java index 5bbed0bc78..e23d9bcb15 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterConfig.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterConfig.java @@ -8,7 +8,6 @@ import org.springframework.context.annotation.Configuration; import spark.servlet.SparkFilter; -import io.fairspace.saturn.config.properties.FeatureProperties; import io.fairspace.saturn.config.properties.KeycloakClientProperties; import static io.fairspace.saturn.config.SparkFilterFactory.createSparkFilter; @@ -23,11 +22,10 @@ public class SparkFilterConfig { @Bean public FilterRegistrationBean sparkFilter( Services svc, - FeatureProperties featureProperties, KeycloakClientProperties keycloakClientProperties, @Value("${application.publicUrl}") String publicUrl) { var registrationBean = new FilterRegistrationBean(); - var sparkFilter = createSparkFilter("/api", svc, keycloakClientProperties, featureProperties, publicUrl); + var sparkFilter = createSparkFilter("/api", svc, keycloakClientProperties, publicUrl); registrationBean.setFilter(sparkFilter); // we cannot set /api/* as the url pattern, because it would override /api/webdav/* // endpoints which defined as a separate servlet diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java index e2acff1618..bf31a5254a 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java @@ -1,8 +1,6 @@ package io.fairspace.saturn.config; -import io.fairspace.saturn.config.properties.FeatureProperties; import io.fairspace.saturn.config.properties.KeycloakClientProperties; -import io.fairspace.saturn.services.features.FeaturesApp; import io.fairspace.saturn.services.maintenance.MaintenanceApp; import io.fairspace.saturn.services.search.SearchApp; import io.fairspace.saturn.services.users.LogoutApp; @@ -10,15 +8,10 @@ public class SparkFilterFactory { public static SaturnSparkFilter createSparkFilter( - String apiPathPrefix, - Services svc, - KeycloakClientProperties keycloakClientProperties, - FeatureProperties featureProperties, - String publicUrl) { + String apiPathPrefix, Services svc, KeycloakClientProperties keycloakClientProperties, String publicUrl) { return new SaturnSparkFilter( new SearchApp(apiPathPrefix + "/search", svc.getSearchService(), svc.getFileSearchService()), new UserApp(apiPathPrefix + "/users", svc.getUserService()), - new FeaturesApp(apiPathPrefix + "/features", featureProperties.getFeatures()), new MaintenanceApp(apiPathPrefix + "/maintenance", svc.getMaintenanceService()), new LogoutApp("/logout", svc.getUserService(), keycloakClientProperties, publicUrl)); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java new file mode 100644 index 0000000000..6afe60dd9e --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java @@ -0,0 +1,25 @@ +package io.fairspace.saturn.controller; + +import java.util.Set; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.config.Feature; +import io.fairspace.saturn.config.properties.FeatureProperties; + +@RestController +@RequestMapping("/api/features/") +@RequiredArgsConstructor +public class FeaturesController { + + private final FeatureProperties featureProperties; + + @GetMapping + public ResponseEntity> getFeatures() { + return ResponseEntity.ok(featureProperties.getFeatures()); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/features/FeaturesApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/features/FeaturesApp.java deleted file mode 100644 index b6aa9b50eb..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/features/FeaturesApp.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.fairspace.saturn.services.features; - -import java.util.Set; - -import io.fairspace.saturn.config.Feature; -import io.fairspace.saturn.services.BaseApp; - -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.get; - -public class FeaturesApp extends BaseApp { - private final Set features; - - public FeaturesApp(String basePath, Set features) { - super(basePath); - this.features = features; - } - - @Override - protected void initApp() { - get("/", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(features); - }); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/config/SparkFilterFactoryTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/config/SparkFilterFactoryTest.java index 0e638c13a1..4f226ad6c0 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/config/SparkFilterFactoryTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/config/SparkFilterFactoryTest.java @@ -5,7 +5,6 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import io.fairspace.saturn.config.properties.FeatureProperties; import io.fairspace.saturn.config.properties.KeycloakClientProperties; import static org.junit.Assert.assertNotNull; @@ -17,7 +16,6 @@ public class SparkFilterFactoryTest { @Test public void itCreatesAFilter() { - assertNotNull(SparkFilterFactory.createSparkFilter( - "/some/path", svc, new KeycloakClientProperties(), new FeatureProperties(), "")); + assertNotNull(SparkFilterFactory.createSparkFilter("/some/path", svc, new KeycloakClientProperties(), "")); } } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java new file mode 100644 index 0000000000..33145c1ae3 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java @@ -0,0 +1,41 @@ +package io.fairspace.saturn.controller; + +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.config.Feature; +import io.fairspace.saturn.config.properties.FeatureProperties; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(FeaturesController.class) +class FeaturesControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private FeatureProperties featureProperties; + + @Test + void testGetFeatures() throws Exception { + // Mock the response from featureProperties + Set features = Set.of(Feature.ExtraStorage); + when(featureProperties.getFeatures()).thenReturn(features); + + // Perform GET request and verify the response + mockMvc.perform(get("/api/features/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[\"ExtraStorage\"]")); + } +} From 1351e93ce390cb99a8a11636c58c81eafc991646 Mon Sep 17 00:00:00 2001 From: anton Date: Tue, 8 Oct 2024 12:55:23 +0200 Subject: [PATCH 07/18] FAIRSPC-81: migrated maintain endpoint to spring mvc --- .../saturn/config/SparkFilterFactory.java | 2 - .../controller/MaintenanceController.java | 36 ++++++++++ .../services/maintenance/MaintenanceApp.java | 35 --------- .../src/main/resources/application.yaml | 2 +- .../controller/MaintenanceControllerTest.java | 72 +++++++++++++++++++ 5 files changed, 109 insertions(+), 38 deletions(-) create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceApp.java create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/MaintenanceControllerTest.java diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java index bf31a5254a..4d77953339 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java @@ -1,7 +1,6 @@ package io.fairspace.saturn.config; import io.fairspace.saturn.config.properties.KeycloakClientProperties; -import io.fairspace.saturn.services.maintenance.MaintenanceApp; import io.fairspace.saturn.services.search.SearchApp; import io.fairspace.saturn.services.users.LogoutApp; import io.fairspace.saturn.services.users.UserApp; @@ -12,7 +11,6 @@ public static SaturnSparkFilter createSparkFilter( return new SaturnSparkFilter( new SearchApp(apiPathPrefix + "/search", svc.getSearchService(), svc.getFileSearchService()), new UserApp(apiPathPrefix + "/users", svc.getUserService()), - new MaintenanceApp(apiPathPrefix + "/maintenance", svc.getMaintenanceService()), new LogoutApp("/logout", svc.getUserService(), keycloakClientProperties, publicUrl)); } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java new file mode 100644 index 0000000000..60a90545aa --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java @@ -0,0 +1,36 @@ +package io.fairspace.saturn.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.config.Services; + +@RestController +@RequestMapping("/api/maintenance") +@RequiredArgsConstructor +public class MaintenanceController { + + private final Services services; + + @PostMapping("/reindex") + public ResponseEntity startReindex() { + services.getMaintenanceService().startRecreateIndexTask(); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/compact") + public ResponseEntity compactRdfStorage() { + services.getMaintenanceService().compactRdfStorageTask(); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/status") + public ResponseEntity getStatus() { + var status = services.getMaintenanceService().active() ? "active" : "inactive"; + return ResponseEntity.ok(status); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceApp.java deleted file mode 100644 index 7f646ee865..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceApp.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.fairspace.saturn.services.maintenance; - -import io.fairspace.saturn.services.BaseApp; - -import static jakarta.servlet.http.HttpServletResponse.*; -import static spark.Spark.get; -import static spark.Spark.post; - -public class MaintenanceApp extends BaseApp { - private final MaintenanceService maintenanceService; - - public MaintenanceApp(String basePath, MaintenanceService maintenanceService) { - super(basePath); - - this.maintenanceService = maintenanceService; - } - - @Override - protected void initApp() { - post("/reindex", (req, res) -> { - maintenanceService.startRecreateIndexTask(); - res.status(SC_NO_CONTENT); - return ""; - }); - post("/compact", (req, res) -> { - maintenanceService.compactRdfStorageTask(); - res.status(SC_NO_CONTENT); - return ""; - }); - get("/status", (req, res) -> { - res.status(SC_OK); - return maintenanceService.active() ? "active" : "inactive"; - }); - } -} diff --git a/projects/saturn/src/main/resources/application.yaml b/projects/saturn/src/main/resources/application.yaml index 368506cfb5..adcd110a8d 100644 --- a/projects/saturn/src/main/resources/application.yaml +++ b/projects/saturn/src/main/resources/application.yaml @@ -74,7 +74,7 @@ application: features: - ExtraStorage view-database: - enabled: ${VIEW_DATABASE_ENABLED:false} + enabled: ${VIEW_DATABASE_ENABLED:true} url: ${VIEW_DATABASE_URL:jdbc:postgresql://localhost:9432/fairspace} username: ${VIEW_DATABASE_USERNAME:fairspace} autoCommitEnabled: ${VIEW_DATABASE_AUTO_COMMIT:false} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/MaintenanceControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MaintenanceControllerTest.java new file mode 100644 index 0000000000..ad0b2af049 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MaintenanceControllerTest.java @@ -0,0 +1,72 @@ +package io.fairspace.saturn.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.services.maintenance.MaintenanceService; + +import static org.mockito.Mockito.doNothing; +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.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(MaintenanceController.class) +class MaintenanceControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private MaintenanceService maintenanceService; + + @BeforeEach + public void setUp() { + when(getService().getMaintenanceService()).thenReturn(maintenanceService); + } + + @Test + void testStartReindex() throws Exception { + doNothing().when(maintenanceService).startRecreateIndexTask(); + + mockMvc.perform(post("/api/maintenance/reindex").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); // Expect 204 No Content + verify(maintenanceService).startRecreateIndexTask(); + } + + @Test + void testCompactRdfStorage() throws Exception { + doNothing().when(maintenanceService).compactRdfStorageTask(); + + mockMvc.perform(post("/api/maintenance/compact").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); // Expect 204 No Content + verify(maintenanceService).compactRdfStorageTask(); + } + + @Test + void testGetStatusActive() throws Exception { + when(maintenanceService.active()).thenReturn(true); + + mockMvc.perform(get("/api/maintenance/status").accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) // Expect 200 OK + .andExpect(content().string("active")); // Expect content "active" + verify(maintenanceService).active(); + } + + @Test + void testGetStatusInactive() throws Exception { + when(maintenanceService.active()).thenReturn(false); + + mockMvc.perform(get("/api/maintenance/status").accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) // Expect 200 OK + .andExpect(content().string("inactive")); // Expect content "inactive" + verify(maintenanceService).active(); + } +} From e34489160b828af42bb888b810c0d2f6f58f34dd Mon Sep 17 00:00:00 2001 From: anton Date: Tue, 8 Oct 2024 13:14:23 +0200 Subject: [PATCH 08/18] FAIRSPC-81: migrated user endpoints to spring mvc --- .../saturn/config/SparkFilterFactory.java | 2 - .../saturn/controller/UserController.java | 36 +++++ .../saturn/services/users/UserApp.java | 36 ----- .../saturn/controller/UserControllerTest.java | 148 ++++++++++++++++++ 4 files changed, 184 insertions(+), 38 deletions(-) create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/users/UserApp.java create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java index 4d77953339..0c2abc22dd 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java @@ -3,14 +3,12 @@ import io.fairspace.saturn.config.properties.KeycloakClientProperties; import io.fairspace.saturn.services.search.SearchApp; import io.fairspace.saturn.services.users.LogoutApp; -import io.fairspace.saturn.services.users.UserApp; public class SparkFilterFactory { public static SaturnSparkFilter createSparkFilter( String apiPathPrefix, Services svc, KeycloakClientProperties keycloakClientProperties, String publicUrl) { return new SaturnSparkFilter( new SearchApp(apiPathPrefix + "/search", svc.getSearchService(), svc.getFileSearchService()), - new UserApp(apiPathPrefix + "/users", svc.getUserService()), new LogoutApp("/logout", svc.getUserService(), keycloakClientProperties, publicUrl)); } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java new file mode 100644 index 0000000000..d570ff7abf --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java @@ -0,0 +1,36 @@ +package io.fairspace.saturn.controller; + +import java.util.Collection; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import io.fairspace.saturn.services.users.User; +import io.fairspace.saturn.services.users.UserRolesUpdate; +import io.fairspace.saturn.services.users.UserService; + +@RestController +@RequestMapping("/api/users/") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @GetMapping + public ResponseEntity> getUsers() { + return ResponseEntity.ok(userService.getUsers()); + } + + @PatchMapping + public ResponseEntity updateUserRoles(@RequestBody UserRolesUpdate update) { + userService.update(update); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/current") + public ResponseEntity getCurrentUser() { + var currentUser = userService.currentUser(); + return ResponseEntity.ok(currentUser); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/users/UserApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/users/UserApp.java deleted file mode 100644 index 8f75b57cb3..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/users/UserApp.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.fairspace.saturn.services.users; - -import io.fairspace.saturn.services.BaseApp; - -import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT; -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.*; - -public class UserApp extends BaseApp { - private final UserService service; - - public UserApp(String basePath, UserService service) { - super(basePath); - this.service = service; - } - - @Override - protected void initApp() { - get("/", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(service.getUsers()); - }); - - patch("/", (req, res) -> { - service.update(mapper.readValue(req.body(), UserRolesUpdate.class)); - res.status(SC_NO_CONTENT); - return ""; - }); - - get("/current", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - var user = service.currentUser(); - return mapper.writeValueAsString(user); - }); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java new file mode 100644 index 0000000000..8fa9669209 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java @@ -0,0 +1,148 @@ +package io.fairspace.saturn.controller; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.services.users.User; +import io.fairspace.saturn.services.users.UserRolesUpdate; +import io.fairspace.saturn.services.users.UserService; + +import static org.mockito.Mockito.doNothing; +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.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserController.class) +class UserControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService service; + + @Test + void testGetUsers() throws Exception { + var user1 = createTestUser("1", "User One", "user1@example.com", "user1", true, false); + var user2 = createTestUser("2", "User Two", "user2@example.com", "user2", false, true); + var users = List.of(user1, user2); + when(service.getUsers()).thenReturn(users); + + mockMvc.perform(get("/api/users/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) // Expect 200 OK + .andExpect( + content() + .json( + """ + [ + { + "id": "1", + "name": "User One", + "email": "user1@example.com", + "username": "user1", + "isSuperadmin": true, + "isAdmin": false, + "canViewPublicMetadata": true, + "canViewPublicData": false, + "canAddSharedMetadata": true, + "canQueryMetadata": true + }, + { + "id": "2", + "name": "User Two", + "email": "user2@example.com", + "username": "user2", + "isSuperadmin": false, + "isAdmin": true, + "canViewPublicMetadata": true, + "canViewPublicData": false, + "canAddSharedMetadata": true, + "canQueryMetadata": true + } + ] + """)); + } + + @Test + void testUpdateUserRoles() throws Exception { + UserRolesUpdate update = new UserRolesUpdate(); + update.setId("1"); + update.setAdmin(true); + update.setCanViewPublicMetadata(true); + update.setCanViewPublicData(false); + update.setCanAddSharedMetadata(true); + update.setCanQueryMetadata(false); + + doNothing().when(service).update(update); + + mockMvc.perform( + patch("/api/users/") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "id": "1", + "isAdmin": true, + "canViewPublicMetadata": true, + "canViewPublicData": false, + "canAddSharedMetadata": true, + "canQueryMetadata": false + } + """)) + .andExpect(status().isNoContent()); + } + + @Test + void testGetCurrentUser() throws Exception { + // Create a test user for the current user endpoint + var currentUser = createTestUser("1", "Current User", "currentuser@example.com", "currentuser", true, true); + + // Mock service behavior to return the current user + when(service.currentUser()).thenReturn(currentUser); + + // Perform GET request to /api/users/current + mockMvc.perform(get("/api/users/current").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) // Expect 200 OK + .andExpect( + content() + .json( + """ + { + "id": "1", + "name": "Current User", + "email": "currentuser@example.com", + "username": "currentuser", + "isSuperadmin": true, + "isAdmin": true, + "canViewPublicMetadata": true, + "canViewPublicData": false, + "canAddSharedMetadata": true, + "canQueryMetadata": true + } + """)); + } + + private User createTestUser( + String id, String name, String email, String username, boolean superadmin, boolean admin) { + User user = new User(); + user.setId(id); + user.setName(name); + user.setEmail(email); + user.setUsername(username); + user.setSuperadmin(superadmin); + user.setAdmin(admin); + user.setCanViewPublicMetadata(true); + user.setCanViewPublicData(false); + user.setCanAddSharedMetadata(true); + user.setCanQueryMetadata(true); + return user; + } +} From b81b005b074c3ff666fea81a93a365f8cc46e966 Mon Sep 17 00:00:00 2001 From: anton Date: Wed, 9 Oct 2024 18:38:11 +0200 Subject: [PATCH 09/18] FAIRSPC-82: migrated search endpoints to spring mvc --- .../saturn/config/SparkFilterFactory.java | 2 - .../saturn/controller/SearchController.java | 37 +++++ .../dto/SearchResultDto.java} | 4 +- .../controller/dto/SearchResultsDto.java | 13 ++ .../dto/request}/FileSearchRequest.java | 4 +- .../dto/request}/LookupSearchRequest.java | 2 +- .../dto/request}/SearchRequest.java | 2 +- .../io/fairspace/saturn/rdf/SparqlUtils.java | 8 +- .../services/search/FileSearchService.java | 5 +- .../search/JdbcFileSearchService.java | 4 +- .../saturn/services/search/SearchApp.java | 40 ------ .../services/search/SearchResultsDTO.java | 13 -- .../saturn/services/search/SearchService.java | 11 +- .../search/SparqlFileSearchService.java | 4 +- .../services/views/ViewStoreReader.java | 12 +- .../controller/SearchControllerTest.java | 127 ++++++++++++++++++ .../search/JdbcFileSearchServiceTest.java | 1 + .../search/SparqlFileSearchServiceTest.java | 1 + 18 files changed, 211 insertions(+), 79 deletions(-) create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java rename projects/saturn/src/main/java/io/fairspace/saturn/{services/search/SearchResultDTO.java => controller/dto/SearchResultDto.java} (69%) create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java rename projects/saturn/src/main/java/io/fairspace/saturn/{services/search => controller/dto/request}/FileSearchRequest.java (58%) rename projects/saturn/src/main/java/io/fairspace/saturn/{services/search => controller/dto/request}/LookupSearchRequest.java (74%) rename projects/saturn/src/main/java/io/fairspace/saturn/{services/search => controller/dto/request}/SearchRequest.java (69%) delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchApp.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultsDTO.java create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/SearchControllerTest.java diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java index 0c2abc22dd..35e33040bf 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java @@ -1,14 +1,12 @@ package io.fairspace.saturn.config; import io.fairspace.saturn.config.properties.KeycloakClientProperties; -import io.fairspace.saturn.services.search.SearchApp; import io.fairspace.saturn.services.users.LogoutApp; public class SparkFilterFactory { public static SaturnSparkFilter createSparkFilter( String apiPathPrefix, Services svc, KeycloakClientProperties keycloakClientProperties, String publicUrl) { return new SaturnSparkFilter( - new SearchApp(apiPathPrefix + "/search", svc.getSearchService(), svc.getFileSearchService()), new LogoutApp("/logout", svc.getUserService(), keycloakClientProperties, publicUrl)); } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java new file mode 100644 index 0000000000..5c68b0f7cd --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java @@ -0,0 +1,37 @@ +package io.fairspace.saturn.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.config.Services; +import io.fairspace.saturn.controller.dto.SearchResultsDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; +import io.fairspace.saturn.controller.dto.request.LookupSearchRequest; + +@RestController +@RequestMapping("/api/search") +@RequiredArgsConstructor +public class SearchController { + + private final Services services; + + @PostMapping(value = "/files") + public ResponseEntity searchFiles(@RequestBody FileSearchRequest request) { + var searchResult = services.getFileSearchService().searchFiles(request); + var resultDto = SearchResultsDto.builder() + .results(searchResult) + .query(request.getQuery()) + .build(); + return ResponseEntity.ok(resultDto); + } + + @PostMapping(value = "/lookup") + public ResponseEntity lookupSearch(@RequestBody LookupSearchRequest request) { + var results = services.getSearchService().getLookupSearchResults(request); + return ResponseEntity.ok(results); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java similarity index 69% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultDTO.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java index e8bdcfe426..c0dda45561 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultDTO.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java @@ -1,4 +1,4 @@ -package io.fairspace.saturn.services.search; +package io.fairspace.saturn.controller.dto; import lombok.Builder; import lombok.NonNull; @@ -6,7 +6,7 @@ @Value @Builder -public class SearchResultDTO { +public class SearchResultDto { @NonNull String id; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java new file mode 100644 index 0000000000..57850e1ee1 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java @@ -0,0 +1,13 @@ +package io.fairspace.saturn.controller.dto; + +import java.util.List; + +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class SearchResultsDto { + List results; + String query; +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchRequest.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/FileSearchRequest.java similarity index 58% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchRequest.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/FileSearchRequest.java index 095a659e63..fbac7e815c 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchRequest.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/FileSearchRequest.java @@ -1,12 +1,10 @@ -package io.fairspace.saturn.services.search; +package io.fairspace.saturn.controller.dto.request; -import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; @Getter @Setter public class FileSearchRequest extends SearchRequest { - @NotBlank private String parentIRI; } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/LookupSearchRequest.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/LookupSearchRequest.java similarity index 74% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/search/LookupSearchRequest.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/LookupSearchRequest.java index 2f56fad35b..269a4171ed 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/LookupSearchRequest.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/LookupSearchRequest.java @@ -1,4 +1,4 @@ -package io.fairspace.saturn.services.search; +package io.fairspace.saturn.controller.dto.request; import lombok.Getter; import lombok.Setter; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchRequest.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/SearchRequest.java similarity index 69% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchRequest.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/SearchRequest.java index 9a1c5493dd..42f67e9c1c 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchRequest.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/SearchRequest.java @@ -1,4 +1,4 @@ -package io.fairspace.saturn.services.search; +package io.fairspace.saturn.controller.dto.request; import lombok.Getter; import lombok.Setter; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SparqlUtils.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SparqlUtils.java index a8d69ad307..f3975d9579 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SparqlUtils.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SparqlUtils.java @@ -25,7 +25,7 @@ import org.apache.jena.update.UpdateFactory; import io.fairspace.saturn.config.properties.JenaProperties; -import io.fairspace.saturn.services.search.SearchResultDTO; +import io.fairspace.saturn.controller.dto.SearchResultDto; import static java.util.Optional.ofNullable; import static java.util.UUID.randomUUID; @@ -84,10 +84,10 @@ public static String getQueryRegex(String query) { .replace("/\\/g", "\\\\"); } - public static List getByQuery(Query query, QuerySolutionMap binding, Dataset dataset) { + public static List getByQuery(Query query, QuerySolutionMap binding, Dataset dataset) { log.debug("Executing query:\n{}", query); try (var selectExecution = QueryExecutionFactory.create(query, dataset, binding)) { - var results = new ArrayList(); + var results = new ArrayList(); return calculateRead(dataset, () -> { try (selectExecution) { @@ -101,7 +101,7 @@ public static List getByQuery(Query query, QuerySolutionMap bin .map(Literal::getString) .orElse(null); - var dto = SearchResultDTO.builder() + var dto = SearchResultDto.builder() .id(id) .label(label) .type(type) diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchService.java index 818cf2ad1c..2baa761878 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchService.java @@ -2,6 +2,9 @@ import java.util.List; +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; + public interface FileSearchService { - List searchFiles(FileSearchRequest request); + List searchFiles(FileSearchRequest request); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/JdbcFileSearchService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/JdbcFileSearchService.java index 5f1339faca..be26d3d69c 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/JdbcFileSearchService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/JdbcFileSearchService.java @@ -9,6 +9,8 @@ import io.fairspace.saturn.config.ViewsConfig; import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; import io.fairspace.saturn.rdf.transactions.Transactions; import io.fairspace.saturn.services.views.ViewStoreClientFactory; import io.fairspace.saturn.services.views.ViewStoreReader; @@ -37,7 +39,7 @@ public JdbcFileSearchService( } @SneakyThrows - public List searchFiles(FileSearchRequest request) { + public List searchFiles(FileSearchRequest request) { var collectionsForUser = transactions.calculateRead(m -> rootSubject.getChildren().stream() .map(collection -> getCollectionNameByUri(rootSubject.getUniqueId(), collection.getUniqueId())) .collect(Collectors.toList())); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchApp.java deleted file mode 100644 index 1b7030d9d7..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchApp.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.fairspace.saturn.services.search; - -import io.fairspace.saturn.services.BaseApp; - -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.post; - -public class SearchApp extends BaseApp { - private final SearchService searchService; - private final FileSearchService fileSearchService; - - public SearchApp(String basePath, SearchService searchService, FileSearchService fileSearchService) { - super(basePath); - this.searchService = searchService; - this.fileSearchService = fileSearchService; - } - - @Override - protected void initApp() { - post("/files", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - var request = mapper.readValue(req.body(), FileSearchRequest.class); - var searchResult = fileSearchService.searchFiles(request); - - SearchResultsDTO resultDto = SearchResultsDTO.builder() - .results(searchResult) - .query(request.getQuery()) - .build(); - - return mapper.writeValueAsString(resultDto); - }); - - post("/lookup", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - var request = mapper.readValue(req.body(), LookupSearchRequest.class); - var results = searchService.getLookupSearchResults(request); - return mapper.writeValueAsString(results); - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultsDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultsDTO.java deleted file mode 100644 index 686789ad71..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultsDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.fairspace.saturn.services.search; - -import java.util.List; - -import lombok.Builder; -import lombok.Value; - -@Value -@Builder -public class SearchResultsDTO { - List results; - String query; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchService.java index 5d525233a6..0cb9831681 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchService.java @@ -5,6 +5,9 @@ import lombok.extern.log4j.*; import org.apache.jena.query.*; +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.SearchResultsDto; +import io.fairspace.saturn.controller.dto.request.LookupSearchRequest; import io.fairspace.saturn.rdf.SparqlUtils; import io.fairspace.saturn.vocabulary.FS; @@ -50,20 +53,20 @@ public SearchService(Dataset ds) { this.ds = ds; } - public SearchResultsDTO getLookupSearchResults(LookupSearchRequest request) { - return SearchResultsDTO.builder() + public SearchResultsDto getLookupSearchResults(LookupSearchRequest request) { + return SearchResultsDto.builder() .results(getResourceByText(request)) .query(request.getQuery()) .build(); } - private List getResourceByText(LookupSearchRequest request) { + private List getResourceByText(LookupSearchRequest request) { var binding = new QuerySolutionMap(); binding.add("query", createStringLiteral(request.getQuery())); binding.add("type", createResource(request.getResourceType())); var results = SparqlUtils.getByQuery(RESOURCE_BY_TEXT_EXACT_MATCH_QUERY, binding, ds); - if (results.size() > 0) { + if (!results.isEmpty()) { return results; } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SparqlFileSearchService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SparqlFileSearchService.java index a38ada61e6..3c05a16b4a 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SparqlFileSearchService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SparqlFileSearchService.java @@ -8,6 +8,8 @@ import org.apache.jena.query.QueryFactory; import org.apache.jena.query.QuerySolutionMap; +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; import io.fairspace.saturn.rdf.SparqlUtils; import io.fairspace.saturn.vocabulary.FS; @@ -23,7 +25,7 @@ public SparqlFileSearchService(Dataset ds) { this.ds = ds; } - public List searchFiles(FileSearchRequest request) { + public List searchFiles(FileSearchRequest request) { var query = getSearchForFilesQuery(request.getParentIRI()); var binding = new QuerySolutionMap(); binding.add("regexQuery", createStringLiteral(SparqlUtils.getQueryRegex(request.getQuery()))); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java index 986ede449d..d1e50ff979 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java @@ -30,9 +30,9 @@ import io.fairspace.saturn.config.ViewsConfig.ColumnType; import io.fairspace.saturn.config.ViewsConfig.View; import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.controller.dto.SearchResultDto; import io.fairspace.saturn.controller.dto.ValueDto; -import io.fairspace.saturn.services.search.FileSearchRequest; -import io.fairspace.saturn.services.search.SearchResultDTO; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; import io.fairspace.saturn.vocabulary.FS; import static io.fairspace.saturn.config.ViewsConfig.ColumnType.Date; @@ -611,7 +611,7 @@ public long countRows(String view, List filters) throws SQLTimeoutEx } } - public List searchFiles(FileSearchRequest request, List userCollections) { + public List searchFiles(FileSearchRequest request, List userCollections) { if (userCollections == null || userCollections.isEmpty()) { return Collections.emptyList(); } @@ -654,10 +654,10 @@ public List searchFiles(FileSearchRequest request, List } @SneakyThrows - private List convertResult(ResultSet resultSet) { - var rows = new ArrayList(); + private List convertResult(ResultSet resultSet) { + var rows = new ArrayList(); while (resultSet.next()) { - var row = SearchResultDTO.builder() + var row = SearchResultDto.builder() .id(resultSet.getString("id")) .label(resultSet.getString("label")) .type(FS.NS + resultSet.getString("type")) diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/SearchControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/SearchControllerTest.java new file mode 100644 index 0000000000..23fbe976b4 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/SearchControllerTest.java @@ -0,0 +1,127 @@ +package io.fairspace.saturn.controller; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.SearchResultsDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; +import io.fairspace.saturn.controller.dto.request.LookupSearchRequest; +import io.fairspace.saturn.services.search.FileSearchService; +import io.fairspace.saturn.services.search.SearchService; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(SearchController.class) +class SearchControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private SearchService searchService; + + @MockBean + private FileSearchService fileSearchService; + + @Test + void testSearchFiles() throws Exception { + when(getService().getFileSearchService()).thenReturn(fileSearchService); + var mockResults = List.of( + SearchResultDto.builder() + .id("file1.txt") + .label("File 1") + .type("text") + .comment("First file") + .build(), + SearchResultDto.builder() + .id("file2.txt") + .label("File 2") + .type("text") + .comment("Second file") + .build()); + when(fileSearchService.searchFiles(any(FileSearchRequest.class))).thenReturn(mockResults); + + mockMvc.perform( + post("/api/search/files") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "query": "test query", + "parentIRI": "parent/iri" + } + """)) + .andExpect(status().isOk()) + .andExpect( + content() + .json( + """ + { + "results": [ + {"id": "file1.txt", "label": "File 1", "type": "text", "comment": "First file"}, + {"id": "file2.txt", "label": "File 2", "type": "text", "comment": "Second file"} + ], + "query": "test query" + } + """)); + } + + @Test + void testLookupSearch() throws Exception { + when(getService().getSearchService()).thenReturn(searchService); + var mockResults = List.of( + SearchResultDto.builder() + .id("file1.txt") + .label("File 1") + .type("text") + .comment("First file") + .build(), + SearchResultDto.builder() + .id("file2.txt") + .label("File 2") + .type("text") + .comment("Second file") + .build()); + var resultsDTO = SearchResultsDto.builder() + .results(mockResults) + .query("test query") + .build(); + when(searchService.getLookupSearchResults(any(LookupSearchRequest.class))) + .thenReturn(resultsDTO); + + mockMvc.perform( + post("/api/search/lookup") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "query": "lookup query", + "resourceType": "resourceType" + } + """)) + .andExpect(status().isOk()) // Expect 200 OK + .andExpect( + content() + .json( + """ + { + "results": [ + {"id": "file1.txt", "label": "File 1", "type": "text", "comment": "First file"}, + {"id": "file2.txt", "label": "File 2", "type": "text", "comment": "Second file"} + ], + "query": "test query" + } + """)); // Verify JSON response + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java index 81949d16a5..7a36d3b45f 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java @@ -24,6 +24,7 @@ import io.fairspace.saturn.config.properties.CacheProperties; import io.fairspace.saturn.config.properties.JenaProperties; import io.fairspace.saturn.config.properties.WebDavProperties; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.transactions.SimpleTransactions; import io.fairspace.saturn.rdf.transactions.Transactions; diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/search/SparqlFileSearchServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/search/SparqlFileSearchServiceTest.java index 51af9be5c8..1af7d5719f 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/search/SparqlFileSearchServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/search/SparqlFileSearchServiceTest.java @@ -20,6 +20,7 @@ import org.mockito.junit.MockitoJUnitRunner; import io.fairspace.saturn.config.properties.WebDavProperties; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.search.FilteredDatasetGraph; import io.fairspace.saturn.rdf.transactions.SimpleTransactions; From 17e751441296040ddd85f3bda9832bfe50ad9533 Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 10 Oct 2024 14:43:49 +0200 Subject: [PATCH 10/18] FAIRSPC-81: got rid of java-spark framework and Jetty --- projects/saturn/build.gradle | 1 - .../saturn/config/SaturnSparkFilter.java | 28 ------- .../saturn/config/SparkFilterConfig.java | 33 -------- .../saturn/config/SparkFilterFactory.java | 12 --- .../io/fairspace/saturn/services/BaseApp.java | 82 ------------------- .../saturn/services/errors/ErrorHelper.java | 33 -------- .../saturn/config/SparkFilterFactoryTest.java | 21 ----- .../services/errors/ErrorHelperTest.java | 45 ---------- .../validation/ShaclValidatorTest.java | 6 +- 9 files changed, 2 insertions(+), 259 deletions(-) delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/config/SaturnSparkFilter.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterConfig.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/BaseApp.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorHelper.java delete mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/config/SparkFilterFactoryTest.java delete mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/services/errors/ErrorHelperTest.java diff --git a/projects/saturn/build.gradle b/projects/saturn/build.gradle index 39ed8c3032..2cdf612308 100644 --- a/projects/saturn/build.gradle +++ b/projects/saturn/build.gradle @@ -57,7 +57,6 @@ dependencies { implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${jacksonVersion}" implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" - implementation "org.zoomba-lang:spark-core:3.0" // todo: to be replaced with Spring MVC implementation 'com.pivovarit:throwing-function:1.5.1' implementation 'com.google.guava:guava:33.0.0-jre' implementation('com.io-informatics.oss:jackson-jsonld:0.1.1') { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SaturnSparkFilter.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SaturnSparkFilter.java deleted file mode 100644 index 130b15226e..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SaturnSparkFilter.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.fairspace.saturn.config; - -import java.util.Arrays; - -import jakarta.servlet.FilterConfig; -import lombok.Getter; -import spark.servlet.SparkApplication; -import spark.servlet.SparkFilter; - -import io.fairspace.saturn.services.BaseApp; - -public class SaturnSparkFilter extends SparkFilter { - - private final BaseApp[] apps; - - @Getter - private final String[] urls; - - public SaturnSparkFilter(BaseApp... apps) { - this.apps = apps; - this.urls = Arrays.stream(apps).map(BaseApp::getBasePath).toArray(String[]::new); - } - - @Override - protected SparkApplication[] getApplications(FilterConfig filterConfig) { - return apps; - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterConfig.java deleted file mode 100644 index dbabe4bae0..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterConfig.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.fairspace.saturn.config; - -import java.util.Arrays; - -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import spark.servlet.SparkFilter; - -import io.fairspace.saturn.config.properties.FeatureProperties; - -import static io.fairspace.saturn.config.SparkFilterFactory.createSparkFilter; - -/** - * Configuration for the Spark filter to enable the Saturn API. - */ -@Configuration -public class SparkFilterConfig { - - // todo: to be removed once switched to Spring MVC - @Bean - public FilterRegistrationBean sparkFilter(Services svc, FeatureProperties featureProperties) { - var registrationBean = new FilterRegistrationBean(); - var sparkFilter = createSparkFilter("/api", svc, featureProperties); - registrationBean.setFilter(sparkFilter); - // we cannot set /api/* as the url pattern, because it would override /api/webdav/* - // endpoints which defined as a separate servlet - String[] urls = - Arrays.stream(sparkFilter.getUrls()).map(url -> url + "/*").toArray(String[]::new); - registrationBean.addUrlPatterns(urls); - return registrationBean; - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java deleted file mode 100644 index 35e33040bf..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.fairspace.saturn.config; - -import io.fairspace.saturn.config.properties.KeycloakClientProperties; -import io.fairspace.saturn.services.users.LogoutApp; - -public class SparkFilterFactory { - public static SaturnSparkFilter createSparkFilter( - String apiPathPrefix, Services svc, KeycloakClientProperties keycloakClientProperties, String publicUrl) { - return new SaturnSparkFilter( - new LogoutApp("/logout", svc.getUserService(), keycloakClientProperties, publicUrl)); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/BaseApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/BaseApp.java deleted file mode 100644 index 75d6221992..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/BaseApp.java +++ /dev/null @@ -1,82 +0,0 @@ -package io.fairspace.saturn.services; - -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import lombok.Getter; -import lombok.extern.log4j.*; -import spark.*; -import spark.servlet.SparkApplication; - -import io.fairspace.saturn.rdf.dao.DAOException; -import io.fairspace.saturn.util.UnsupportedMediaTypeException; - -import static io.fairspace.saturn.services.errors.ErrorHelper.errorBody; -import static io.fairspace.saturn.services.errors.ErrorHelper.exceptionHandler; - -import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; -import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS; -import static jakarta.servlet.http.HttpServletResponse.*; -import static spark.Spark.notFound; -import static spark.Spark.path; -import static spark.globalstate.ServletFlag.isRunningFromServlet; - -@Log4j2 -public abstract class BaseApp implements SparkApplication { - protected static final ObjectMapper mapper = new ObjectMapper() - .registerModule(new IRIModule()) - .registerModule(new JavaTimeModule()) - .configure(WRITE_DATES_AS_TIMESTAMPS, false) - .configure(FAIL_ON_UNKNOWN_PROPERTIES, false); - - @Getter - private final String basePath; - - protected BaseApp(String basePath) { - this.basePath = basePath; - } - - @Override - public final void init() { - path(basePath, () -> { - notFound((req, res) -> { - String pathInfo = req.pathInfo(); - if (pathInfo.startsWith("/api/webdav") - || pathInfo.startsWith("/api/extra-storage") - || pathInfo.startsWith("/api/rdf")) { - return null; - } - return errorBody(SC_NOT_FOUND, "Not found"); - }); - exception(JsonMappingException.class, exceptionHandler(SC_BAD_REQUEST, "Invalid request body")); - exception(IllegalArgumentException.class, exceptionHandler(SC_BAD_REQUEST, null)); - exception(DAOException.class, exceptionHandler(SC_BAD_REQUEST, "Bad request")); - exception(UnsupportedMediaTypeException.class, exceptionHandler(SC_UNSUPPORTED_MEDIA_TYPE, null)); - exception(AccessDeniedException.class, exceptionHandler(SC_FORBIDDEN, null)); - exception(Exception.class, exceptionHandler(SC_INTERNAL_SERVER_ERROR, "Internal server error")); - exception(NotAvailableException.class, exceptionHandler(SC_SERVICE_UNAVAILABLE, null)); - exception(ConflictException.class, exceptionHandler(SC_CONFLICT, null)); - - initApp(); - }); - } - - protected abstract void initApp(); - - // A temporary workaround for https://github.com/perwendel/spark/issues/1062 - // Shadows spark.Spark.exception - public static void exception(Class exceptionClass, ExceptionHandler handler) { - if (isRunningFromServlet()) { - var wrapper = new ExceptionHandlerImpl<>(exceptionClass) { - @Override - public void handle(T exception, Request request, Response response) { - handler.handle(exception, request, response); - } - }; - - ExceptionMapper.getServletInstance().map(exceptionClass, wrapper); - } else { - Spark.exception(exceptionClass, handler); - } - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorHelper.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorHelper.java deleted file mode 100644 index 4189783165..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorHelper.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.fairspace.saturn.services.errors; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import ioinformarics.oss.jackson.module.jsonld.JsonldModule; -import lombok.SneakyThrows; -import lombok.extern.log4j.*; -import spark.ExceptionHandler; - -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; - -@Log4j2 -public class ErrorHelper { - private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JsonldModule()); - - public static ExceptionHandler exceptionHandler(int status, String message) { - return (e, req, res) -> { - log.error("{} Error handling request {} {}", status, req.requestMethod(), req.uri(), e); - res.status(status); - res.type(APPLICATION_JSON.asString()); - res.body(errorBody(status, message != null ? message : e.getMessage())); - }; - } - - public static String errorBody(int status, String message) { - return errorBody(status, message, null); - } - - @SneakyThrows(JsonProcessingException.class) - public static String errorBody(int status, String message, Object info) { - return mapper.writeValueAsString(new ErrorDto(status, message, info)); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/config/SparkFilterFactoryTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/config/SparkFilterFactoryTest.java deleted file mode 100644 index 4f226ad6c0..0000000000 --- a/projects/saturn/src/test/java/io/fairspace/saturn/config/SparkFilterFactoryTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.fairspace.saturn.config; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import io.fairspace.saturn.config.properties.KeycloakClientProperties; - -import static org.junit.Assert.assertNotNull; - -@RunWith(MockitoJUnitRunner.class) -public class SparkFilterFactoryTest { - @Mock - private Services svc; - - @Test - public void itCreatesAFilter() { - assertNotNull(SparkFilterFactory.createSparkFilter("/some/path", svc, new KeycloakClientProperties(), "")); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/errors/ErrorHelperTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/errors/ErrorHelperTest.java deleted file mode 100644 index dcaaf4ee22..0000000000 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/errors/ErrorHelperTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.fairspace.saturn.services.errors; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; - -import io.fairspace.saturn.vocabulary.FS; - -import static org.junit.Assert.assertEquals; - -public class ErrorHelperTest { - - @Test - public void errorBody() throws IOException { - var errorBody = ErrorHelper.errorBody(100, "BaseEvent", List.of("a", "b")); - - // Parse the json body - Map parsedMap = new ObjectMapper().readValue(errorBody, Map.class); - - // Expect the properties to be serialized as json - assertEquals(100, parsedMap.get("status")); - assertEquals("BaseEvent", parsedMap.get("message")); - assertEquals(List.of("a", "b"), parsedMap.get("details")); - } - - @Test - public void errorBodyContext() throws IOException { - var errorBody = ErrorHelper.errorBody(100, "BaseEvent", List.of("a", "b")); - - // Parse the json body - Map parsedMap = new ObjectMapper().readValue(errorBody, Map.class); - - // Expect the properties to be serialized as json - assertEquals(FS.ERROR_URI, parsedMap.get("@type")); - assertEquals( - Map.of( - "details", FS.ERROR_DETAILS_URI, - "message", FS.ERROR_MESSAGE_URI, - "status", FS.ERROR_STATUS_URI), - parsedMap.get("@context")); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ShaclValidatorTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ShaclValidatorTest.java index 32d7c9f75a..47b5a94a17 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ShaclValidatorTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ShaclValidatorTest.java @@ -21,7 +21,6 @@ import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; import static org.apache.jena.rdf.model.ResourceFactory.*; -import static org.eclipse.jetty.util.ProcessorUtils.availableProcessors; import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) @@ -32,14 +31,13 @@ public class ShaclValidatorTest { private static final Resource closedClassShape = createResource("http://example.com/ClosedClassShape"); private ShaclValidator validator; - private Model vocabulary; @Mock private ViolationHandler violationHandler; @Before public void setUp() { - vocabulary = SYSTEM_VOCABULARY.union(createDefaultModel() + Model vocabulary = SYSTEM_VOCABULARY.union(createDefaultModel() .add(closedClassShape, RDF.type, SHACLM.NodeShape) .add(closedClassShape, SHACLM.targetClass, closedClass) .add(closedClassShape, SHACLM.closed, createTypedLiteral(true))); @@ -211,7 +209,7 @@ public void validationForSomethingReferringToABlankNode2() { @Test public void multipleResourcesAreValidatedAsExpected() { var model = createDefaultModel(); - for (int i = 0; i < 2 * availableProcessors(); i++) { + for (int i = 0; i < 2 * Runtime.getRuntime().availableProcessors(); i++) { var resource = createResource(); model.add(resource, RDF.type, FS.File).add(resource, FS.createdBy, createTypedLiteral(123)); } From 921ade771bb2c79c565de784e746afa1620e25b2 Mon Sep 17 00:00:00 2001 From: anton Date: Fri, 11 Oct 2024 12:20:52 +0200 Subject: [PATCH 11/18] FAIRSPC-81: tuned global exception handling --- .../saturn/controller/dto/ErrorDto.java | 12 ++ .../exception/GlobalExceptionHandler.java | 30 ++-- .../services/PayloadParsingException.java | 25 ---- .../saturn/services/errors/ErrorDto.java | 20 --- .../saturn/controller/BaseControllerTest.java | 2 +- .../exception/GlobalExceptionHandlerTest.java | 135 ++++++++++++++++++ .../controller/exception/TestController.java | 23 +++ 7 files changed, 184 insertions(+), 63 deletions(-) create mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ErrorDto.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/PayloadParsingException.java delete mode 100644 projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorDto.java create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandlerTest.java create mode 100644 projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/TestController.java diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ErrorDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ErrorDto.java new file mode 100644 index 0000000000..0741c49902 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ErrorDto.java @@ -0,0 +1,12 @@ +package io.fairspace.saturn.controller.dto; + +import ioinformarics.oss.jackson.module.jsonld.annotation.JsonldProperty; +import ioinformarics.oss.jackson.module.jsonld.annotation.JsonldType; + +import io.fairspace.saturn.vocabulary.FS; + +@JsonldType(FS.ERROR_URI) +public record ErrorDto( + @JsonldProperty(FS.ERROR_STATUS_URI) int status, + @JsonldProperty(FS.ERROR_MESSAGE_URI) String message, + @JsonldProperty(FS.ERROR_DETAILS_URI) Object details) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java index 55e4deab16..d8ac294b15 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java @@ -1,44 +1,41 @@ package io.fairspace.saturn.controller.exception; +import java.util.stream.Collectors; + import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; -import io.fairspace.saturn.services.PayloadParsingException; -import io.fairspace.saturn.services.errors.ErrorDto; +import io.fairspace.saturn.controller.dto.ErrorDto; import io.fairspace.saturn.services.metadata.validation.ValidationException; @Slf4j @ControllerAdvice public class GlobalExceptionHandler { - // // todo: add tests - // @ExceptionHandler(AccessDeniedException.class) - // public ResponseEntity handleAccessDenied(AccessDeniedException ex) { - // return new ResponseEntity<>("Access Denied: " + ex.getMessage(), HttpStatus.FORBIDDEN); - // } - - @ExceptionHandler(PayloadParsingException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - public ResponseEntity handlePayloadParsingException(PayloadParsingException ex, HttpServletRequest req) { - log.error("Malformed request body for request {} {}", req.getMethod(), req.getRequestURI(), ex); - return buildErrorResponse(HttpStatus.BAD_REQUEST, "Malformed request body"); + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleConstraintViolationException( + ConstraintViolationException ex, HttpServletRequest req) { + var violations = ex.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .sorted() + .collect(Collectors.joining("; ")); + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", "Violations: " + violations); } @ExceptionHandler(ValidationException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) public ResponseEntity handleValidationException(ValidationException ex, HttpServletRequest req) { log.error("Validation error for request {} {}", req.getMethod(), req.getRequestURI(), ex); return buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", ex.getViolations()); } @ExceptionHandler(IllegalArgumentException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) public ResponseEntity handleIllegalArgumentException( IllegalArgumentException ex, HttpServletRequest req) { log.error("Validation error for request {} {}", req.getMethod(), req.getRequestURI(), ex); @@ -46,7 +43,6 @@ public ResponseEntity handleIllegalArgumentException( } @ExceptionHandler(AccessDeniedException.class) - @ResponseStatus(HttpStatus.FORBIDDEN) public ResponseEntity handleAccessDeniedException(AccessDeniedException ex, HttpServletRequest req) { log.error("Access denied for request {} {}", req.getMethod(), req.getRequestURI(), ex); return buildErrorResponse(HttpStatus.FORBIDDEN, "Access Denied"); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/PayloadParsingException.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/PayloadParsingException.java deleted file mode 100644 index 7fc4e4d7b2..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/PayloadParsingException.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.fairspace.saturn.services; - -/** - * Can represent an error that happened during parsing of HTTP request body, etc - */ -public class PayloadParsingException extends RuntimeException { - public PayloadParsingException() {} - - public PayloadParsingException(String message) { - super(message); - } - - public PayloadParsingException(String message, Throwable cause) { - super(message, cause); - } - - public PayloadParsingException(Throwable cause) { - super(cause); - } - - public PayloadParsingException( - String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorDto.java deleted file mode 100644 index e841b7488f..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.fairspace.saturn.services.errors; - -import ioinformarics.oss.jackson.module.jsonld.annotation.JsonldProperty; -import ioinformarics.oss.jackson.module.jsonld.annotation.JsonldType; -import lombok.Value; - -import io.fairspace.saturn.vocabulary.FS; - -@Value -@JsonldType(FS.ERROR_URI) -public class ErrorDto { - @JsonldProperty(FS.ERROR_STATUS_URI) - private int status; - - @JsonldProperty(FS.ERROR_MESSAGE_URI) - private String message; - - @JsonldProperty(FS.ERROR_DETAILS_URI) - private Object details; -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/BaseControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/BaseControllerTest.java index 4c843499f6..afa3c256f9 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/BaseControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/BaseControllerTest.java @@ -15,7 +15,7 @@ @ImportAutoConfiguration(exclude = {SecurityAutoConfiguration.class, OAuth2ResourceServerAutoConfiguration.class}) @Import(BaseControllerTest.CustomObjectMapperConfig.class) -class BaseControllerTest { +public class BaseControllerTest { @MockBean private JwtAuthConverterProperties jwtAuthConverterProperties; diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandlerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000000..7b8c8b231b --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandlerTest.java @@ -0,0 +1,135 @@ +package io.fairspace.saturn.controller.exception; + +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.controller.BaseControllerTest; +import io.fairspace.saturn.services.metadata.validation.ValidationException; +import io.fairspace.saturn.services.metadata.validation.Violation; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest({GlobalExceptionHandler.class, TestController.class}) +public class GlobalExceptionHandlerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private TestController.TestInnerClass testInnerClass; + + @Test + public void testHandleConstraintViolationException() throws Exception { + // Mocking a ConstraintViolationException with a couple of violations + ConstraintViolation violation1 = Mockito.mock(ConstraintViolation.class); + ConstraintViolation violation2 = Mockito.mock(ConstraintViolation.class); + when(violation1.getMessage()).thenReturn("Violation 1"); + when(violation2.getMessage()).thenReturn("Violation 2"); + Set> violations = Set.of(violation1, violation2); + ConstraintViolationException exception = new ConstraintViolationException(violations); + + doThrow(exception).when(testInnerClass).method(); // Simulating the exception + + mockMvc.perform(get("/test")) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content() + .json( + """ + { + "status": 400, + "message": "Validation Error", + "details": "Violations: Violation 1; Violation 2" + } + """)); + } + + @Test + public void testHandleValidationException() throws Exception { + // Mocking a ValidationException with a violation + Set violations = Set.of(new Violation("Invalid value", "subject", "predicate", "value")); + ValidationException exception = new ValidationException(violations); + + doThrow(exception).when(testInnerClass).method(); // Simulating the exception + + mockMvc.perform(get("/test")) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content() + .json( + """ + { + "status": 400, + "message": "Validation Error", + "details": [ + { + "message": "Invalid value", + "subject": "subject", + "predicate": "predicate", + "value": "value" + } + ] + } + """)); + } + + @Test + public void testHandleIllegalArgumentException() throws Exception { + // Mocking an IllegalArgumentException + IllegalArgumentException exception = new IllegalArgumentException("Invalid argument"); + + doThrow(exception).when(testInnerClass).method(); // Simulating the exception + + mockMvc.perform(get("/test")) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content() + .json( + """ + { + "status": 400, + "message": "Validation Error", + "details": "Invalid argument" + } + """)); + } + + @Test + public void testHandleAccessDeniedException() throws Exception { + // Mocking an AccessDeniedException + AccessDeniedException exception = new AccessDeniedException("Access denied"); + + doThrow(exception).when(testInnerClass).method(); // Simulating the exception + + mockMvc.perform(get("/test")) + .andExpect(status().isForbidden()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content() + .json( + """ + { + "status": 403, + "message": "Access Denied", + "details": null + } + """)); + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/TestController.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/TestController.java new file mode 100644 index 0000000000..97151333f3 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/TestController.java @@ -0,0 +1,23 @@ +package io.fairspace.saturn.controller.exception; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class TestController { + + private final TestInnerClass testInnerClass; + + @GetMapping("/test") + public void testMethod() { + testInnerClass.method(); + } + + @Component + public static class TestInnerClass { + public void method() {} + } +} From ac7b6e41a3d3390578572ea89234754d5f9050bd Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 14 Oct 2024 17:42:19 +0200 Subject: [PATCH 12/18] FAIRSPC-81: aligned all controllers with /api/ path --- .../io/fairspace/saturn/controller/FeaturesController.java | 2 +- .../io/fairspace/saturn/controller/MaintenanceController.java | 2 +- .../io/fairspace/saturn/controller/MetadataController.java | 2 +- .../java/io/fairspace/saturn/controller/SearchController.java | 2 +- .../java/io/fairspace/saturn/controller/SparqlController.java | 2 +- .../java/io/fairspace/saturn/controller/UserController.java | 2 +- .../io/fairspace/saturn/controller/VocabularyController.java | 2 +- .../io/fairspace/saturn/controller/WorkspaceController.java | 3 +-- 8 files changed, 8 insertions(+), 9 deletions(-) diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java index 6afe60dd9e..7b0e9c652a 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java @@ -12,7 +12,7 @@ import io.fairspace.saturn.config.properties.FeatureProperties; @RestController -@RequestMapping("/api/features/") +@RequestMapping("${application.basePath}/features/") @RequiredArgsConstructor public class FeaturesController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java index 60a90545aa..acade28c84 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java @@ -10,7 +10,7 @@ import io.fairspace.saturn.config.Services; @RestController -@RequestMapping("/api/maintenance") +@RequestMapping("${application.basePath}/maintenance") @RequiredArgsConstructor public class MaintenanceController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java index f678e9a3b9..5b8674163c 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java @@ -32,7 +32,7 @@ @Log4j2 @RestController -@RequestMapping("/api/metadata/") +@RequestMapping("${application.basePath}/metadata/") @RequiredArgsConstructor @Validated public class MetadataController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java index 5c68b0f7cd..bbea8d2085 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java @@ -13,7 +13,7 @@ import io.fairspace.saturn.controller.dto.request.LookupSearchRequest; @RestController -@RequestMapping("/api/search") +@RequestMapping("${application.basePath}/search") @RequiredArgsConstructor public class SearchController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java index c2296b0140..7dc7a78359 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java @@ -14,7 +14,7 @@ import io.fairspace.saturn.services.views.SparqlQueryService; @RestController -@RequestMapping("/api/rdf") +@RequestMapping("${application.basePath}/rdf") @Validated @RequiredArgsConstructor public class SparqlController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java index d570ff7abf..b77b1c8cc7 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java @@ -11,7 +11,7 @@ import io.fairspace.saturn.services.users.UserService; @RestController -@RequestMapping("/api/users/") +@RequestMapping("${application.basePath}/users/") @RequiredArgsConstructor public class UserController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java index a0eb1c61df..99cac773e0 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java @@ -14,7 +14,7 @@ import static io.fairspace.saturn.vocabulary.Vocabularies.VOCABULARY; @RestController -@RequestMapping("/api/vocabulary/") +@RequestMapping("${application.basePath}/vocabulary/") public class VocabularyController { @GetMapping diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java index 2853326686..6c2edfa900 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java @@ -38,8 +38,7 @@ public WorkspaceController(Services services) { @PutMapping(value = "/") public ResponseEntity createWorkspace(@RequestBody Workspace workspace) { var createdWorkspace = services.getWorkspaceService().createWorkspace(workspace); - return ResponseEntity.ok( - createdWorkspace); // it should return HTTP 201 CREATED - tobe analyzed across the codebase + return ResponseEntity.ok(createdWorkspace); } @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) From b18d47a6a16bf667fc6535fa7402ff3ba310e15a Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 14 Oct 2024 17:56:12 +0200 Subject: [PATCH 13/18] FAIRSPC-81: named reasonable get view data --- .../java/io/fairspace/saturn/controller/ViewController.java | 2 +- .../io/fairspace/saturn/controller/ViewControllerTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java index be70285fc6..812acda76f 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java @@ -36,7 +36,7 @@ public ViewsDto getViews() { } @PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createView(@Valid @RequestBody ViewRequest requestBody) { + public ResponseEntity getViewData(@Valid @RequestBody ViewRequest requestBody) { var result = services.getQueryService().retrieveViewPage(requestBody); return ResponseEntity.ok(result); } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java index 445546bd17..f7151b2afe 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java @@ -73,7 +73,7 @@ public void testGetViewsSuccess() throws Exception { } @Test - public void testCreateViewSuccess() throws Exception { + public void testGetViewDataSuccess() throws Exception { // Mock request body and response var viewRequest = new ViewRequest(); viewRequest.setView("view1"); @@ -141,7 +141,7 @@ public void testCountSuccess() throws Exception { } @Test - public void testCreateViewValidationFailure() throws Exception { + public void testGetViewDataValidationFailure() throws Exception { // Test validation error (e.g., invalid request body) var invalidRequestBody = new ViewRequest(); invalidRequestBody.setPage(0); // Invalid page (must be >= 1) From c35bae3978ef49f1a9504674adb3166ca0211605 Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 14 Oct 2024 18:01:46 +0200 Subject: [PATCH 14/18] FAIRSPC-81: removed redundant try-catch --- .../services/workspaces/WorkspaceService.java | 82 ++++++++----------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java index 701fc3a591..9d69190448 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.Optional; -import lombok.extern.log4j.*; +import lombok.extern.log4j.Log4j2; import org.apache.jena.graph.Node; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; @@ -38,51 +38,41 @@ public WorkspaceService(Transactions tx, UserService userService) { public List listWorkspaces() { return tx.calculateRead(m -> { - try { - var user = m.wrapAsResource(getUserURI()); - return new DAO(m) - .list(Workspace.class).stream() - .peek(ws -> { - var res = m.wrapAsResource(ws.getIri()); - ws.setCanManage(userService.currentUser().isAdmin() - || user.hasProperty(FS.isManagerOf, res)); - ws.setCanCollaborate(ws.isCanManage() || user.hasProperty(FS.isMemberOf, res)); - var workspaceCollections = m.listSubjectsWithProperty(FS.ownedBy, res) - .filterKeep(r -> r.hasProperty(RDF.type, FS.Collection)) - .toList(); - var totalCollectionCount = workspaceCollections.size(); - var nonDeletedCollectionCount = (int) workspaceCollections.stream() - .filter(collection -> !collection.hasProperty(FS.dateDeleted)) - .count(); - var memberCount = m.listSubjectsWithProperty(RDF.type, FS.User) - .filterKeep(u -> u.hasProperty(FS.isMemberOf, res)) - .toList() - .size(); - var managers = new DAO(m) - .list(User.class).stream() - .filter(u -> m.wrapAsResource(u.getIri()) - .hasProperty(FS.isManagerOf, res)) - .collect(toList()); - ws.setSummary(WorkspaceSummary.builder() - .totalCollectionCount(totalCollectionCount) - .nonDeletedCollectionCount(nonDeletedCollectionCount) - .memberCount(memberCount + managers.size()) - .build()); - ws.setManagers(managers); - }) - .filter(ws -> userService.currentUser().isCanViewPublicMetadata() - || ws.isCanManage() - || ws.isCanCollaborate()) - .collect(toList()); - } catch (Throwable e) { - log.error("+++++++++++++++++++++++++++++"); - log.error("+++++++++++++++++++++++++++++"); - log.error("+++++++++++++++++++++++++++++"); - log.error("+++++++++++++++++++++++++++++"); - log.error("+++++++++++++++++++++++++++++"); - log.error("Error listing workspaces", e); - throw e; - } + var user = m.wrapAsResource(getUserURI()); + return new DAO(m) + .list(Workspace.class).stream() + .peek(ws -> { + var res = m.wrapAsResource(ws.getIri()); + ws.setCanManage( + userService.currentUser().isAdmin() || user.hasProperty(FS.isManagerOf, res)); + ws.setCanCollaborate(ws.isCanManage() || user.hasProperty(FS.isMemberOf, res)); + var workspaceCollections = m.listSubjectsWithProperty(FS.ownedBy, res) + .filterKeep(r -> r.hasProperty(RDF.type, FS.Collection)) + .toList(); + var totalCollectionCount = workspaceCollections.size(); + var nonDeletedCollectionCount = (int) workspaceCollections.stream() + .filter(collection -> !collection.hasProperty(FS.dateDeleted)) + .count(); + var memberCount = m.listSubjectsWithProperty(RDF.type, FS.User) + .filterKeep(u -> u.hasProperty(FS.isMemberOf, res)) + .toList() + .size(); + var managers = new DAO(m) + .list(User.class).stream() + .filter(u -> m.wrapAsResource(u.getIri()) + .hasProperty(FS.isManagerOf, res)) + .collect(toList()); + ws.setSummary(WorkspaceSummary.builder() + .totalCollectionCount(totalCollectionCount) + .nonDeletedCollectionCount(nonDeletedCollectionCount) + .memberCount(memberCount + managers.size()) + .build()); + ws.setManagers(managers); + }) + .filter(ws -> userService.currentUser().isCanViewPublicMetadata() + || ws.isCanManage() + || ws.isCanCollaborate()) + .collect(toList()); }); } From a0c37d1277c258b9ea938457c05922492c9c1dad Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 14 Oct 2024 18:20:32 +0200 Subject: [PATCH 15/18] FAIRSPC-81: aligned the way we map resources across all controllers --- .../saturn/controller/FeaturesController.java | 4 ++-- .../saturn/controller/MetadataController.java | 13 +++++++++---- .../saturn/controller/SparqlController.java | 4 +++- .../fairspace/saturn/controller/UserController.java | 6 +++--- .../fairspace/saturn/controller/ViewController.java | 12 ++++-------- .../saturn/controller/VocabularyController.java | 4 ++-- .../saturn/controller/WorkspaceController.java | 9 ++++----- .../saturn/controller/enums/CustomMediaType.java | 2 ++ 8 files changed, 29 insertions(+), 25 deletions(-) diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java index 7b0e9c652a..02fa3a6210 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java @@ -12,13 +12,13 @@ import io.fairspace.saturn.config.properties.FeatureProperties; @RestController -@RequestMapping("${application.basePath}/features/") +@RequestMapping("${application.basePath}/features") @RequiredArgsConstructor public class FeaturesController { private final FeatureProperties featureProperties; - @GetMapping + @GetMapping("/") public ResponseEntity> getFeatures() { return ResponseEntity.ok(featureProperties.getFeatures()); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java index 5b8674163c..04e7add08e 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java @@ -32,7 +32,7 @@ @Log4j2 @RestController -@RequestMapping("${application.basePath}/metadata/") +@RequestMapping("${application.basePath}/metadata") @RequiredArgsConstructor @Validated public class MetadataController { @@ -43,7 +43,9 @@ public class MetadataController { private final Services services; - @GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) + @GetMapping( + value = "/", + produces = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) public ResponseEntity getMetadata( @RequestParam(required = false) String subject, @RequestParam(name = "withValueProperties", defaultValue = "false") boolean withValueProperties, @@ -54,7 +56,9 @@ public ResponseEntity getMetadata( return ResponseEntity.ok(metadata); } - @PutMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) + @PutMapping( + value = "/", + consumes = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) @ResponseStatus(HttpStatus.NO_CONTENT) public void putMetadata( @RequestBody String body, @@ -66,6 +70,7 @@ public void putMetadata( } @PatchMapping( + value = "/", consumes = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) @ResponseStatus(HttpStatus.NO_CONTENT) public void patchMetadata( @@ -76,7 +81,7 @@ public void patchMetadata( services.getMetadataService().patch(model, doViewsUpdate); } - @DeleteMapping + @DeleteMapping("/") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteMetadata( @RequestParam(required = false) @ValidIri String subject, diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java index 7dc7a78359..bd0d705665 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java @@ -13,6 +13,8 @@ import io.fairspace.saturn.services.AccessDeniedException; import io.fairspace.saturn.services.views.SparqlQueryService; +import static io.fairspace.saturn.controller.enums.CustomMediaType.APPLICATION_SPARQL_QUERY; + @RestController @RequestMapping("${application.basePath}/rdf") @Validated @@ -29,7 +31,7 @@ public class SparqlController { * @param sparqlQuery the SPARQL query * @return the result of the query (JSON) */ - @PostMapping(value = "/query", consumes = "application/sparql-query", produces = "application/json") + @PostMapping(value = "/query", consumes = APPLICATION_SPARQL_QUERY) // todo: uncomment the line below and remove the metadataPermissions.hasMetadataQueryPermission() call once // the MetadataPermissions is available in the IoC container // @PreAuthorize("@metadataPermissions.hasMetadataQueryPermission()") diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java index b77b1c8cc7..8bcdab5d36 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java @@ -11,18 +11,18 @@ import io.fairspace.saturn.services.users.UserService; @RestController -@RequestMapping("${application.basePath}/users/") +@RequestMapping("${application.basePath}/users") @RequiredArgsConstructor public class UserController { private final UserService userService; - @GetMapping + @GetMapping("/") public ResponseEntity> getUsers() { return ResponseEntity.ok(userService.getUsers()); } - @PatchMapping + @PatchMapping("/") public ResponseEntity updateUserRoles(@RequestBody UserRolesUpdate update) { userService.update(update); return ResponseEntity.noContent().build(); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java index 812acda76f..9f6fb30f3c 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java @@ -1,7 +1,6 @@ package io.fairspace.saturn.controller; import jakarta.validation.Valid; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; @@ -29,28 +28,25 @@ public ViewController(Services services) { this.services = services; } - @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping("/") public ViewsDto getViews() { var views = services.getViewService().getViews(); return new ViewsDto(views); } - @PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping("/") public ResponseEntity getViewData(@Valid @RequestBody ViewRequest requestBody) { var result = services.getQueryService().retrieveViewPage(requestBody); return ResponseEntity.ok(result); } - @GetMapping(value = "/facets", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping("/facets") public ResponseEntity getFacets() { var facets = services.getViewService().getFacets(); return ResponseEntity.ok(new FacetsDto(facets)); } - @PostMapping( - value = "/count", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping("/count") public ResponseEntity count(@Valid @RequestBody CountRequest requestBody) { var result = services.getQueryService().count(requestBody); return ResponseEntity.ok(result); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java index 99cac773e0..af13baa3f8 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java @@ -14,10 +14,10 @@ import static io.fairspace.saturn.vocabulary.Vocabularies.VOCABULARY; @RestController -@RequestMapping("${application.basePath}/vocabulary/") +@RequestMapping("${application.basePath}/vocabulary") public class VocabularyController { - @GetMapping + @GetMapping("/") public ResponseEntity getVocabulary( @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader) { var format = getFormat(acceptHeader); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java index 6c2edfa900..8dd7ad42a3 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java @@ -6,7 +6,6 @@ import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; @@ -35,19 +34,19 @@ public WorkspaceController(Services services) { this.services = services; } - @PutMapping(value = "/") + @PutMapping("/") public ResponseEntity createWorkspace(@RequestBody Workspace workspace) { var createdWorkspace = services.getWorkspaceService().createWorkspace(workspace); return ResponseEntity.ok(createdWorkspace); } - @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping("/") public ResponseEntity> listWorkspaces() { var workspaces = services.getWorkspaceService().listWorkspaces(); return ResponseEntity.ok(workspaces); } - @DeleteMapping(value = "/") + @DeleteMapping("/") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteWorkspace(@RequestParam("workspace") String workspaceUri) { services.getWorkspaceService().deleteWorkspace(NodeFactory.createURI(workspaceUri)); @@ -60,7 +59,7 @@ public ResponseEntity> getUsers(@RequestParam("workspac return ResponseEntity.ok(users); } - @PatchMapping(value = "/users/", consumes = MediaType.APPLICATION_JSON_VALUE) + @PatchMapping(value = "/users/") @ResponseStatus(HttpStatus.NO_CONTENT) public void setUserRole(@RequestBody UserRoleDto userRoleDto) { services.getWorkspaceService() diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java index 8eaa0b184c..cde1da8f21 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java @@ -8,4 +8,6 @@ public enum CustomMediaType { public static final String TEXT_TURTLE = "text/turtle"; public static final String APPLICATION_N_TRIPLES = "application/n-triples"; + + public static final String APPLICATION_SPARQL_QUERY = "application/sparql-query"; } From 001022e6db2da14ed67636e4a2d62bfab5cfed95 Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 14 Oct 2024 18:22:42 +0200 Subject: [PATCH 16/18] FAIRSPC-81: removed mentioning of spark on log config --- projects/saturn/src/main/resources/log4j2.properties | 2 -- 1 file changed, 2 deletions(-) diff --git a/projects/saturn/src/main/resources/log4j2.properties b/projects/saturn/src/main/resources/log4j2.properties index 42ead9fb0a..74c11128af 100644 --- a/projects/saturn/src/main/resources/log4j2.properties +++ b/projects/saturn/src/main/resources/log4j2.properties @@ -4,8 +4,6 @@ rootLogger.appenderRef.stdout.ref = stdout # Avoid warn messages from milton standard filter, as they # also appear whenever the user makes a mistake -logger.spark.name = spark.http -logger.spark.level = warn logger.milton.name = io.milton.http logger.milton.level = warn logger.milton-filter.name = io.milton.http.StandardFilter From 91ef6af46fe9156c3f6656c1adac2c933c815728 Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 14 Oct 2024 18:40:46 +0200 Subject: [PATCH 17/18] FAIRSPC-81: replaced two value classes with records --- .../saturn/controller/dto/SearchResultDto.java | 11 +---------- .../saturn/controller/dto/SearchResultsDto.java | 7 +------ .../services/search/JdbcFileSearchServiceTest.java | 8 ++++---- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java index c0dda45561..9d86207112 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java @@ -2,15 +2,6 @@ import lombok.Builder; import lombok.NonNull; -import lombok.Value; -@Value @Builder -public class SearchResultDto { - @NonNull - String id; - - String label; - String type; - String comment; -} +public record SearchResultDto(@NonNull String id, String label, String type, String comment) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java index 57850e1ee1..4875192363 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java @@ -3,11 +3,6 @@ import java.util.List; import lombok.Builder; -import lombok.Value; -@Value @Builder -public class SearchResultsDto { - List results; - String query; -} +public record SearchResultsDto(List results, String query) {} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java index 7a36d3b45f..043db9ee8f 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java @@ -184,8 +184,8 @@ public void testSearchFiles() { var results = fileSearchService.searchFiles(request); Assert.assertEquals(2, results.size()); // Expect the results to be sorted by id - Assert.assertEquals("sample-s2-b-rna.fastq", results.get(0).getLabel()); - Assert.assertEquals("sample-s2-b-rna_copy.fastq", results.get(1).getLabel()); + Assert.assertEquals("sample-s2-b-rna.fastq", results.get(0).label()); + Assert.assertEquals("sample-s2-b-rna_copy.fastq", results.get(1).label()); } @Test @@ -199,7 +199,7 @@ public void testSearchFilesRestrictsToAccessibleCollections() { selectAdmin(); results = fileSearchService.searchFiles(request); Assert.assertEquals(1, results.size()); - Assert.assertEquals("coffee.jpg", results.getFirst().getLabel()); + Assert.assertEquals("coffee.jpg", results.getFirst().label()); } @Test @@ -214,7 +214,7 @@ public void testSearchFilesRestrictsToAccessibleCollectionsAfterReindexing() { selectAdmin(); results = fileSearchService.searchFiles(request); Assert.assertEquals(1, results.size()); - Assert.assertEquals("coffee.jpg", results.getFirst().getLabel()); + Assert.assertEquals("coffee.jpg", results.getFirst().label()); } @Test From fc40d62e82bab5f8f10fd26db4ee3c90d93e786c Mon Sep 17 00:00:00 2001 From: anton Date: Tue, 15 Oct 2024 19:39:01 +0200 Subject: [PATCH 18/18] FAIRSPC-81: set servlet context path to /api --- .../saturn/controller/FeaturesController.java | 2 +- .../saturn/controller/MaintenanceController.java | 2 +- .../saturn/controller/MetadataController.java | 2 +- .../saturn/controller/SearchController.java | 2 +- .../saturn/controller/SparqlController.java | 2 +- .../saturn/controller/UserController.java | 2 +- .../saturn/controller/ViewController.java | 2 +- .../saturn/controller/VocabularyController.java | 2 +- .../saturn/controller/WorkspaceController.java | 2 +- .../io/fairspace/saturn/webdav/WebDAVConfig.java | 4 ++-- .../saturn/src/main/resources/application.yaml | 3 ++- .../saturn/controller/FeaturesControllerTest.java | 2 +- .../controller/MaintenanceControllerTest.java | 8 ++++---- .../saturn/controller/MetadataControllerTest.java | 15 ++++++--------- .../saturn/controller/SearchControllerTest.java | 4 ++-- .../saturn/controller/UserControllerTest.java | 9 +++------ .../saturn/controller/ViewControllerTest.java | 13 +++++-------- .../controller/VocabularyControllerTest.java | 2 +- .../controller/WorkspaceControllerTest.java | 11 +++++------ 19 files changed, 40 insertions(+), 49 deletions(-) diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java index 02fa3a6210..69a60edaad 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java @@ -12,7 +12,7 @@ import io.fairspace.saturn.config.properties.FeatureProperties; @RestController -@RequestMapping("${application.basePath}/features") +@RequestMapping("/features") @RequiredArgsConstructor public class FeaturesController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java index acade28c84..5d7911aa9e 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java @@ -10,7 +10,7 @@ import io.fairspace.saturn.config.Services; @RestController -@RequestMapping("${application.basePath}/maintenance") +@RequestMapping("/maintenance") @RequiredArgsConstructor public class MaintenanceController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java index 04e7add08e..28e8b0daf8 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java @@ -32,7 +32,7 @@ @Log4j2 @RestController -@RequestMapping("${application.basePath}/metadata") +@RequestMapping("/metadata") @RequiredArgsConstructor @Validated public class MetadataController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java index bbea8d2085..5ac8952143 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java @@ -13,7 +13,7 @@ import io.fairspace.saturn.controller.dto.request.LookupSearchRequest; @RestController -@RequestMapping("${application.basePath}/search") +@RequestMapping("/search") @RequiredArgsConstructor public class SearchController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java index bd0d705665..110b07c263 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java @@ -16,7 +16,7 @@ import static io.fairspace.saturn.controller.enums.CustomMediaType.APPLICATION_SPARQL_QUERY; @RestController -@RequestMapping("${application.basePath}/rdf") +@RequestMapping("/rdf") @Validated @RequiredArgsConstructor public class SparqlController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java index 8bcdab5d36..3789614e69 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java @@ -11,7 +11,7 @@ import io.fairspace.saturn.services.users.UserService; @RestController -@RequestMapping("${application.basePath}/users") +@RequestMapping("/users") @RequiredArgsConstructor public class UserController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java index 9f6fb30f3c..cc9cbfc91e 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java @@ -18,7 +18,7 @@ import io.fairspace.saturn.controller.dto.request.ViewRequest; @RestController -@RequestMapping("${application.basePath}/views") +@RequestMapping("/views") @Validated public class ViewController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java index af13baa3f8..0aa5d9da12 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java @@ -14,7 +14,7 @@ import static io.fairspace.saturn.vocabulary.Vocabularies.VOCABULARY; @RestController -@RequestMapping("${application.basePath}/vocabulary") +@RequestMapping("/vocabulary") public class VocabularyController { @GetMapping("/") diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java index 8dd7ad42a3..75c4bc20de 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java @@ -24,7 +24,7 @@ import io.fairspace.saturn.services.workspaces.WorkspaceRole; @RestController -@RequestMapping("${application.basePath}/workspaces") +@RequestMapping("/workspaces") @Validated public class WorkspaceController { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/WebDAVConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/WebDAVConfig.java index 49901c1c04..dd591d6ca6 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/WebDAVConfig.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/WebDAVConfig.java @@ -15,13 +15,13 @@ public class WebDAVConfig { @Bean public ServletRegistrationBean webDavServlet(Services svcs) { - return new ServletRegistrationBean<>(svcs.getDavServlet(), "/api/webdav/*"); + return new ServletRegistrationBean<>(svcs.getDavServlet(), "/webdav/*"); } @Bean @ConditionalOnMultiValuedProperty(prefix = "application", name = "features", havingValue = "ExtraStorage") public ServletRegistrationBean webDavExtraStorageServlet(Services svcs) { - return new ServletRegistrationBean<>(svcs.getExtraDavServlet(), "/api/extra-storage/*"); + return new ServletRegistrationBean<>(svcs.getExtraDavServlet(), "/extra-storage/*"); } /** diff --git a/projects/saturn/src/main/resources/application.yaml b/projects/saturn/src/main/resources/application.yaml index 7f26388a5e..fe34a41771 100644 --- a/projects/saturn/src/main/resources/application.yaml +++ b/projects/saturn/src/main/resources/application.yaml @@ -1,5 +1,7 @@ server: port: 8090 + servlet: + context-path: /api # Configuration to access the Keycloak server (user lists, user count, etc.) keycloak: @@ -35,7 +37,6 @@ jwt: principal-attribute: preferred_username application: - basePath: /api publicUrl: ${PUBLIC_URL:http://localhost:8080} jena: # Base IRI for all metadata entities diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java index 33145c1ae3..c4bbd18905 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java @@ -33,7 +33,7 @@ void testGetFeatures() throws Exception { when(featureProperties.getFeatures()).thenReturn(features); // Perform GET request and verify the response - mockMvc.perform(get("/api/features/").accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(get("/features/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json("[\"ExtraStorage\"]")); diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/MaintenanceControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MaintenanceControllerTest.java index ad0b2af049..a93447ef18 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/MaintenanceControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MaintenanceControllerTest.java @@ -36,7 +36,7 @@ public void setUp() { void testStartReindex() throws Exception { doNothing().when(maintenanceService).startRecreateIndexTask(); - mockMvc.perform(post("/api/maintenance/reindex").contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform(post("/maintenance/reindex").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()); // Expect 204 No Content verify(maintenanceService).startRecreateIndexTask(); } @@ -45,7 +45,7 @@ void testStartReindex() throws Exception { void testCompactRdfStorage() throws Exception { doNothing().when(maintenanceService).compactRdfStorageTask(); - mockMvc.perform(post("/api/maintenance/compact").contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform(post("/maintenance/compact").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()); // Expect 204 No Content verify(maintenanceService).compactRdfStorageTask(); } @@ -54,7 +54,7 @@ void testCompactRdfStorage() throws Exception { void testGetStatusActive() throws Exception { when(maintenanceService.active()).thenReturn(true); - mockMvc.perform(get("/api/maintenance/status").accept(MediaType.TEXT_PLAIN)) + mockMvc.perform(get("/maintenance/status").accept(MediaType.TEXT_PLAIN)) .andExpect(status().isOk()) // Expect 200 OK .andExpect(content().string("active")); // Expect content "active" verify(maintenanceService).active(); @@ -64,7 +64,7 @@ void testGetStatusActive() throws Exception { void testGetStatusInactive() throws Exception { when(maintenanceService.active()).thenReturn(false); - mockMvc.perform(get("/api/maintenance/status").accept(MediaType.TEXT_PLAIN)) + mockMvc.perform(get("/maintenance/status").accept(MediaType.TEXT_PLAIN)) .andExpect(status().isOk()) // Expect 200 OK .andExpect(content().string("inactive")); // Expect content "inactive" verify(maintenanceService).active(); diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java index 21cfe4058c..db55df951d 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java @@ -49,7 +49,7 @@ public void testGetMetadata() throws Exception { "test-value"); Mockito.when(metadataService.get(eq("http://example.com"), eq(false))).thenReturn(mockModel); - mockMvc.perform(get("/api/metadata/") + mockMvc.perform(get("/metadata/") .param("subject", "http://example.com") .param("withValueProperties", "false") .header("Accept", TEXT_TURTLE)) @@ -66,10 +66,7 @@ public void testPutMetadata() throws Exception { ex:subject ex:property "value" . """; - mockMvc.perform(put("/api/metadata/") - .content(body) - .contentType(TEXT_TURTLE) - .param("doViewsUpdate", "true")) + mockMvc.perform(put("/metadata/").content(body).contentType(TEXT_TURTLE).param("doViewsUpdate", "true")) .andExpect(status().isNoContent()); Mockito.verify(metadataService).put(any(Model.class), eq(true)); @@ -83,7 +80,7 @@ public void testPatchMetadata() throws Exception { ex:subject ex:property "updated-value" . """; - mockMvc.perform(patch("/api/metadata/") + mockMvc.perform(patch("/metadata/") .content(body) .contentType(TEXT_TURTLE) .param("doViewsUpdate", "false")) @@ -96,7 +93,7 @@ public void testPatchMetadata() throws Exception { public void testDeleteMetadataBySubject() throws Exception { Mockito.when(metadataService.softDelete(any())).thenReturn(true); - mockMvc.perform(delete("/api/metadata/").param("subject", "http://example.com")) + mockMvc.perform(delete("/metadata/").param("subject", "http://example.com")) .andExpect(status().isNoContent()); Mockito.verify(metadataService).softDelete(any()); @@ -110,7 +107,7 @@ public void testDeleteMetadataByModel() throws Exception { ex:subject ex:property "value" . """; - mockMvc.perform(delete("/api/metadata/") + mockMvc.perform(delete("/metadata/") .content(body) .contentType(TEXT_TURTLE) .param("doViewsUpdate", "true")) @@ -123,7 +120,7 @@ public void testDeleteMetadataByModel() throws Exception { public void testDeleteMetadataSubjectNotFound() throws Exception { Mockito.when(metadataService.softDelete(any())).thenReturn(false); - mockMvc.perform(delete("/api/metadata/").param("subject", "http://example.com")) + mockMvc.perform(delete("/metadata/").param("subject", "http://example.com")) .andExpect(status().isBadRequest()); } } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/SearchControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/SearchControllerTest.java index 23fbe976b4..a266d278a4 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/SearchControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/SearchControllerTest.java @@ -53,7 +53,7 @@ void testSearchFiles() throws Exception { when(fileSearchService.searchFiles(any(FileSearchRequest.class))).thenReturn(mockResults); mockMvc.perform( - post("/api/search/files") + post("/search/files") .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -101,7 +101,7 @@ void testLookupSearch() throws Exception { .thenReturn(resultsDTO); mockMvc.perform( - post("/api/search/lookup") + post("/search/lookup") .contentType(MediaType.APPLICATION_JSON) .content( """ diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java index 8fa9669209..70e7587cac 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java @@ -36,7 +36,7 @@ void testGetUsers() throws Exception { var users = List.of(user1, user2); when(service.getUsers()).thenReturn(users); - mockMvc.perform(get("/api/users/").accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(get("/users/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // Expect 200 OK .andExpect( content() @@ -84,7 +84,7 @@ void testUpdateUserRoles() throws Exception { doNothing().when(service).update(update); mockMvc.perform( - patch("/api/users/") + patch("/users/") .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -102,14 +102,11 @@ void testUpdateUserRoles() throws Exception { @Test void testGetCurrentUser() throws Exception { - // Create a test user for the current user endpoint var currentUser = createTestUser("1", "Current User", "currentuser@example.com", "currentuser", true, true); - // Mock service behavior to return the current user when(service.currentUser()).thenReturn(currentUser); - // Perform GET request to /api/users/current - mockMvc.perform(get("/api/users/current").accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(get("/users/current").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // Expect 200 OK .andExpect( content() diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java index f7151b2afe..a665643887 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java @@ -36,8 +36,6 @@ @WebMvcTest(ViewController.class) public class ViewControllerTest extends BaseControllerTest { - private static final String VIEWS_URL_TEMPLATE = "/api/views/"; - @Autowired private MockMvc mockMvc; @@ -58,12 +56,11 @@ public void setUp() { @Test public void testGetViewsSuccess() throws Exception { - // Mock data for getViews var viewDto = new ViewDto("view1", "View 1", List.of(), 100L); when(viewService.getViews()).thenReturn(List.of(viewDto)); - mockMvc.perform(get(VIEWS_URL_TEMPLATE).contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform(get("/views/").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.views", hasSize(1))) .andExpect(jsonPath("$.views[0].name", is("view1"))) @@ -91,7 +88,7 @@ public void testGetViewDataSuccess() throws Exception { when(queryService.retrieveViewPage(viewRequest)).thenReturn(viewPageDto); - mockMvc.perform(post(VIEWS_URL_TEMPLATE) + mockMvc.perform(post("/views/") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(viewRequest))) .andExpect(status().isOk()) @@ -112,7 +109,7 @@ public void testGetFacetsSuccess() throws Exception { when(viewService.getFacets()).thenReturn(List.of(facetDto)); - mockMvc.perform(get(VIEWS_URL_TEMPLATE + "facets").contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform(get("/views/facets").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.facets", hasSize(1))) .andExpect(jsonPath("$.facets[0].name", is("facet1"))) @@ -132,7 +129,7 @@ public void testCountSuccess() throws Exception { when(queryService.count(countRequest)).thenReturn(countDto); - mockMvc.perform(post(VIEWS_URL_TEMPLATE + "count") + mockMvc.perform(post("/views/count") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(countRequest))) .andExpect(status().isOk()) @@ -147,7 +144,7 @@ public void testGetViewDataValidationFailure() throws Exception { invalidRequestBody.setPage(0); // Invalid page (must be >= 1) invalidRequestBody.setSize(0); // Invalid size (must be >= 1) - mockMvc.perform(post(VIEWS_URL_TEMPLATE) + mockMvc.perform(post("/views/") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(invalidRequestBody))) .andExpect(status().isBadRequest()); diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/VocabularyControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/VocabularyControllerTest.java index 6945fae945..7ea79effe5 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/VocabularyControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/VocabularyControllerTest.java @@ -18,7 +18,7 @@ class VocabularyControllerTest extends BaseControllerTest { @Test void testGetVocabularyWithJsonLd() throws Exception { - mockMvc.perform(get("/api/vocabulary/").header(HttpHeaders.ACCEPT, "application/ld+json")) + mockMvc.perform(get("/vocabulary/").header(HttpHeaders.ACCEPT, "application/ld+json")) .andExpect(status().isOk()) .andExpect(header().string(HttpHeaders.CONTENT_TYPE, "application/ld+json")); } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java index af944fe9db..de09b9f6b5 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java @@ -52,7 +52,7 @@ void createWorkspace_shouldReturnCreatedWorkspace() throws Exception { when(workspaceService.createWorkspace(any(Workspace.class))).thenReturn(workspace); - mockMvc.perform(put("/api/workspaces/") + mockMvc.perform(put("/workspaces/") .contentType(MediaType.APPLICATION_JSON) .content("{\"code\": \"WS001\", \"title\": \"New Workspace\"}")) .andExpect(status().isOk()) // Use isCreated() if HTTP 201 Created is implemented @@ -66,7 +66,7 @@ void listWorkspaces_shouldReturnListOfWorkspaces() throws Exception { when(workspaceService.listWorkspaces()).thenReturn(List.of(workspace)); - mockMvc.perform(get("/api/workspaces/").accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(get("/workspaces/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$[0].code").value("WS001")); @@ -76,8 +76,7 @@ void listWorkspaces_shouldReturnListOfWorkspaces() throws Exception { void deleteWorkspace_shouldDeleteWorkspace() throws Exception { String workspaceUri = "http://example.com/workspace/1"; - mockMvc.perform(delete("/api/workspaces/").param("workspace", workspaceUri)) - .andExpect(status().isNoContent()); + mockMvc.perform(delete("/workspaces/").param("workspace", workspaceUri)).andExpect(status().isNoContent()); Mockito.verify(workspaceService).deleteWorkspace(NodeFactory.createURI(workspaceUri)); } @@ -89,7 +88,7 @@ void getUsers_shouldReturnWorkspaceUsers() throws Exception { when(workspaceService.getUsers(any())).thenReturn(users); - mockMvc.perform(get("/api/workspaces/users/").param("workspace", workspaceUri)) + mockMvc.perform(get("/workspaces/users/").param("workspace", workspaceUri)) .andExpect(status().isOk()) .andExpect(jsonPath("$['http://example.com/user/1']").value("Member")); } @@ -97,7 +96,7 @@ void getUsers_shouldReturnWorkspaceUsers() throws Exception { @Test void setUserRole_shouldUpdateUserRole() throws Exception { mockMvc.perform( - patch("/api/workspaces/users/") + patch("/workspaces/users/") .contentType(MediaType.APPLICATION_JSON) .content( "{\"workspace\": \"http://example.com/workspace/1\", \"user\": \"http://example.com/user/1\", \"role\": \"Manager\"}"))