diff --git a/projects/saturn/build.gradle b/projects/saturn/build.gradle index 30211520da..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') { @@ -75,16 +74,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 +113,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/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/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/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 c1e1c22c04..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.fairspace.saturn.config; - -import io.fairspace.saturn.config.properties.FeatureProperties; -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.UserApp; -import io.fairspace.saturn.services.views.ViewApp; -import io.fairspace.saturn.services.workspaces.WorkspaceApp; - -public class SparkFilterFactory { - public static SaturnSparkFilter createSparkFilter( - String apiPathPrefix, Services svc, FeatureProperties featureProperties) { - 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()), - new FeaturesApp(apiPathPrefix + "/features", featureProperties.getFeatures()), - new MaintenanceApp(apiPathPrefix + "/maintenance", svc.getMaintenanceService())); - } -} 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..69a60edaad --- /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("/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/controller/GlobalExceptionHandler.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/GlobalExceptionHandler.java deleted file mode 100644 index 4f8b4a0199..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/controller/GlobalExceptionHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.fairspace.saturn.controller; - -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; - -@ControllerAdvice -public class GlobalExceptionHandler { - - // todo: add tests - @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity handleAccessDenied(AccessDeniedException ex) { - return new ResponseEntity<>("Access Denied: " + ex.getMessage(), HttpStatus.FORBIDDEN); - } -} 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..5d7911aa9e --- /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("/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/controller/MetadataController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java new file mode 100644 index 0000000000..28e8b0daf8 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java @@ -0,0 +1,101 @@ +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.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.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("/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( + 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, + @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader) { + var model = services.getMetadataService().get(subject, withValueProperties); + var format = getFormat(acceptHeader); + var metadata = serialize(model, format); + return ResponseEntity.ok(metadata); + } + + @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, + @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( + value = "/", + 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/SearchController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java new file mode 100644 index 0000000000..5ac8952143 --- /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("/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/controller/SparqlController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java index c2296b0140..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 @@ -13,8 +13,10 @@ 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("/api/rdf") +@RequestMapping("/rdf") @Validated @RequiredArgsConstructor public class SparqlController { @@ -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 new file mode 100644 index 0000000000..3789614e69 --- /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("/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/controller/ViewController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java new file mode 100644 index 0000000000..cc9cbfc91e --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java @@ -0,0 +1,54 @@ +package io.fairspace.saturn.controller; + +import jakarta.validation.Valid; +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("/views") +@Validated +public class ViewController { + + private final Services services; + + public ViewController(Services services) { + this.services = services; + } + + @GetMapping("/") + public ViewsDto getViews() { + var views = services.getViewService().getViews(); + return new ViewsDto(views); + } + + @PostMapping("/") + public ResponseEntity getViewData(@Valid @RequestBody ViewRequest requestBody) { + var result = services.getQueryService().retrieveViewPage(requestBody); + return ResponseEntity.ok(result); + } + + @GetMapping("/facets") + public ResponseEntity getFacets() { + var facets = services.getViewService().getFacets(); + return ResponseEntity.ok(new FacetsDto(facets)); + } + + @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 new file mode 100644 index 0000000000..0aa5d9da12 --- /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("/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/controller/WorkspaceController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java new file mode 100644 index 0000000000..75c4bc20de --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java @@ -0,0 +1,68 @@ +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.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("/workspaces") +@Validated +public class WorkspaceController { + + private final Services services; + + public WorkspaceController(Services services) { + this.services = services; + } + + @PutMapping("/") + public ResponseEntity createWorkspace(@RequestBody Workspace workspace) { + var createdWorkspace = services.getWorkspaceService().createWorkspace(workspace); + return ResponseEntity.ok(createdWorkspace); + } + + @GetMapping("/") + public ResponseEntity> listWorkspaces() { + var workspaces = services.getWorkspaceService().listWorkspaces(); + return ResponseEntity.ok(workspaces); + } + + @DeleteMapping("/") + @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/") + @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/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/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/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/SearchResultDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java new file mode 100644 index 0000000000..9d86207112 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java @@ -0,0 +1,7 @@ +package io.fairspace.saturn.controller.dto; + +import lombok.Builder; +import lombok.NonNull; + +@Builder +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 new file mode 100644 index 0000000000..4875192363 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java @@ -0,0 +1,8 @@ +package io.fairspace.saturn.controller.dto; + +import java.util.List; + +import lombok.Builder; + +@Builder +public record SearchResultsDto(List results, String query) {} 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/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/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/controller/enums/CustomMediaType.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java new file mode 100644 index 0000000000..cde1da8f21 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java @@ -0,0 +1,13 @@ +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"; + + public static final String APPLICATION_SPARQL_QUERY = "application/sparql-query"; +} 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 new file mode 100644 index 0000000000..d8ac294b15 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java @@ -0,0 +1,58 @@ +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 io.fairspace.saturn.controller.dto.ErrorDto; +import io.fairspace.saturn.services.metadata.validation.ValidationException; + +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + + @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) + 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) + 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 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/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/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/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/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/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/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/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/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/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/SearchResultDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultDTO.java deleted file mode 100644 index e8bdcfe426..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultDTO.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.fairspace.saturn.services.search; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; - -@Value -@Builder -public class SearchResultDTO { - @NonNull - String id; - - String label; - String type; - String comment; -} 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/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/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 861f893deb..0ab6efc8e8 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 @@ -43,6 +43,11 @@ import io.fairspace.saturn.config.ViewsConfig.View; import io.fairspace.saturn.config.properties.JenaProperties; 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; @@ -108,7 +113,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); @@ -144,7 +149,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) @@ -153,10 +158,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) { @@ -194,7 +199,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) @@ -207,13 +212,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) @@ -221,7 +226,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) { @@ -435,7 +440,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); @@ -451,13 +456,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..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,8 +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.services.search.FileSearchRequest; -import io.fairspace.saturn.services.search.SearchResultDTO; +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; import io.fairspace.saturn.vocabulary.FS; import static io.fairspace.saturn.config.ViewsConfig.ColumnType.Date; @@ -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); @@ -610,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(); } @@ -653,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/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/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/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java index aca6bf19a0..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; 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 7d789372ab..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: @@ -74,7 +76,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/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 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 d61450c418..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.FeatureProperties; - -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 FeatureProperties())); - } -} 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..afa3c256f9 --- /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) +public 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/FeaturesControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java new file mode 100644 index 0000000000..c4bbd18905 --- /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("/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 new file mode 100644 index 0000000000..a93447ef18 --- /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("/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("/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("/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("/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 new file mode 100644 index 0000000000..db55df951d --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java @@ -0,0 +1,126 @@ +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("/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("/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("/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("/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("/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("/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 new file mode 100644 index 0000000000..a266d278a4 --- /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("/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("/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/controller/UserControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java new file mode 100644 index 0000000000..70e7587cac --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java @@ -0,0 +1,145 @@ +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("/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("/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 { + var currentUser = createTestUser("1", "Current User", "currentuser@example.com", "currentuser", true, true); + + when(service.currentUser()).thenReturn(currentUser); + + mockMvc.perform(get("/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; + } +} 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..a665643887 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java @@ -0,0 +1,152 @@ +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.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.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) +public class ViewControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ViewService viewService; + + @MockBean + private QueryService queryService; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeEach + public void setUp() { + when(getService().getViewService()).thenReturn(viewService); + when(getService().getQueryService()).thenReturn(queryService); + } + + @Test + public void testGetViewsSuccess() throws Exception { + var viewDto = new ViewDto("view1", "View 1", List.of(), 100L); + + when(viewService.getViews()).thenReturn(List.of(viewDto)); + + mockMvc.perform(get("/views/").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 testGetViewDataSuccess() 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/") + .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/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/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 testGetViewDataValidationFailure() 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/") + .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 new file mode 100644 index 0000000000..7ea79effe5 --- /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("/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 new file mode 100644 index 0000000000..de09b9f6b5 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java @@ -0,0 +1,119 @@ +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.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.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.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) +class WorkspaceControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private WorkspaceService workspaceService; + + @BeforeEach + void setUp() { + when(getService().getWorkspaceService()).thenReturn(workspaceService); + } + + @Test + void createWorkspace_shouldReturnCreatedWorkspace() throws Exception { + Workspace workspace = new Workspace(); + workspace.setCode("WS001"); + + when(workspaceService.createWorkspace(any(Workspace.class))).thenReturn(workspace); + + 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 + .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("/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("/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("/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("/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 + } +} 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() {} + } +} 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()); + } +} 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)); } 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..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 @@ -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; @@ -183,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 @@ -198,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 @@ -213,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 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; 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 c66f2bb6f3..11c3e3e89e 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 @@ -27,6 +27,8 @@ import io.fairspace.saturn.config.properties.SearchProperties; import io.fairspace.saturn.config.properties.StoreParamsProperties; 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; @@ -192,24 +194,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); } @@ -219,7 +216,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 @@ -227,7 +224,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 @@ -237,7 +234,7 @@ public void testCountResourceWithAccess() { request.setView("Resource"); var result = queryService.count(request); - Assert.assertEquals(3, result.getCount()); + Assert.assertEquals(3, result.count()); } @Test @@ -246,7 +243,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