From 62a0406302fec42bd256f3d32c2e9252047e1d04 Mon Sep 17 00:00:00 2001 From: southeo Date: Wed, 20 Nov 2024 11:20:47 +0100 Subject: [PATCH] Feature/update readme and swagger (#151) * swagger endpoint doc * swagger * trivy * annotation controller swagger * digital media swagger * digital specimen swagger * mjr swagger * readme * code review * trivy --- README.md | 46 ++-- pom.xml | 23 ++ .../controller/AnnotationController.java | 196 +++++++++++--- .../backend/controller/BaseController.java | 25 +- .../controller/DigitalMediaController.java | 141 ++++++++-- .../controller/DigitalSpecimenController.java | 256 +++++++++++++++--- .../controller/MasJobRecordController.java | 64 ++++- .../dissco/backend/domain/MasJobRequest.java | 10 +- .../batch/AnnotationEventRequest.java | 7 +- .../domain/jsonapi/JsonApiRequest.java | 10 - .../domain/jsonapi/JsonApiRequestWrapper.java | 5 - .../openapi/annotation/AnnotationRequest.java | 17 ++ .../annotation/AnnotationResponseData.java | 12 + .../annotation/AnnotationResponseList.java | 14 + .../annotation/AnnotationResponseSingle.java | 11 + .../BatchAnnotationCountRequest.java | 25 ++ .../BatchAnnotationCountResponse.java | 27 ++ .../annotation/BatchAnnotationRequest.java | 17 ++ .../media/DigitalMediaResponseData.java | 13 + .../media/DigitalMediaResponseList.java | 14 + .../media/DigitalMediaResponseSingle.java | 12 + .../openapi/shared/MasResponseList.java | 25 ++ .../openapi/shared/MasSchedulingRequest.java | 29 ++ .../openapi/shared/MjrResponseData.java | 12 + .../openapi/shared/MjrResponseList.java | 16 ++ .../openapi/shared/MjrResponseSingle.java | 14 + .../openapi/shared/VersionResponse.java | 24 ++ .../openapi/specimen/AggregationResponse.java | 35 +++ .../specimen/DigitalSpecimenResponseData.java | 12 + .../specimen/DigitalSpecimenResponseFull.java | 31 +++ .../specimen/DigitalSpecimenResponseList.java | 13 + .../DigitalSpecimenResponseSingle.java | 12 + .../backend/service/AnnotationService.java | 33 +-- .../controller/AnnotationControllerTest.java | 51 +++- .../DigitalMediaControllerTest.java | 6 +- .../DigitalSpecimenControllerTest.java | 15 +- .../service/AnnotationServiceTest.java | 65 ----- .../MachineAnnotationServiceServiceTest.java | 6 +- .../service/MasJobRecordServiceTest.java | 3 +- .../dissco/backend/utils/AnnotationUtils.java | 19 +- .../utils/MachineAnnotationServiceUtils.java | 25 +- 41 files changed, 1082 insertions(+), 309 deletions(-) delete mode 100644 src/main/java/eu/dissco/backend/domain/jsonapi/JsonApiRequest.java delete mode 100644 src/main/java/eu/dissco/backend/domain/jsonapi/JsonApiRequestWrapper.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationRequest.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseData.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseList.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseSingle.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationCountRequest.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationCountResponse.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationRequest.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseData.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseList.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseSingle.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/shared/MasResponseList.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/shared/MasSchedulingRequest.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseData.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseList.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseSingle.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/shared/VersionResponse.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/specimen/AggregationResponse.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseData.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseFull.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseList.java create mode 100644 src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseSingle.java diff --git a/README.md b/README.md index 5e1854b6..ed4c20bd 100644 --- a/README.md +++ b/README.md @@ -7,30 +7,24 @@ In general, the find/search APIs are open, and APIs for create, update and delet All endpoints are based on the [JSON:API](https://jsonapi.org/) specification. It follows the guidelines and best practices described in [BiCIKL Deliverable 1.3](https://docs.google.com/document/d/1RgngKSPabEs-Pir6vA25iFDgVorbEZe7duT7L7vQ7QI) -The organisation endpoints are an exception. The backend provides APIs for the following objects: - Digital Specimens - Digital Media Objects - Annotations -- Users -- Organisations ## Storage solutions In general, there are three places where data is stored: -- Postgres database -The Postgres database stores the latest active version of the object. +- **Postgres database**: The Postgres database stores the latest active version of the object. It is used in retrieving a specific object based on the id. It can also be used to combine objects based on their relationships. -- Elasticsearch -This data storage is used for searching and aggregating. +- **Elasticsearch**: This data storage is used for searching and aggregating. It is used for data discovery and provides endpoints for the filters and search fields. In general, it does not return a single object, but a paginated list of objects. Additionally, it can provide aggregations showing how many items comply to the search criteria. -- MongoDB -MongoDB is used for provenance storage and stores the historical data. +- **MongoDB**: MongoDB is used for provenance storage and stores the historical data. This data storage is used for displaying previous versions of the data. ## API documentation @@ -38,9 +32,6 @@ The APIs are documented through code-generated OpenAPI v3 and Swagger. - The swagger endpoint is: https://sandbox.dissco.tech/api/swagger-ui/index.html#/ - The OpenAPI endpoint is: https://sandbox.dissco.tech/api/v3/api-docs -As we use generic objects, the API documentation is not as detailed as we would like. -We are looking at additional options for more detailed documentation. - ### Digital Specimens For Digital Specimen, we only provide search and read functionality through the APIs. It follows a generic structure. @@ -79,24 +70,10 @@ The create, update and tombstone endpoints are protected through authentication. The actions posted to these endpoints are forwarded to the annotation processor. This processor manages all the annotations and is the only application authorized to create or change annotations. -### User -For users, we provide a set of read, create, update and delete functionality through the APIs. -When a new user is registered it will be created in the database. -A user can update his or her information through the profile page. -The backend will then update the user information in the database. - -### Organisation -The organisation endpoints provide read functionality for the organisations. -These organisations are inserted in the database and need to be updated manually. -The organisation endpoints can be used by forms. - -### Organisation document -The organisation documents can be used to insert documents for a particular organisation. -These endpoints can be used to insert the result of forms. - ## Run locally To run the system locally, it can be run from an IDEA. Clone the code and fill in the application properties (see below). +The application requires a connection to an elastic search instance and a mongodb instance. The application needs a connection to a Postgres database, MongoDB and Elasticsearch. For creation and modification of annotations it needs a reachable annotation processor service. @@ -121,10 +98,23 @@ elasticsearch.port=# The port of the Elasticsearch cluster #Oauth properties spring.security.oauth2.resourceserver.jwt.issuer-uri=# The URI to the JWT issuer spring.security.oauth2.authorizationserver.endpoint.jwk-set-uri=# The URI to the JWT OpenId certifications +token.secret=# Keycloak secret +token.id=# Keycloak client id +token.grant-type= # Keycloak grant type + +#Kafka properties +kafka.publisher.host=# Endpoint for kafka publisher #MongoDB properties mongo.connection-string=# Connection string to MongoDB mongo.database=# Database name of MongoDB #Feign clients -feign.annotations=# Path to annotation proccessor endpoint \ No newline at end of file +feign.annotations=# Path to annotation proccessor endpoint + +#Endpoints +endpoint.handle-endpoint=# Endpoint to handle API +endpoint.token-endpoint=# Endpoint to keycloak authenticator + +#Application Properties +application.base-url=# The url of the application (used to build JsonApiLinks objects) \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4c44e4f6..e6318c52 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,11 @@ springdoc-openapi-starter-webmvc-ui ${springdoc.version} + + org.springdoc + springdoc-openapi-starter-webflux-ui + ${springdoc.version} + org.springframework.boot spring-boot-starter-webflux @@ -249,6 +254,24 @@ false + + org.springdoc + springdoc-openapi-maven-plugin + 1.4 + + + integration-test + + generate + + + openapi.yaml + ${project.build.directory} + + + + + org.codehaus.mojo exec-maven-plugin diff --git a/src/main/java/eu/dissco/backend/controller/AnnotationController.java b/src/main/java/eu/dissco/backend/controller/AnnotationController.java index 96a782c8..f8a72bf0 100644 --- a/src/main/java/eu/dissco/backend/controller/AnnotationController.java +++ b/src/main/java/eu/dissco/backend/controller/AnnotationController.java @@ -8,8 +8,14 @@ import eu.dissco.backend.component.SchemaValidatorComponent; import eu.dissco.backend.domain.annotation.batch.AnnotationEventRequest; import eu.dissco.backend.domain.jsonapi.JsonApiListResponseWrapper; -import eu.dissco.backend.domain.jsonapi.JsonApiRequestWrapper; import eu.dissco.backend.domain.jsonapi.JsonApiWrapper; +import eu.dissco.backend.domain.openapi.annotation.AnnotationRequest; +import eu.dissco.backend.domain.openapi.annotation.AnnotationResponseList; +import eu.dissco.backend.domain.openapi.annotation.AnnotationResponseSingle; +import eu.dissco.backend.domain.openapi.shared.VersionResponse; +import eu.dissco.backend.domain.openapi.annotation.BatchAnnotationCountRequest; +import eu.dissco.backend.domain.openapi.annotation.BatchAnnotationCountResponse; +import eu.dissco.backend.domain.openapi.annotation.BatchAnnotationRequest; import eu.dissco.backend.exceptions.ForbiddenException; import eu.dissco.backend.exceptions.InvalidAnnotationRequestException; import eu.dissco.backend.exceptions.NoAnnotationFoundException; @@ -17,6 +23,12 @@ import eu.dissco.backend.properties.ApplicationProperties; import eu.dissco.backend.schema.AnnotationProcessingRequest; import eu.dissco.backend.service.AnnotationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; import lombok.extern.slf4j.Slf4j; @@ -56,30 +68,53 @@ public AnnotationController( this.schemaValidator = schemaValidator; } + @Operation(summary = "Get annotation by id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Annotation retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseSingle.class)) + }) + }) @GetMapping(value = "/{prefix}/{suffix}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getAnnotation(@PathVariable("prefix") String prefix, - @PathVariable("suffix") String suffix, HttpServletRequest request) { + public ResponseEntity getAnnotation( + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, + HttpServletRequest request) { var id = prefix + '/' + suffix; log.info("Received get request for annotationRequests: {}", id); var annotation = service.getAnnotation(id, getPath(request)); return ResponseEntity.ok(annotation); } + @Operation(summary = "Get latest annotations, paginated") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Annotations retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseList.class)) + }) + }) @GetMapping(value = "/latest", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getLatestAnnotations( - @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, - @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, HttpServletRequest request) + @Parameter(description = PAGE_NUM_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, + @Parameter(description = PAGE_SIZE_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, + HttpServletRequest request) throws IOException { - log.info("Received get request for latest paginated annotationRequests. Page number: {}, page size {}", + log.info( + "Received get request for latest paginated annotationRequests. Page number: {}, page size {}", pageNumber, pageSize); var annotations = service.getLatestAnnotations(pageNumber, pageSize, getPath(request)); return ResponseEntity.ok(annotations); } + @Operation(summary = "Get annotation by ID and desired version") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Annotation successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseSingle.class)) + }) + }) @GetMapping(value = "/{prefix}/{suffix}/{version}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getAnnotationByVersion( - @PathVariable("prefix") String prefix, - @PathVariable("suffix") String suffix, @PathVariable("version") int version, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, + @Parameter(description = VERSION_OAS) @PathVariable("version") int version, HttpServletRequest request) throws JsonProcessingException, NotFoundException { var id = HANDLE_STRING + prefix + '/' + suffix; @@ -88,21 +123,44 @@ public ResponseEntity getAnnotationByVersion( return ResponseEntity.ok(annotation); } - + @Operation(summary = "Get annotations, paginated") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Annotations retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseList.class)) + }) + }) @GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getAnnotations( - @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, - @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, HttpServletRequest request) { - log.info("Received get request for json paginated annotationRequests. Page number: {}, page size {}", + @Parameter(description = PAGE_NUM_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, + @Parameter(description = PAGE_SIZE_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, + HttpServletRequest request) { + log.info( + "Received get request for json paginated annotationRequests. Page number: {}, page size {}", pageNumber, pageSize); var annotations = service.getAnnotations(pageNumber, pageSize, getPath(request)); return ResponseEntity.ok(annotations); } + @Operation( + summary = "Create annotation", + description = """ + Create an annotation on a digital specimen or digital media. Only users who have + registered their ORCID may create annotations. + """) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Annotation successfully created", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseSingle.class)) + }) + }) @ResponseStatus(HttpStatus.CREATED) @PostMapping(value = "", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createAnnotation(Authentication authentication, - @RequestBody JsonApiRequestWrapper requestBody, HttpServletRequest request) + public ResponseEntity createAnnotation( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Annotation adhering to JSON:API standard", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = AnnotationRequest.class))) + Authentication authentication, + @RequestBody AnnotationRequest requestBody, HttpServletRequest request) throws JsonProcessingException, ForbiddenException { var annotation = getAnnotationFromRequest(requestBody); var agent = getAgent(authentication); @@ -115,20 +173,52 @@ public ResponseEntity createAnnotation(Authentication authentica } } + @Operation(summary = "Given a set of search parameters, calculates how many objects would be annotated in a batch annotation event", + description = """ + Given a set of search parameters, calculates how many objects would be annotated in a batch annotation event. + This is a prerequisite for applying batch annotations. This can only be requested by users with the "batch annotations" permission. + """) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Projected Annotation count calculated", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = BatchAnnotationCountResponse.class)) + }) + }) @PreAuthorize("hasRole('dissco-web-batch-annotations')") @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/batch", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCountForBatchAnnotations(@RequestBody JsonNode request) throws IOException { + public ResponseEntity getCountForBatchAnnotations( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Annotation adhering to JSON:API standard", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = BatchAnnotationCountRequest.class))) + @RequestBody BatchAnnotationCountRequest request) + throws IOException { log.info("Received request for batch annotation count"); var result = service.getCountForBatchAnnotations(request); return ResponseEntity.ok(result); } + @Operation(summary = "Apply an annotation to all objects that match a given criteria", + description = """ + Given a set of search parameters, applies an annotation to all objects that match this criteria. + The first annotation created, which is the annotation on the provided target, is returned. + Subsequent annotations are scheduled. This is only possible for users with the "batch annotations" permission. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Batching scheduled; initial annotation returned", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseSingle.class)) + }) + }) @PreAuthorize("hasRole('dissco-web-batch-annotations')") @ResponseStatus(HttpStatus.CREATED) @PostMapping(value = "/batch", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity createAnnotationBatch(Authentication authentication, - @RequestBody JsonApiRequestWrapper requestBody, HttpServletRequest request) + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Annotation batch request", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = BatchAnnotationRequest.class))) + @RequestBody BatchAnnotationRequest requestBody, HttpServletRequest request) throws JsonProcessingException, ForbiddenException, InvalidAnnotationRequestException { var event = getAnnotationFromRequestEvent(requestBody); schemaValidator.validateAnnotationEventRequest(event, true); @@ -142,11 +232,27 @@ public ResponseEntity createAnnotationBatch(Authentication authe } } + @Operation( + summary = "Update existing annotation", + description = """ + Update an existing annotation. Users may only update annotations they have created. + """) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Annotation successfully updated", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseSingle.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @PatchMapping(value = "/{prefix}/{suffix}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity updateAnnotation(Authentication authentication, - @RequestBody JsonApiRequestWrapper requestBody, @PathVariable("prefix") String prefix, - @PathVariable("suffix") String suffix, HttpServletRequest request) + public ResponseEntity updateAnnotation( + @io.swagger.v3.oas.annotations.parameters.RequestBody + (description = "Annotation adhering to JSON:API standard", + content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationRequest.class))}) + Authentication authentication, @RequestBody AnnotationRequest requestBody, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, + HttpServletRequest request) throws NoAnnotationFoundException, JsonProcessingException, ForbiddenException { var id = prefix + '/' + suffix; var agent = getAgent(authentication); @@ -161,12 +267,18 @@ public ResponseEntity updateAnnotation(Authentication authentica } } - @PreAuthorize("isAuthenticated()") + @Operation(summary = "Get all annotations for an authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Annotations successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseList.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/creator", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getAnnotationsForUser( - @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, - @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, HttpServletRequest request, + @Parameter(description = PAGE_NUM_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, + @Parameter(description = PAGE_SIZE_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, + HttpServletRequest request, Authentication authentication) throws IOException, ForbiddenException { var orcid = getAgent(authentication).getId(); log.info("Received get request to show all annotationRequests for user: {}", orcid); @@ -175,24 +287,40 @@ public ResponseEntity getAnnotationsForUser( return ResponseEntity.ok(annotations); } + @Operation(summary = "Get all versions for a given annotation") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Annotation versions successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = VersionResponse.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}/versions", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getAnnotationVersions(@PathVariable("prefix") String prefix, - @PathVariable("suffix") String suffix, HttpServletRequest request) throws NotFoundException { + public ResponseEntity getAnnotationVersions( + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, + HttpServletRequest request) throws NotFoundException { var id = HANDLE_STRING + prefix + '/' + suffix; log.info("Received get request for versions of annotationRequests with id: {}", id); var versions = service.getAnnotationVersions(id, getPath(request)); return ResponseEntity.ok(versions); } + @Operation(summary = "Tombstone a given annotation", + description = """ + Tombstone a given annotation. Users may only tombstone annotations they created. + """) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Annotation successfully tombstoned")}) @ResponseStatus(HttpStatus.NO_CONTENT) @DeleteMapping(value = "/{prefix}/{suffix}") public ResponseEntity tombstoneAnnotation(Authentication authentication, - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix) + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix) throws NoAnnotationFoundException, ForbiddenException { var agent = getAgent(authentication); var isAdmin = isAdmin(authentication); - log.info("Received delete for annotationRequests: {} from user: {}", (prefix + suffix), agent.getId()); + log.info("Received delete for annotationRequests: {} from user: {}", (prefix + suffix), + agent.getId()); var success = service.tombstoneAnnotation(prefix, suffix, agent, isAdmin); if (success) { return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); @@ -201,22 +329,22 @@ public ResponseEntity tombstoneAnnotation(Authentication authentication, } } - private AnnotationProcessingRequest getAnnotationFromRequest(JsonApiRequestWrapper requestBody) - throws JsonProcessingException { + private AnnotationProcessingRequest getAnnotationFromRequest(AnnotationRequest requestBody) { if (!requestBody.data().type().equals(ANNOTATION_TYPE)) { throw new IllegalArgumentException( - "Invalid type. Type must be " + ANNOTATION_TYPE + " but was " + requestBody.data().type()); + "Invalid type. Type must be " + ANNOTATION_TYPE + " but was " + requestBody.data() + .type()); } - return mapper.treeToValue(requestBody.data().attributes(), AnnotationProcessingRequest.class); + return requestBody.data().attributes(); } - private AnnotationEventRequest getAnnotationFromRequestEvent(JsonApiRequestWrapper requestBody) - throws JsonProcessingException { + private AnnotationEventRequest getAnnotationFromRequestEvent(BatchAnnotationRequest requestBody) { if (!requestBody.data().type().equals(ANNOTATION_TYPE)) { throw new IllegalArgumentException( - "Invalid type. Type must be " + ANNOTATION_TYPE + " but was " + requestBody.data().type()); + "Invalid type. Type must be " + ANNOTATION_TYPE + " but was " + requestBody.data() + .type()); } - return mapper.treeToValue(requestBody.data().attributes(), AnnotationEventRequest.class); + return requestBody.data().attributes(); } @ExceptionHandler(NoAnnotationFoundException.class) diff --git a/src/main/java/eu/dissco/backend/controller/BaseController.java b/src/main/java/eu/dissco/backend/controller/BaseController.java index 4545d886..d0de1a85 100644 --- a/src/main/java/eu/dissco/backend/controller/BaseController.java +++ b/src/main/java/eu/dissco/backend/controller/BaseController.java @@ -2,10 +2,9 @@ import static eu.dissco.backend.schema.Identifier.OdsGupriLevel.GLOBALLY_UNIQUE_STABLE_PERSISTENT_RESOLVABLE; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import eu.dissco.backend.domain.MasJobRequest; -import eu.dissco.backend.domain.jsonapi.JsonApiRequestWrapper; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest; import eu.dissco.backend.exceptions.ConflictException; import eu.dissco.backend.exceptions.ForbiddenException; import eu.dissco.backend.properties.ApplicationProperties; @@ -34,12 +33,20 @@ public abstract class BaseController { public static final String DATE_STRING = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; protected static final String DEFAULT_PAGE_NUM = "1"; protected static final String DEFAULT_PAGE_SIZE = "10"; - private static final TypeReference> LIST_TYPE = new TypeReference<>() { - }; private static final String ORCID = "orcid"; protected final ObjectMapper mapper; private final ApplicationProperties applicationProperties; + // OpenAPI Messages + protected static final String PAGE_NUM_OAS = "Desired page number"; + protected static final String PAGE_SIZE_OAS = "Desired page size"; + + protected static final String PREFIX_OAS = "Prefix of target ID"; + protected static final String SUFFIX_OAS = "Suffix of target ID"; + + protected static final String VERSION_OAS = "Desired version"; + protected static final String JOB_STATUS_OAS ="Optional filter on job status"; + protected static Agent getAgent(Authentication authentication) throws ForbiddenException { var claims = ((Jwt) authentication.getPrincipal()).getClaims(); @@ -87,7 +94,7 @@ protected static boolean isAdmin(Authentication authentication) { return (roles.contains("dissco-admin")); } catch (NullPointerException e) { return false; - } catch (ClassCastException e){ + } catch (ClassCastException e) { log.warn("Unable to read claims", e); return false; } @@ -103,16 +110,16 @@ protected String getPath(HttpServletRequest request) { return path; } - protected Map getMassRequestFromRequest(JsonApiRequestWrapper requestBody) + protected Map getMassRequestFromRequest(MasSchedulingRequest requestBody) throws ConflictException { if (!requestBody.data().type().equals("MasRequest")) { throw new ConflictException(); } - if (requestBody.data().attributes().get("mass") == null) { + if (requestBody.data().attributes().mass() == null) { throw new IllegalArgumentException(); } - var list = mapper.convertValue(requestBody.data().attributes().get("mass"), LIST_TYPE); - return list.stream().collect(Collectors.toMap(MasJobRequest::masId, Function.identity())); + return requestBody.data().attributes().mass().stream() + .collect(Collectors.toMap(MasJobRequest::masId, Function.identity())); } } diff --git a/src/main/java/eu/dissco/backend/controller/DigitalMediaController.java b/src/main/java/eu/dissco/backend/controller/DigitalMediaController.java index 691caf48..9e8d10d5 100644 --- a/src/main/java/eu/dissco/backend/controller/DigitalMediaController.java +++ b/src/main/java/eu/dissco/backend/controller/DigitalMediaController.java @@ -7,14 +7,25 @@ import com.fasterxml.jackson.databind.ObjectMapper; import eu.dissco.backend.database.jooq.enums.JobState; import eu.dissco.backend.domain.jsonapi.JsonApiListResponseWrapper; -import eu.dissco.backend.domain.jsonapi.JsonApiRequestWrapper; import eu.dissco.backend.domain.jsonapi.JsonApiWrapper; +import eu.dissco.backend.domain.openapi.annotation.AnnotationResponseList; +import eu.dissco.backend.domain.openapi.media.DigitalMediaResponseList; +import eu.dissco.backend.domain.openapi.media.DigitalMediaResponseSingle; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest; +import eu.dissco.backend.domain.openapi.shared.MjrResponseList; +import eu.dissco.backend.domain.openapi.shared.VersionResponse; import eu.dissco.backend.exceptions.ConflictException; import eu.dissco.backend.exceptions.ForbiddenException; import eu.dissco.backend.exceptions.NotFoundException; import eu.dissco.backend.exceptions.PidCreationException; import eu.dissco.backend.properties.ApplicationProperties; import eu.dissco.backend.service.DigitalMediaService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -45,20 +56,34 @@ public DigitalMediaController(ApplicationProperties applicationProperties, this.service = service; } + @Operation(summary = "Get paginated digital media") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital media successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = DigitalMediaResponseList.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getDigitalMediaObjects( - @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, - @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, HttpServletRequest request) { - log.info("Received get request for digital media objects in json format"); + @Parameter(description = PREFIX_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, + @Parameter(description = SUFFIX_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, + HttpServletRequest request) { + log.info("Received get request for digital digital medias in json format"); var digitalMedia = service.getDigitalMediaObjects(pageNumber, pageSize, getPath(request)); return ResponseEntity.ok(digitalMedia); } + @Operation(summary = "Get digital media by ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital media successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = DigitalMediaResponseSingle.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getDigitalMediaObjectById( - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, HttpServletRequest request) { var id = prefix + '/' + suffix; log.info("Received get request for multiMedia with id: {}", id); @@ -66,10 +91,17 @@ public ResponseEntity getDigitalMediaObjectById( return ResponseEntity.ok(multiMedia); } + @Operation(summary = "Get annotations for a given digital media") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital media annotations successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseList.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}/annotations", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getMediaAnnotationsById( - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, HttpServletRequest request) { var id = prefix + '/' + suffix; log.info("Received get request for annotationRequests on digitalMedia with id: {}", id); @@ -77,10 +109,17 @@ public ResponseEntity getMediaAnnotationsById( return ResponseEntity.ok(annotations); } + @Operation(summary = "Get all versions for a given digital media") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital media versions successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = VersionResponse.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}/versions", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getDigitalMediaVersions( - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, HttpServletRequest request) throws NotFoundException { var id = DOI_STRING + prefix + '/' + suffix; @@ -89,10 +128,18 @@ public ResponseEntity getDigitalMediaVersions( return ResponseEntity.ok(versions); } + @Operation(summary = "Get digital media by ID and desired version") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital media successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = DigitalMediaResponseSingle.class)) + }) + }) @GetMapping(value = "/{prefix}/{suffix}/{version}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getDigitalMediaObjectByVersion( - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, - @PathVariable("version") int version, HttpServletRequest request) + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, + @Parameter(description = VERSION_OAS) @PathVariable("version") int version, + HttpServletRequest request) throws JsonProcessingException, NotFoundException { var id = DOI_STRING + prefix + '/' + suffix; log.info("Received get request for digital media: {} with version: {}", id, version); @@ -100,9 +147,22 @@ public ResponseEntity getDigitalMediaObjectByVersion( return ResponseEntity.ok(digitalMedia); } + @Operation( + summary = "Get MASs that may be run on the given digital media", + description = """ + Retrieves a list of Machine Annotation Services (MASs) suitable for processing a given + digital media, based on the MASs' respective filter criteria. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital media MASs successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseList.class)) + }) + }) @GetMapping(value = "/{prefix}/{suffix}/mas", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getMassForDigitalMediaObject( - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, HttpServletRequest request) { var id = prefix + '/' + suffix; log.info("Received get request for mass for digital media: {}", id); @@ -110,13 +170,26 @@ public ResponseEntity getMassForDigitalMediaObject( return ResponseEntity.ok(mass); } + @Operation( + summary = "Get MAS jobs for digital media", + description = """ + Retrieves a list of Machine Annotation Service Job Records (MJRs). + These are scheduled, running, or completed machine annotation service jobs. + Pagination is offered. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "MAS Job records successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = MjrResponseList.class)) + }) + }) @GetMapping(value = "/{prefix}/{suffix}/mjr", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getMasJobRecordForMedia( - @PathVariable("prefix") String prefix, - @PathVariable("suffix") String suffix, - @RequestParam(required = false) JobState state, - @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, - @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, + @Parameter(description = JOB_STATUS_OAS) @RequestParam(required = false) JobState state, + @Parameter(description = PAGE_NUM_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, + @Parameter(description = PAGE_SIZE_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, HttpServletRequest request ) throws NotFoundException { var path = getPath(request); @@ -125,28 +198,52 @@ public ResponseEntity getMasJobRecordForMedia( service.getMasJobRecordsForMedia(id, path, state, pageNumber, pageSize)); } + @Operation( + summary = "Get original digital media data", + description = """ + DiSSCo provides harmonised data according to the OpenDS specification. + This endpoint provides the unharmonised data as it appears in the source system. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Original Data successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = JsonApiWrapper.class)) + }) + }) @GetMapping(value = "/{prefix}/{suffix}/original-data", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getOriginalDataForMedia( - @PathVariable("prefix") String prefix, - @PathVariable("suffix") String suffix, - HttpServletRequest request){ + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, + HttpServletRequest request) { var path = getPath(request); var id = prefix + '/' + suffix; return ResponseEntity.ok(service.getOriginalDataForMedia(id, path)); } + @Operation( + summary = "Schedule Machine Annotation Services", + description = """ + Schedules applicable MASs on a given digital media. + Only users who have provided their ORCID may schedule MASs. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "MAS successfully scheduled", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = MjrResponseList.class)) + }) + }) @PostMapping(value = "/{prefix}/{suffix}/mas", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity scheduleMassForDigitalMediaObject( - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, - @RequestBody JsonApiRequestWrapper requestBody, Authentication authentication, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = PREFIX_OAS) @PathVariable("suffix") String suffix, + @RequestBody MasSchedulingRequest requestBody, Authentication authentication, HttpServletRequest request) throws ConflictException, ForbiddenException, PidCreationException, NotFoundException { var orcid = getAgent(authentication).getId(); var id = prefix + '/' + suffix; var masRequests = getMassRequestFromRequest(requestBody); log.info("Received request to schedule all relevant MASs of: {} on digital media: {}", - masRequests, - id); + masRequests, id); var massResponse = service.scheduleMass(id, masRequests, getPath(request), orcid); return ResponseEntity.accepted().body(massResponse); diff --git a/src/main/java/eu/dissco/backend/controller/DigitalSpecimenController.java b/src/main/java/eu/dissco/backend/controller/DigitalSpecimenController.java index ba96dce7..e615f08c 100644 --- a/src/main/java/eu/dissco/backend/controller/DigitalSpecimenController.java +++ b/src/main/java/eu/dissco/backend/controller/DigitalSpecimenController.java @@ -6,8 +6,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import eu.dissco.backend.database.jooq.enums.JobState; import eu.dissco.backend.domain.jsonapi.JsonApiListResponseWrapper; -import eu.dissco.backend.domain.jsonapi.JsonApiRequestWrapper; import eu.dissco.backend.domain.jsonapi.JsonApiWrapper; +import eu.dissco.backend.domain.openapi.annotation.AnnotationResponseList; +import eu.dissco.backend.domain.openapi.media.DigitalMediaResponseList; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest; +import eu.dissco.backend.domain.openapi.shared.MjrResponseList; +import eu.dissco.backend.domain.openapi.shared.VersionResponse; +import eu.dissco.backend.domain.openapi.specimen.AggregationResponse; +import eu.dissco.backend.domain.openapi.specimen.DigitalSpecimenResponseFull; +import eu.dissco.backend.domain.openapi.specimen.DigitalSpecimenResponseList; +import eu.dissco.backend.domain.openapi.specimen.DigitalSpecimenResponseSingle; import eu.dissco.backend.exceptions.ConflictException; import eu.dissco.backend.exceptions.ForbiddenException; import eu.dissco.backend.exceptions.NotFoundException; @@ -15,6 +23,12 @@ import eu.dissco.backend.exceptions.UnknownParameterException; import eu.dissco.backend.properties.ApplicationProperties; import eu.dissco.backend.service.DigitalSpecimenService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; import lombok.extern.slf4j.Slf4j; @@ -47,41 +61,71 @@ public DigitalSpecimenController(ApplicationProperties applicationProperties, Ob this.service = service; } + @Operation(summary = "Get paginated digital specimen") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital specimens successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = DigitalSpecimenResponseList.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getSpecimen( - @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, - @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, HttpServletRequest request) - throws IOException { + @Parameter(description = PAGE_NUM_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, + @Parameter(description = PAGE_SIZE_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, + HttpServletRequest request) throws IOException { log.info("Received get request for specimen"); var specimen = service.getSpecimen(pageNumber, pageSize, getPath(request)); return ResponseEntity.ok(specimen); } + @Operation(summary = "Get latest paginated digital specimens") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital specimens successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = DigitalSpecimenResponseList.class)) + }) + }) @GetMapping(value = "/latest", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getLatestSpecimen( - @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, - @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, HttpServletRequest request) - throws IOException { + @Parameter(description = PAGE_NUM_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, + @Parameter(description = PAGE_SIZE_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, + HttpServletRequest request) throws IOException { log.info("Received get request for latest digital specimen"); var specimens = service.getLatestSpecimen(pageNumber, pageSize, getPath(request)); return ResponseEntity.ok(specimens); } + @Operation(summary = "Get digital specimen by ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital specimen successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = DigitalSpecimenResponseSingle.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getSpecimenById(@PathVariable("prefix") String prefix, - @PathVariable("suffix") String suffix, HttpServletRequest request) { + public ResponseEntity getSpecimenById( + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, + HttpServletRequest request) { var id = prefix + '/' + suffix; log.info("Received get request for specimen with id: {}", id); var specimen = service.getSpecimenById(id, getPath(request)); return ResponseEntity.ok(specimen); } + @Operation(summary = "Get full digital specimen by ID", + description = """ + Returns full version of a given digital specimen, including digital media associated with the specimen and annotations. + """) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital specimen successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = DigitalSpecimenResponseFull.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}/full", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getSpecimenByIdFull( - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, HttpServletRequest request) { var id = prefix + '/' + suffix; log.info("Received get request for full specimen with id: {}", id); @@ -89,11 +133,21 @@ public ResponseEntity getSpecimenByIdFull( return ResponseEntity.ok(specimen); } + @Operation(summary = "Get full digital specimen by ID and version", + description = """ + Returns full version of a given digital specimen, including digital media associated with the specimen and annotations. + """) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital specimen successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = DigitalSpecimenResponseFull.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}/{version}/full", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getSpecimenByVersionFull( - @PathVariable("prefix") String prefix, - @PathVariable("suffix") String suffix, @PathVariable("version") int version, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = PREFIX_OAS) @PathVariable("suffix") String suffix, + @PathVariable("version") int version, HttpServletRequest request) throws NotFoundException, JsonProcessingException { var id = DOI_STRING + prefix + '/' + suffix; log.info("Received get request for full specimen with id: {} and version: {}", id, version); @@ -101,10 +155,17 @@ public ResponseEntity getSpecimenByVersionFull( return ResponseEntity.ok(specimen); } + @Operation(summary = "Get digital specimen by ID and version") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital specimen successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = DigitalSpecimenResponseSingle.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}/{version}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getSpecimenByVersion(@PathVariable("prefix") String prefix, - @PathVariable("suffix") String suffix, @PathVariable("version") int version, + @Parameter(description = PREFIX_OAS) @PathVariable("suffix") String suffix, + @Parameter(description = SUFFIX_OAS) @PathVariable("version") int version, HttpServletRequest request) throws JsonProcessingException, NotFoundException { var id = DOI_STRING + prefix + '/' + suffix; @@ -113,20 +174,35 @@ public ResponseEntity getSpecimenByVersion(@PathVariable("prefix return ResponseEntity.ok(specimen); } + @Operation(summary = "Get all versions for a given digital specimen") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital specimen versions successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = VersionResponse.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}/versions", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getSpecimenVersions(@PathVariable("prefix") String prefix, - @PathVariable("suffix") String suffix, HttpServletRequest request) throws NotFoundException { + public ResponseEntity getSpecimenVersions( + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, + HttpServletRequest request) throws NotFoundException { var id = DOI_STRING + prefix + '/' + suffix; log.info("Received get request for specimen with id and version: {}", id); var versions = service.getSpecimenVersions(id, getPath(request)); return ResponseEntity.ok(versions); } + @Operation(summary = "Get annotations for a given digital specimen") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital specimen annotations successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseList.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}/annotations", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getSpecimenAnnotations( - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, HttpServletRequest request) { var id = prefix + '/' + suffix; log.info("Received get request for annotationRequests of specimen with id: {}", id); @@ -134,10 +210,17 @@ public ResponseEntity getSpecimenAnnotations( return ResponseEntity.ok(annotations); } + @Operation(summary = "Get digital media for a given digital specimen") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital specimen media successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = DigitalMediaResponseList.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}/digital-media", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getSpecimenDigitalMedia( - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = PREFIX_OAS) @PathVariable("suffix") String suffix, HttpServletRequest request) { var id = prefix + '/' + suffix; log.info("Received get request for digital media of specimen with id: {}", id); @@ -145,13 +228,27 @@ public ResponseEntity getSpecimenDigitalMedia( return ResponseEntity.ok(digitalMedia); } + @Operation( + summary = "Get MAS jobs for digital media", + description = """ + Retrieves a list of Machine Annotation Service Job Records (MJRs). + These are scheduled, running, or completed machine annotation service jobs. + Pagination is offered. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "MAS Job records successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = MjrResponseList.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/{prefix}/{suffix}/mjr", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getMasJobRecordsForSpecimen( - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, - @RequestParam(required = false) JobState state, - @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, - @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, + @Parameter(description = JOB_STATUS_OAS) @RequestParam(required = false) JobState state, + @Parameter(description = PAGE_NUM_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, + @Parameter(description = PAGE_SIZE_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, HttpServletRequest request) throws NotFoundException { var id = prefix + '/' + suffix; log.info("Received get request for MAS Job records for specimen {}", id); @@ -160,49 +257,101 @@ public ResponseEntity getMasJobRecordsForSpecimen( service.getMasJobRecordsForSpecimen(id, state, path, pageNumber, pageSize)); } + @Operation( + summary = "Search for digital specimen", + description = """ + Accepts key-value pairs of search parameters. Terms are mapped to the OpenDS standard. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Search results successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = DigitalSpecimenResponseList.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "/search", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity search( - @RequestParam MultiValueMap params, + @Parameter(description = "Search parameters") @RequestParam MultiValueMap params, HttpServletRequest request) throws IOException, UnknownParameterException { log.info("Received request params: {}", params); var specimen = service.search(params, getPath(request)); return ResponseEntity.ok(specimen); } + @Operation( + summary = "Aggregate digital specimens", + description = """ + Accepts key-value pairs of terns to aggregate on. If no aggregation terms are set, returns + aggregations on all aggregatable terms. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Aggregations successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AggregationResponse.class)) + }) + }) @ResponseStatus(HttpStatus.OK) - @GetMapping(value = "aggregation", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "/aggregation", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity aggregation( - @RequestParam MultiValueMap params, HttpServletRequest request) - throws IOException, UnknownParameterException { + @Parameter(description = "Aggregation terms") @RequestParam MultiValueMap params, + HttpServletRequest request) throws IOException, UnknownParameterException { log.info("Request for aggregations"); var aggregations = service.aggregations(params, getPath(request)); return ResponseEntity.ok(aggregations); } + @Operation( + summary = "Aggregate digital specimens on taxonomy", + description = """ + Accepts key-value pairs of terns to aggregate on. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Taxonomic aggregations successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AggregationResponse.class)) + }) + }) @ResponseStatus(HttpStatus.OK) @GetMapping(value = "taxonomy/aggregation", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity taxonAggregation( - @RequestParam MultiValueMap params, HttpServletRequest request) - throws IOException, UnknownParameterException { + @Parameter(description = "Taxonomic aggregation terms") @RequestParam MultiValueMap params, + HttpServletRequest request) throws IOException, UnknownParameterException { log.info("Request for taxonomy aggregations"); var aggregations = service.taxonAggregations(params, getPath(request)); return ResponseEntity.ok(aggregations); } + + @Operation( + summary = "Aggregate digital specimens on topic discipline" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Aggregations successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AggregationResponse.class)) + }) + }) @ResponseStatus(HttpStatus.OK) - @GetMapping(value = "discipline", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "/discipline", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity discipline(HttpServletRequest request) throws IOException { - log.info("Request for aggregations"); + log.info("Request for discipline aggregations"); var aggregations = service.discipline(getPath(request)); return ResponseEntity.ok(aggregations); } + @Operation( + summary = "Aggregate digital specimens using search terms" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Aggregations successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AggregationResponse.class)) + }) + }) @ResponseStatus(HttpStatus.OK) - @GetMapping(value = "searchTermValue", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "/searchTermValue", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity searchTermValue( - @RequestParam String term, @RequestParam String value, - @RequestParam(defaultValue = "false") boolean sort, + @Parameter(description = "Term to search on") @RequestParam String term, + @Parameter(description = "Value of term") @RequestParam String value, + @Parameter (description = "Whether or not to sort") @RequestParam(defaultValue = "false") boolean sort, HttpServletRequest request) throws IOException, UnknownParameterException { log.info("Request text search for term value of term: {} with value: {}", term, value); @@ -210,9 +359,22 @@ public ResponseEntity searchTermValue( return ResponseEntity.ok(result); } + @Operation( + summary = "Get MASs that may be run on the given digital specimen", + description = """ + Retrieves a list of Machine Annotation Services (MASs) suitable for processing a given + digital specimen, based on the MASs' respective filter criteria. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Digital media MASs successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AnnotationResponseList.class)) + }) + }) @GetMapping(value = "/{prefix}/{suffix}/mas", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getMassForDigitalSpecimen( - @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, + @Parameter(description = PREFIX_OAS) @PathVariable("prefix") String prefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("suffix") String suffix, HttpServletRequest request) { var id = prefix + '/' + suffix; log.info("Received get request for mass for digital specimen: {}", id); @@ -220,20 +382,44 @@ public ResponseEntity getMassForDigitalSpecimen( return ResponseEntity.ok(mass); } + @Operation( + summary = "Get original digital media data", + description = """ + DiSSCo provides harmonised data according to the OpenDS specification. + This endpoint provides the unharmonised data as it appears in the source system. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Original Data successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = JsonApiWrapper.class)) + }) + }) @GetMapping(value = "/{prefix}/{suffix}/original-data", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getOriginalDataForSpecimen( @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, - HttpServletRequest request){ + HttpServletRequest request) { var path = getPath(request); var id = prefix + '/' + suffix; return ResponseEntity.ok(service.getOriginalDataForSpecimen(id, path)); } + @Operation( + summary = "Schedule Machine Annotation Services", + description = """ + Schedules applicable MASs on a given digital media. + Only users who have provided their ORCID may schedule MASs. + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "MAS successfully scheduled", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = MjrResponseList.class)) + }) + }) @PostMapping(value = "/{prefix}/{suffix}/mas", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity scheduleMassForDigitalSpecimen( @PathVariable("prefix") String prefix, @PathVariable("suffix") String suffix, - @RequestBody JsonApiRequestWrapper requestBody, Authentication authentication, + @RequestBody MasSchedulingRequest requestBody, Authentication authentication, HttpServletRequest request) throws ConflictException, ForbiddenException, PidCreationException, NotFoundException { var orcid = getAgent(authentication).getId(); diff --git a/src/main/java/eu/dissco/backend/controller/MasJobRecordController.java b/src/main/java/eu/dissco/backend/controller/MasJobRecordController.java index b2cda3b2..5780087a 100644 --- a/src/main/java/eu/dissco/backend/controller/MasJobRecordController.java +++ b/src/main/java/eu/dissco/backend/controller/MasJobRecordController.java @@ -4,9 +4,17 @@ import eu.dissco.backend.database.jooq.enums.JobState; import eu.dissco.backend.domain.jsonapi.JsonApiListResponseWrapper; import eu.dissco.backend.domain.jsonapi.JsonApiWrapper; +import eu.dissco.backend.domain.openapi.shared.MjrResponseList; +import eu.dissco.backend.domain.openapi.shared.MjrResponseSingle; import eu.dissco.backend.exceptions.NotFoundException; import eu.dissco.backend.properties.ApplicationProperties; import eu.dissco.backend.service.MasJobRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; @@ -32,35 +40,65 @@ public MasJobRecordController(ObjectMapper mapper, this.service = service; } + @Operation( + summary = "Get MAS Job Record by ID", + description = """ + Retrieves record of running, scheduled, or completed Machine Annotation Service job + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "MAS Job Record successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = MjrResponseSingle.class)) + }) + }) @GetMapping(value = "/{jobIdPrefix}/{jobIdSuffix}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getMasJobRecord( - @PathVariable("jobIdPrefix") String masJobHandlePrefix, - @PathVariable("jobIdSuffix") String masJobHandleSuffix, + @Parameter(description = PREFIX_OAS) @PathVariable("jobIdPrefix") String masJobHandlePrefix, + @Parameter(description = SUFFIX_OAS) @PathVariable("jobIdSuffix") String masJobHandleSuffix, HttpServletRequest request) throws NotFoundException { var masJobHandle = masJobHandlePrefix + "/" + masJobHandleSuffix; return ResponseEntity.ok().body(service.getMasJobRecordById(masJobHandle, getPath(request))); } - @GetMapping(value = "/creator/" - + "{creatorId}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "Get MAS Job Record by creator", + description = """ + Retrieves record of running, scheduled, or completed Machine Annotation Service job + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "MAS Job Record successfully retrieved", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = MjrResponseList.class)) + }) + }) + @GetMapping(value = "/creator/{creatorId}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getMasJobRecordsForCreator( - @PathVariable("creatorId") String creatorId, - @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, - @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, - @RequestParam(required = false) JobState state, + @Parameter(description = "ID of Scheduler") @PathVariable("creatorId") String creatorId, + @Parameter(description = PAGE_NUM_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_NUM) int pageNumber, + @Parameter(description = PAGE_SIZE_OAS) @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize, + @Parameter(description = JOB_STATUS_OAS) @RequestParam(required = false) JobState state, HttpServletRequest request) { return ResponseEntity.ok().body( service.getMasJobRecordsByMasId(creatorId, getPath(request), pageNumber, pageSize, state)); - } + @Operation( + summary = "Mark job as running", + description = """ + Utility function for Machine Annotation Services to update job status to "running". + """ + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "MAS Job Record successfully retrieved") + }) @GetMapping(value = "/{masIdPrefix}/{masIdSuffix}/{jobIdPrefix}/{jobIdSuffix}/running") public ResponseEntity markMjrAsRunning( - @PathVariable("masIdPrefix") String masIdPrefix, - @PathVariable("masIdSuffix") String masIdSuffix, - @PathVariable("jobIdPrefix") String jobIdPrefix, - @PathVariable("jobIdSuffix") String jobIdSuffix) throws NotFoundException { + @Parameter(description = "Prefix of ID of MAS") @PathVariable("masIdPrefix") String masIdPrefix, + @Parameter(description = "Suffix of ID of MAS") @PathVariable("masIdSuffix") String masIdSuffix, + @Parameter(description = "Prefix of ID of Job") @PathVariable("jobIdPrefix") String jobIdPrefix, + @Parameter(description = "Suffix of ID of Job") @PathVariable("jobIdSuffix") String jobIdSuffix) + throws NotFoundException { var masId = masIdPrefix + "/" + masIdSuffix; var jobId = jobIdPrefix + "/" + jobIdSuffix; service.markMasJobRecordAsRunning(masId, jobId); diff --git a/src/main/java/eu/dissco/backend/domain/MasJobRequest.java b/src/main/java/eu/dissco/backend/domain/MasJobRequest.java index bf5ccbba..baa28769 100644 --- a/src/main/java/eu/dissco/backend/domain/MasJobRequest.java +++ b/src/main/java/eu/dissco/backend/domain/MasJobRequest.java @@ -1,7 +1,11 @@ package eu.dissco.backend.domain; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema public record MasJobRequest( - String masId, - boolean batching, - Long timeToLive) { + @Parameter(description = "ID of the MAS") String masId, + @Parameter(description = "If MAS should be run as a batch") boolean batching +) { } diff --git a/src/main/java/eu/dissco/backend/domain/annotation/batch/AnnotationEventRequest.java b/src/main/java/eu/dissco/backend/domain/annotation/batch/AnnotationEventRequest.java index d37bb342..f18031ab 100644 --- a/src/main/java/eu/dissco/backend/domain/annotation/batch/AnnotationEventRequest.java +++ b/src/main/java/eu/dissco/backend/domain/annotation/batch/AnnotationEventRequest.java @@ -1,9 +1,12 @@ package eu.dissco.backend.domain.annotation.batch; import eu.dissco.backend.schema.AnnotationProcessingRequest; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; -public record AnnotationEventRequest(List annotationRequests, - List batchMetadata) { +@Schema +public record AnnotationEventRequest( + List annotationRequests, + List batchMetadata) { } diff --git a/src/main/java/eu/dissco/backend/domain/jsonapi/JsonApiRequest.java b/src/main/java/eu/dissco/backend/domain/jsonapi/JsonApiRequest.java deleted file mode 100644 index b6733d0e..00000000 --- a/src/main/java/eu/dissco/backend/domain/jsonapi/JsonApiRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package eu.dissco.backend.domain.jsonapi; - -import com.fasterxml.jackson.databind.JsonNode; - -public record JsonApiRequest( - String type, - JsonNode attributes -) { - -} diff --git a/src/main/java/eu/dissco/backend/domain/jsonapi/JsonApiRequestWrapper.java b/src/main/java/eu/dissco/backend/domain/jsonapi/JsonApiRequestWrapper.java deleted file mode 100644 index 53510b8f..00000000 --- a/src/main/java/eu/dissco/backend/domain/jsonapi/JsonApiRequestWrapper.java +++ /dev/null @@ -1,5 +0,0 @@ -package eu.dissco.backend.domain.jsonapi; - -public record JsonApiRequestWrapper(JsonApiRequest data) { - -} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationRequest.java b/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationRequest.java new file mode 100644 index 00000000..a6cd0d48 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationRequest.java @@ -0,0 +1,17 @@ +package eu.dissco.backend.domain.openapi.annotation; + +import eu.dissco.backend.schema.AnnotationProcessingRequest; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record AnnotationRequest( + AnnotationRequestData data) { + + @Schema + public record AnnotationRequestData( + @Schema(description = "Type of request. For annotations, must be \"ods:Annotation\"") String type, + @Schema(description = "Desired annotation") AnnotationProcessingRequest attributes) { + + } + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseData.java b/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseData.java new file mode 100644 index 00000000..318904a2 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseData.java @@ -0,0 +1,12 @@ +package eu.dissco.backend.domain.openapi.annotation; + +import eu.dissco.backend.schema.Annotation; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record AnnotationResponseData( + @Schema(description = "Handle of the annotation") String id, + @Schema(description = "Type of the object, in this case \"ods:Annotation\"") String type, + @Schema(description = "Annotation") Annotation attributes) { + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseList.java b/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseList.java new file mode 100644 index 00000000..d2ac3704 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseList.java @@ -0,0 +1,14 @@ +package eu.dissco.backend.domain.openapi.annotation; + +import eu.dissco.backend.domain.jsonapi.JsonApiLinksFull; +import eu.dissco.backend.domain.jsonapi.JsonApiMeta; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema +public record AnnotationResponseList( + List data, + @Schema(description = "Links object, for pagination") JsonApiLinksFull links, + @Schema(description = "Response metadata") JsonApiMeta meta) { + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseSingle.java b/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseSingle.java new file mode 100644 index 00000000..673dda5d --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/annotation/AnnotationResponseSingle.java @@ -0,0 +1,11 @@ +package eu.dissco.backend.domain.openapi.annotation; + +import eu.dissco.backend.domain.jsonapi.JsonApiLinks; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record AnnotationResponseSingle( + @Schema(description = "Links object, self-referencing") JsonApiLinks links, + AnnotationResponseData data) { + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationCountRequest.java b/src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationCountRequest.java new file mode 100644 index 00000000..66d08487 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationCountRequest.java @@ -0,0 +1,25 @@ +package eu.dissco.backend.domain.openapi.annotation; + +import eu.dissco.backend.domain.annotation.AnnotationTargetType; +import eu.dissco.backend.domain.annotation.batch.BatchMetadata; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record BatchAnnotationCountRequest( + eu.dissco.backend.domain.openapi.annotation.BatchAnnotationCountRequest.BatchAnnotationCountRequestData data) { + + @Schema + public record BatchAnnotationCountRequestData( + @Schema(description = "Type of request, in this case \"batchAnnotationCountRequest\"") String type, + BatchAnnotationCountRequest.BatchAnnotationCountRequestData.BatchAnnotationCountRequestAttributes attributes) { + + @Schema + public record BatchAnnotationCountRequestAttributes( + BatchMetadata batchMetadata, + @Schema(description = "Type of target, either https://doi.org/21.T11148/894b1e6cad57e921764e (digital specimen) or https://doi.org21.T11148/bbad8c4e101e8af01115 (digital media)") AnnotationTargetType annotationTargetType) { + + } + + } + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationCountResponse.java b/src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationCountResponse.java new file mode 100644 index 00000000..43943b08 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationCountResponse.java @@ -0,0 +1,27 @@ +package eu.dissco.backend.domain.openapi.annotation; + +import eu.dissco.backend.domain.annotation.batch.BatchMetadata; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record BatchAnnotationCountResponse( + BatchAnnotationCountResponseData data) { + + @Schema + private record BatchAnnotationCountResponseData( + @Schema(description = "Type of response. In this case, \"batchAnnotationCount\"") + String type, + BatchAnnotationCountResponseAttributes attributes + ) { + + @Schema + private record BatchAnnotationCountResponseAttributes( + @Schema(description = "Number of objects affected by given search parameters") + Long objectAffected, + @Schema(description = "Provided search parameters") + BatchMetadata batchMetadata) { + + } + } + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationRequest.java b/src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationRequest.java new file mode 100644 index 00000000..b1e75405 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/annotation/BatchAnnotationRequest.java @@ -0,0 +1,17 @@ +package eu.dissco.backend.domain.openapi.annotation; + +import eu.dissco.backend.domain.annotation.batch.AnnotationEventRequest; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record BatchAnnotationRequest( + eu.dissco.backend.domain.openapi.annotation.BatchAnnotationRequest.BatchAnnotationRequestData data) { + + @Schema + public record BatchAnnotationRequestData( + @Schema(description = "Type of request, in this case \"ods:Annotation\"") String type, + AnnotationEventRequest attributes) { + + } + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseData.java b/src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseData.java new file mode 100644 index 00000000..84ac161f --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseData.java @@ -0,0 +1,13 @@ +package eu.dissco.backend.domain.openapi.media; + +import eu.dissco.backend.schema.DigitalMedia; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record DigitalMediaResponseData( + @Schema(description = "DOI of the Digital Media") String id, + @Schema(description = "Fdo type of the object") String type, + DigitalMedia attributes +) { + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseList.java b/src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseList.java new file mode 100644 index 00000000..464fb1d2 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseList.java @@ -0,0 +1,14 @@ +package eu.dissco.backend.domain.openapi.media; + +import eu.dissco.backend.domain.jsonapi.JsonApiLinksFull; +import eu.dissco.backend.domain.jsonapi.JsonApiMeta; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema +public record DigitalMediaResponseList( + List data, + @Schema(description = "Links object, for pagination") JsonApiLinksFull links, + @Schema(description = "Response metadata") JsonApiMeta meta) { + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseSingle.java b/src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseSingle.java new file mode 100644 index 00000000..5c622ddd --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/media/DigitalMediaResponseSingle.java @@ -0,0 +1,12 @@ +package eu.dissco.backend.domain.openapi.media; + +import eu.dissco.backend.domain.jsonapi.JsonApiLinks; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record DigitalMediaResponseSingle( + @Schema(description = "Links object, self-referencing") JsonApiLinks links, + DigitalMediaResponseData data +) { + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/shared/MasResponseList.java b/src/main/java/eu/dissco/backend/domain/openapi/shared/MasResponseList.java new file mode 100644 index 00000000..13610613 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/shared/MasResponseList.java @@ -0,0 +1,25 @@ +package eu.dissco.backend.domain.openapi.shared; + +import eu.dissco.backend.domain.jsonapi.JsonApiLinksFull; +import eu.dissco.backend.domain.jsonapi.JsonApiMeta; +import eu.dissco.backend.schema.MachineAnnotationService; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema +public record MasResponseList( + List data, + @Schema(description = "Links object, for pagination") JsonApiLinksFull links, + @Schema(description = "Response metadata") JsonApiMeta meta) { + + @Schema + private record MasResponseData ( + @Schema(description = "ID of the resource") String id, + @Schema(description = "Type of the resource") String type, + MachineAnnotationService attributes + ){ + + } + + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/shared/MasSchedulingRequest.java b/src/main/java/eu/dissco/backend/domain/openapi/shared/MasSchedulingRequest.java new file mode 100644 index 00000000..236aa445 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/shared/MasSchedulingRequest.java @@ -0,0 +1,29 @@ +package eu.dissco.backend.domain.openapi.shared; + +import eu.dissco.backend.domain.MasJobRequest; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema +public record MasSchedulingRequest( + MasSchedulingData data +) { + + @Schema + public record MasSchedulingData( + @Parameter(description = "Type of request, in this case \"MasRequest\"") + String type, + MasSchedulingAttributes attributes + ) { + + @Schema + public record MasSchedulingAttributes( + List mass + ) { + + } + + } + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseData.java b/src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseData.java new file mode 100644 index 00000000..e0fd658e --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseData.java @@ -0,0 +1,12 @@ +package eu.dissco.backend.domain.openapi.shared; + +import eu.dissco.backend.domain.MasJobRecord; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record MjrResponseData( + @Schema(description = "ID of the resource") String id, + @Schema(description = "Type of the resource") String type, + MasJobRecord attributes) { + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseList.java b/src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseList.java new file mode 100644 index 00000000..6dd09cfc --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseList.java @@ -0,0 +1,16 @@ +package eu.dissco.backend.domain.openapi.shared; + +import eu.dissco.backend.domain.jsonapi.JsonApiLinksFull; +import eu.dissco.backend.domain.jsonapi.JsonApiMeta; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema +public record MjrResponseList( + List data, + @Schema(description = "Links object, for pagination") + JsonApiLinksFull links, + @Schema(description = "Response metadata") + JsonApiMeta meta) { + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseSingle.java b/src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseSingle.java new file mode 100644 index 00000000..3fda9ebe --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/shared/MjrResponseSingle.java @@ -0,0 +1,14 @@ +package eu.dissco.backend.domain.openapi.shared; + +import eu.dissco.backend.domain.jsonapi.JsonApiLinks; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record MjrResponseSingle( + MjrResponseData data, + @Schema(description = "Links object, self-referencing") JsonApiLinks links +) { + + + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/shared/VersionResponse.java b/src/main/java/eu/dissco/backend/domain/openapi/shared/VersionResponse.java new file mode 100644 index 00000000..73ce223d --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/shared/VersionResponse.java @@ -0,0 +1,24 @@ +package eu.dissco.backend.domain.openapi.shared; + +import eu.dissco.backend.domain.jsonapi.JsonApiLinks; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema +public record VersionResponse( + VersionResponseData data, + @Schema(description = "Links object, self-referencing") JsonApiLinks links) { + + @Schema + private record VersionResponseData( + @Schema(description = "Handle of the target annotation") String id, + @Schema(description = "Type of response") String type, + @Schema(description = "Versions of the target object") VersionResponseAttributes attributes) { + + @Schema + private record VersionResponseAttributes(List versions) { + + } + } + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/specimen/AggregationResponse.java b/src/main/java/eu/dissco/backend/domain/openapi/specimen/AggregationResponse.java new file mode 100644 index 00000000..57f7a462 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/specimen/AggregationResponse.java @@ -0,0 +1,35 @@ +package eu.dissco.backend.domain.openapi.specimen; + +import com.fasterxml.jackson.databind.JsonNode; +import eu.dissco.backend.domain.jsonapi.JsonApiLinks; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record AggregationResponse( + AggregationResponseData data, + @Schema(description = "Links object, self-referencing") JsonApiLinks links +) { + + @Schema + private record AggregationResponseData( + @Parameter(description = "ID of the response") String id, + @Parameter(description = "Type of response, in this case \"aggregations\"") String type, + @Parameter(description = "Aggregation terms", example = """ + "country": { + "Netherlands": 314879, + "Indonesia": 207148, + "Estonia": 164357, + "Greece": 111511, + "Germany": 45251, + "Denmark": 26121, + "El Salvador": 23224, + "Russia": 21921, + "Tanzania": 18061, + "France": 15391 + } + """) JsonNode attributes + ){ + } + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseData.java b/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseData.java new file mode 100644 index 00000000..ffb7615a --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseData.java @@ -0,0 +1,12 @@ +package eu.dissco.backend.domain.openapi.specimen; + +import eu.dissco.backend.schema.DigitalSpecimen; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record DigitalSpecimenResponseData( + @Schema(description = "DOI of the Digital Specimen") String id, + @Schema(description = "Fdo type of the object") String type, + DigitalSpecimen attributes) { + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseFull.java b/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseFull.java new file mode 100644 index 00000000..9843729d --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseFull.java @@ -0,0 +1,31 @@ +package eu.dissco.backend.domain.openapi.specimen; + +import eu.dissco.backend.domain.jsonapi.JsonApiLinksFull; +import eu.dissco.backend.domain.jsonapi.JsonApiMeta; +import eu.dissco.backend.schema.Annotation; +import eu.dissco.backend.schema.DigitalMedia; +import eu.dissco.backend.schema.DigitalSpecimen; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record DigitalSpecimenResponseFull( + DigitalSpecimenResponseDataFull data, + @Schema(description = "Links object, for pagination") JsonApiLinksFull links, + @Schema(description = "Response metadata") JsonApiMeta meta +) { + + private record DigitalSpecimenResponseDataFull( + DigitalSpecimenResponseAttributesFull attributes + ) { + + private record DigitalSpecimenResponseAttributesFull( + DigitalSpecimen digitalSpecimen, + List digitalMedia, + List annotations + ) { + + } + } + + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseList.java b/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseList.java new file mode 100644 index 00000000..18d39d42 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseList.java @@ -0,0 +1,13 @@ +package eu.dissco.backend.domain.openapi.specimen; + +import eu.dissco.backend.domain.jsonapi.JsonApiLinksFull; +import eu.dissco.backend.domain.jsonapi.JsonApiMeta; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record DigitalSpecimenResponseList( + DigitalSpecimenResponseData data, + @Schema(description = "Links object, for pagination") JsonApiLinksFull links, + @Schema(description = "Response metadata") JsonApiMeta meta) { + +} diff --git a/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseSingle.java b/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseSingle.java new file mode 100644 index 00000000..6e539b16 --- /dev/null +++ b/src/main/java/eu/dissco/backend/domain/openapi/specimen/DigitalSpecimenResponseSingle.java @@ -0,0 +1,12 @@ +package eu.dissco.backend.domain.openapi.specimen; + +import eu.dissco.backend.domain.jsonapi.JsonApiLinksFull; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record DigitalSpecimenResponseSingle( + DigitalSpecimenResponseData data, + @Schema(description = "Links object, self-referencing") JsonApiLinksFull links) +{ + +} diff --git a/src/main/java/eu/dissco/backend/service/AnnotationService.java b/src/main/java/eu/dissco/backend/service/AnnotationService.java index 5093a419..b44aaefc 100644 --- a/src/main/java/eu/dissco/backend/service/AnnotationService.java +++ b/src/main/java/eu/dissco/backend/service/AnnotationService.java @@ -6,17 +6,16 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import eu.dissco.backend.client.AnnotationClient; -import eu.dissco.backend.domain.annotation.AnnotationTargetType; import eu.dissco.backend.domain.annotation.AnnotationTombstoneWrapper; import eu.dissco.backend.domain.annotation.batch.AnnotationEvent; import eu.dissco.backend.domain.annotation.batch.AnnotationEventRequest; -import eu.dissco.backend.domain.annotation.batch.BatchMetadata; import eu.dissco.backend.domain.jsonapi.JsonApiData; import eu.dissco.backend.domain.jsonapi.JsonApiLinks; import eu.dissco.backend.domain.jsonapi.JsonApiLinksFull; import eu.dissco.backend.domain.jsonapi.JsonApiListResponseWrapper; import eu.dissco.backend.domain.jsonapi.JsonApiMeta; import eu.dissco.backend.domain.jsonapi.JsonApiWrapper; +import eu.dissco.backend.domain.openapi.annotation.BatchAnnotationCountRequest; import eu.dissco.backend.exceptions.NoAnnotationFoundException; import eu.dissco.backend.exceptions.NotFoundException; import eu.dissco.backend.repository.AnnotationRepository; @@ -35,7 +34,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; -import org.apache.kafka.common.errors.InvalidRequestException; import org.springframework.stereotype.Service; @Slf4j @@ -113,34 +111,17 @@ public JsonApiWrapper formatResponse(JsonNode response, String path) return null; } - public JsonNode getCountForBatchAnnotations(JsonNode annotationCountJson) throws IOException { - var annotationCountRequest = getAnnotationBatchCount(annotationCountJson); - var count = elasticRepository.getCountForBatchAnnotations(annotationCountRequest.getLeft(), - annotationCountRequest.getRight()); + public JsonNode getCountForBatchAnnotations(BatchAnnotationCountRequest annotationCountRequest) throws IOException { + var count = elasticRepository.getCountForBatchAnnotations(annotationCountRequest.data().attributes() + .batchMetadata(), + annotationCountRequest.data().attributes().annotationTargetType()); return mapper.createObjectNode() .set(DATA, mapper.createObjectNode() .put("type", "batchAnnotationCount") .set(ATTRIBUTES, mapper.createObjectNode() .put("objectAffected", count) - .set("batchMetadata", mapper.valueToTree(annotationCountRequest.getLeft())))); - } - - private Pair getAnnotationBatchCount( - JsonNode annotationCountRequest) { - try { - var batchMetadata = mapper.treeToValue( - annotationCountRequest.get(DATA).get(ATTRIBUTES).get("batchMetadata"), - BatchMetadata.class); - var targetType = mapper.treeToValue( - annotationCountRequest.get(DATA).get(ATTRIBUTES).get("annotationTargetType"), - AnnotationTargetType.class); - assert (batchMetadata != null); - assert (targetType != null); - return Pair.of(batchMetadata, targetType); - } catch (JsonProcessingException | NullPointerException | AssertionError e) { - log.info("Unable to read request body for batch annotation count", e); - throw new InvalidRequestException("Invalid request for batch annotation request"); - } + .set("batchMetadata", mapper.valueToTree(annotationCountRequest.data().attributes() + .batchMetadata())))); } private Annotation buildAnnotation(AnnotationProcessingRequest annotationProcessingRequest, diff --git a/src/test/java/eu/dissco/backend/controller/AnnotationControllerTest.java b/src/test/java/eu/dissco/backend/controller/AnnotationControllerTest.java index e01a9753..81998f3f 100644 --- a/src/test/java/eu/dissco/backend/controller/AnnotationControllerTest.java +++ b/src/test/java/eu/dissco/backend/controller/AnnotationControllerTest.java @@ -12,12 +12,11 @@ import static eu.dissco.backend.TestUtils.givenClaims; import static eu.dissco.backend.utils.AnnotationUtils.ANNOTATION_PATH; import static eu.dissco.backend.utils.AnnotationUtils.ANNOTATION_URI; -import static eu.dissco.backend.utils.AnnotationUtils.givenAnnotationCountRequest; import static eu.dissco.backend.utils.AnnotationUtils.givenAnnotationEventRequest; import static eu.dissco.backend.utils.AnnotationUtils.givenAnnotationJsonResponse; import static eu.dissco.backend.utils.AnnotationUtils.givenAnnotationRequest; import static eu.dissco.backend.utils.AnnotationUtils.givenAnnotationResponseSingleDataNode; -import static eu.dissco.backend.utils.AnnotationUtils.givenJsonApiAnnotationRequest; +import static eu.dissco.backend.utils.AnnotationUtils.givenBatchMetadata; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import static org.mockito.ArgumentMatchers.any; @@ -28,6 +27,14 @@ import static org.mockito.Mockito.mock; import eu.dissco.backend.component.SchemaValidatorComponent; +import eu.dissco.backend.domain.annotation.AnnotationTargetType; +import eu.dissco.backend.domain.openapi.annotation.AnnotationRequest; +import eu.dissco.backend.domain.openapi.annotation.AnnotationRequest.AnnotationRequestData; +import eu.dissco.backend.domain.openapi.annotation.BatchAnnotationCountRequest; +import eu.dissco.backend.domain.openapi.annotation.BatchAnnotationCountRequest.BatchAnnotationCountRequestData; +import eu.dissco.backend.domain.openapi.annotation.BatchAnnotationCountRequest.BatchAnnotationCountRequestData.BatchAnnotationCountRequestAttributes; +import eu.dissco.backend.domain.openapi.annotation.BatchAnnotationRequest; +import eu.dissco.backend.domain.openapi.annotation.BatchAnnotationRequest.BatchAnnotationRequestData; import eu.dissco.backend.exceptions.ForbiddenException; import eu.dissco.backend.exceptions.NotFoundException; import eu.dissco.backend.properties.ApplicationProperties; @@ -151,7 +158,7 @@ void testCreateAnnotation() throws Exception { givenAuthentication(givenClaims()); var annotation = givenAnnotationRequest(); - var request = givenJsonApiAnnotationRequest(annotation); + var request = givenAnnotationRequestObject(); var expectedResponse = givenAnnotationResponseSingleDataNode(ANNOTATION_PATH); given(service.persistAnnotation(annotation, givenAgent(), ANNOTATION_PATH)) .willReturn(expectedResponse); @@ -173,7 +180,7 @@ void testPersistAnnotationMissingOrcid() { given(principal.getClaims()).willReturn(Map.of( "client-id", "demo-api-client" )); - var request = givenJsonApiAnnotationRequest(givenAnnotationRequest()); + var request = givenAnnotationRequestObject(); // When / Then assertThrowsExactly(ForbiddenException.class, @@ -183,7 +190,12 @@ void testPersistAnnotationMissingOrcid() { @Test void testGetBatchConfirmation() throws Exception { // When - var result = controller.getCountForBatchAnnotations(givenAnnotationCountRequest()); + var result = controller.getCountForBatchAnnotations(new BatchAnnotationCountRequest( + new BatchAnnotationCountRequestData( + "atchAnnotationCountRequest", + new BatchAnnotationCountRequestAttributes( + givenBatchMetadata(), AnnotationTargetType.DIGITAL_SPECIMEN + )))); // Then assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -194,14 +206,14 @@ void testCreateAnnotationBatch() throws Exception { // Given givenAuthentication(givenClaims()); var event = givenAnnotationEventRequest(); - var request = givenJsonApiAnnotationRequest(event); var expectedResponse = givenAnnotationResponseSingleDataNode(ANNOTATION_PATH); given(service.persistAnnotationBatch(event, givenAgent(), ANNOTATION_PATH)) .willReturn(expectedResponse); given(applicationProperties.getBaseUrl()).willReturn("https://sandbox.dissco.tech"); // When - var receivedResponse = controller.createAnnotationBatch(authentication, request, mockRequest); + var receivedResponse = controller.createAnnotationBatch(authentication, + givenBatchAnnotationRequestObject(), mockRequest); // Then assertThat(receivedResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); @@ -212,8 +224,7 @@ void testCreateAnnotationBatch() throws Exception { void testCreateAnnotationNullResponse() throws Exception { // Given givenAuthentication(givenClaims()); - var annotation = givenAnnotationRequest(); - var request = givenJsonApiAnnotationRequest(annotation); + var request = givenAnnotationRequestObject(); given( service.persistAnnotation(any(AnnotationProcessingRequest.class), any(), any())).willReturn( null); @@ -230,13 +241,13 @@ void testCreateAnnotationBatchNullResponse() throws Exception { // Given givenAuthentication(givenClaims()); var event = givenAnnotationEventRequest(); - var request = givenJsonApiAnnotationRequest(event); given(service.persistAnnotationBatch(event, givenAgent(), ANNOTATION_PATH)) .willReturn(null); given(applicationProperties.getBaseUrl()).willReturn("https://sandbox.dissco.tech"); // When - var receivedResponse = controller.createAnnotationBatch(authentication, request, mockRequest); + var receivedResponse = controller.createAnnotationBatch(authentication, + givenBatchAnnotationRequestObject(), mockRequest); // Then assertThat(receivedResponse.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -247,7 +258,7 @@ void testUpdateAnnotation() throws Exception { // Given givenAuthentication(givenClaims()); var annotation = givenAnnotationRequest(); - var requestBody = givenJsonApiAnnotationRequest(annotation); + var requestBody = givenAnnotationRequestObject(); var expected = givenAnnotationResponseSingleDataNode(ANNOTATION_PATH); given(service.updateAnnotation(ID, annotation, givenAgent(), ANNOTATION_PATH, PREFIX, SUFFIX)).willReturn( @@ -290,7 +301,8 @@ void testGetAnnotationsByVersion() throws NotFoundException { @ParameterizedTest @MethodSource("nonValidAdminClaims") - void testTombstoneAnnotationSuccessNonValidAdminClaims(Map claims) throws Exception { + void testTombstoneAnnotationSuccessNonValidAdminClaims(Map claims) + throws Exception { // Given givenAuthentication(claims); given(service.tombstoneAnnotation(PREFIX, SUFFIX, givenAgent(), false)).willReturn(true); @@ -302,7 +314,7 @@ void testTombstoneAnnotationSuccessNonValidAdminClaims(Map claim assertThat(receivedResponse.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); } - private static Stream nonValidAdminClaims(){ + private static Stream nonValidAdminClaims() { return Stream.of( Arguments.of(givenClaims()), Arguments.of(Map.of( @@ -348,4 +360,15 @@ private void givenAuthentication(Map claims) { given(authentication.getPrincipal()).willReturn(principal); given(principal.getClaims()).willReturn(claims); } + + public static AnnotationRequest givenAnnotationRequestObject() { + return new AnnotationRequest( + new AnnotationRequestData("ods:Annotation", givenAnnotationRequest())); + } + + public static BatchAnnotationRequest givenBatchAnnotationRequestObject() { + return new BatchAnnotationRequest(new BatchAnnotationRequestData( + "ods:Annotation", givenAnnotationEventRequest())); + } + } diff --git a/src/test/java/eu/dissco/backend/controller/DigitalMediaControllerTest.java b/src/test/java/eu/dissco/backend/controller/DigitalMediaControllerTest.java index f87ed2e5..fa07a864 100644 --- a/src/test/java/eu/dissco/backend/controller/DigitalMediaControllerTest.java +++ b/src/test/java/eu/dissco/backend/controller/DigitalMediaControllerTest.java @@ -26,6 +26,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import eu.dissco.backend.database.jooq.enums.JobState; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest.MasSchedulingData; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest.MasSchedulingData.MasSchedulingAttributes; import eu.dissco.backend.exceptions.ConflictException; import eu.dissco.backend.exceptions.NotFoundException; import eu.dissco.backend.properties.ApplicationProperties; @@ -184,7 +187,8 @@ void testScheduleMas() throws Exception { @Test void testScheduleMasInvalidType() { // Given - var request = givenMasRequest("Invalid Type"); + var request = new MasSchedulingRequest(new MasSchedulingData("Invalid type", + new MasSchedulingAttributes(List.of(givenMasJobRequest())))); givenAuthentication(); // When / Then diff --git a/src/test/java/eu/dissco/backend/controller/DigitalSpecimenControllerTest.java b/src/test/java/eu/dissco/backend/controller/DigitalSpecimenControllerTest.java index d359cd06..31e9f8cd 100644 --- a/src/test/java/eu/dissco/backend/controller/DigitalSpecimenControllerTest.java +++ b/src/test/java/eu/dissco/backend/controller/DigitalSpecimenControllerTest.java @@ -29,9 +29,10 @@ import eu.dissco.backend.domain.jsonapi.JsonApiData; import eu.dissco.backend.domain.jsonapi.JsonApiLinks; import eu.dissco.backend.domain.jsonapi.JsonApiListResponseWrapper; -import eu.dissco.backend.domain.jsonapi.JsonApiRequest; -import eu.dissco.backend.domain.jsonapi.JsonApiRequestWrapper; import eu.dissco.backend.domain.jsonapi.JsonApiWrapper; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest.MasSchedulingData; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest.MasSchedulingData.MasSchedulingAttributes; import eu.dissco.backend.exceptions.ConflictException; import eu.dissco.backend.properties.ApplicationProperties; import eu.dissco.backend.service.DigitalSpecimenService; @@ -279,7 +280,8 @@ void testScheduleMas() throws Exception { @Test void testScheduleMasInvalidType() { // Given - var request = givenMasRequest("Invalid Type"); + var request = new MasSchedulingRequest(new MasSchedulingData("Invalid type", + new MasSchedulingAttributes(List.of(givenMasJobRequest())))); givenAuthentication(); // When / Then @@ -289,11 +291,10 @@ void testScheduleMasInvalidType() { } @Test - void testScheduleMasNoAttribute() throws Exception{ + void testScheduleMasNoAttribute() { // Given - var mass = Map.of("somethingElse", Map.of(ID, false)); - var apiRequest = new JsonApiRequest("MasRequest", MAPPER.valueToTree(mass)); - var request = new JsonApiRequestWrapper(apiRequest); + var request = new MasSchedulingRequest(new MasSchedulingData("MasRequest", + new MasSchedulingAttributes(null))); givenAuthentication(); // When / Then diff --git a/src/test/java/eu/dissco/backend/service/AnnotationServiceTest.java b/src/test/java/eu/dissco/backend/service/AnnotationServiceTest.java index 1bcf1408..c5aae734 100644 --- a/src/test/java/eu/dissco/backend/service/AnnotationServiceTest.java +++ b/src/test/java/eu/dissco/backend/service/AnnotationServiceTest.java @@ -27,9 +27,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mockStatic; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; import eu.dissco.backend.client.AnnotationClient; import eu.dissco.backend.domain.annotation.AnnotationTargetType; import eu.dissco.backend.domain.jsonapi.JsonApiData; @@ -50,16 +48,12 @@ import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Pair; -import org.apache.kafka.common.errors.InvalidRequestException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -86,46 +80,6 @@ class AnnotationServiceTest { private MockedStatic mockedClock; private AnnotationService service; - private static Stream badAnnotationCountRequest() throws JsonProcessingException { - return Stream.of( - Arguments.of(MAPPER.readTree(""" - { - "data": { - "type": "annotationRequests", - "attributes": { - "batchMetadata": { - "searchParams": [ - { - "inputField": "digitalSpecimenWrapper.occurrences[*].location.dwc:country", - "inputValue": "Canada" - } - ] - } - } - } - } - """)), - Arguments.of(MAPPER.readTree(""" - { - "data": { - "type": "annotationRequests", - "attributes": { - "annotationTargetType": "https://doi.org/21.T11148/894b1e6cad57e921764e" - } - } - } - """)), - Arguments.of(MAPPER.readTree(""" - { - "data": { - "type": "annotationRequests" - } - } - """)), - Arguments.of(MAPPER.createObjectNode()) - ); - } - @BeforeEach void setup() { service = new AnnotationService(repository, annotationClient, elasticRepository, @@ -350,14 +304,6 @@ void testGetAnnotationBatchCount() throws Exception { MAPPER.configure(DeserializationFeature.USE_LONG_FOR_INTS, false); } - @ParameterizedTest - @MethodSource("badAnnotationCountRequest") - void testGetAnnotationBatchCountBadRequest(JsonNode badRequest) { - // When / Then - assertThrowsExactly(InvalidRequestException.class, - () -> service.getCountForBatchAnnotations(badRequest)); - } - @Test void testPersistAnnotationBatchBatch() throws Exception { // Given @@ -392,17 +338,6 @@ void testPersistAnnotationBatchIsNull() throws Exception { } - @Test - void testGetAnnotationBatchCountNpe() { - // Given - var badRequest = MAPPER.createObjectNode() - .set("data", MAPPER.createObjectNode()); - - // When / Then - assertThrowsExactly(InvalidRequestException.class, - () -> service.getCountForBatchAnnotations(badRequest)); - } - @Test void testUpdateAnnotation() throws Exception { // Given diff --git a/src/test/java/eu/dissco/backend/service/MachineAnnotationServiceServiceTest.java b/src/test/java/eu/dissco/backend/service/MachineAnnotationServiceServiceTest.java index e9b21445..9be7b408 100644 --- a/src/test/java/eu/dissco/backend/service/MachineAnnotationServiceServiceTest.java +++ b/src/test/java/eu/dissco/backend/service/MachineAnnotationServiceServiceTest.java @@ -119,13 +119,13 @@ void testScheduleMass() throws JsonProcessingException, ConflictException { given(repository.getMasRecords(Set.of(HANDLE + ID))).willReturn(List.of(masRecord)); given(masJobRecordService.createMasJobRecord(Set.of(masRecord), HANDLE + ID, ORCID, MjrTargetType.DIGITAL_SPECIMEN, - Map.of(HANDLE + ID, givenMasJobRequest(true, null)))).willReturn( + Map.of(HANDLE + ID, givenMasJobRequest(true)))).willReturn( givenMasJobRecordIdMap(masRecord.getId(), true)); var sendObject = new MasTarget(digitalSpecimen, JOB_ID, true); // When var result = service.scheduleMass(givenFlattenedDigitalSpecimen(), - Map.of(HANDLE + ID, givenMasJobRequest(true, null)), + Map.of(HANDLE + ID, givenMasJobRequest(true)), SPECIMEN_PATH, digitalSpecimen, digitalSpecimen.getDctermsIdentifier(), ORCID, MjrTargetType.DIGITAL_SPECIMEN); @@ -169,7 +169,7 @@ void testScheduleMassInvalidBatchRequest() { // Then assertThrowsExactly(BatchingNotPermittedException.class, () -> service.scheduleMass(givenFlattenedDigitalSpecimen(), - Map.of(HANDLE + ID, givenMasJobRequest(true, null)), SPECIMEN_PATH, + Map.of(HANDLE + ID, givenMasJobRequest(true)), SPECIMEN_PATH, digitalSpecimen, digitalSpecimen.getDctermsIdentifier(), ORCID, MjrTargetType.DIGITAL_SPECIMEN)); } diff --git a/src/test/java/eu/dissco/backend/service/MasJobRecordServiceTest.java b/src/test/java/eu/dissco/backend/service/MasJobRecordServiceTest.java index 1e30a9c2..aa2c3fd4 100644 --- a/src/test/java/eu/dissco/backend/service/MasJobRecordServiceTest.java +++ b/src/test/java/eu/dissco/backend/service/MasJobRecordServiceTest.java @@ -202,14 +202,13 @@ void testCreateMasJobRecordForDisscoMedia() { @Test void testCreateMasJobRecordCustomTTL() { // Given - var ttl = 3600L; var masRecord = MachineAnnotationServiceUtils.givenMas(); var expected = givenMasJobRecordIdMap(masRecord.getId()); given(handleComponent.postHandle(1)).willReturn(List.of(JOB_ID)); // When var result = masJobRecordService.createMasJobRecord(Set.of(masRecord), ID_ALT, ORCID, - MjrTargetType.DIGITAL_SPECIMEN, Map.of(masRecord.getId(), givenMasJobRequest(false, ttl))); + MjrTargetType.DIGITAL_SPECIMEN, Map.of(masRecord.getId(), givenMasJobRequest(false))); // Then assertThat(result).isEqualTo(expected); diff --git a/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java b/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java index 010e8315..5721ff74 100644 --- a/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java +++ b/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java @@ -9,7 +9,6 @@ import static eu.dissco.backend.TestUtils.TARGET_ID; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import eu.dissco.backend.domain.annotation.batch.AnnotationEvent; import eu.dissco.backend.domain.annotation.batch.AnnotationEventRequest; import eu.dissco.backend.domain.annotation.batch.BatchMetadata; @@ -18,9 +17,8 @@ import eu.dissco.backend.domain.jsonapi.JsonApiLinks; import eu.dissco.backend.domain.jsonapi.JsonApiLinksFull; import eu.dissco.backend.domain.jsonapi.JsonApiListResponseWrapper; -import eu.dissco.backend.domain.jsonapi.JsonApiRequest; -import eu.dissco.backend.domain.jsonapi.JsonApiRequestWrapper; import eu.dissco.backend.domain.jsonapi.JsonApiWrapper; +import eu.dissco.backend.domain.openapi.annotation.BatchAnnotationCountRequest; import eu.dissco.backend.schema.Agent; import eu.dissco.backend.schema.Agent.Type; import eu.dissco.backend.schema.Annotation; @@ -172,15 +170,6 @@ public static OdsHasAggregateRating givenAggregationRating() { .withSchemaRatingCount(2); } - public static JsonApiRequestWrapper givenJsonApiAnnotationRequest(Object request) { - return new JsonApiRequestWrapper( - new JsonApiRequest( - "ods:Annotation", - MAPPER.valueToTree(request) - ) - ); - } - // JsonApiWrapper public static JsonApiWrapper givenAnnotationResponseSingleDataNode(String path) { return givenAnnotationResponseSingleDataNode(path, ORCID); @@ -250,8 +239,8 @@ public static SearchParam givenSearchParam(String country) { ); } - public static JsonNode givenAnnotationCountRequest() throws JsonProcessingException { - return MAPPER.readTree(""" + public static BatchAnnotationCountRequest givenAnnotationCountRequest() throws JsonProcessingException { + return MAPPER.readValue(""" { "data": { "type": "batchAnnotationCount", @@ -268,7 +257,7 @@ public static JsonNode givenAnnotationCountRequest() throws JsonProcessingExcept } } } - """); + """, BatchAnnotationCountRequest.class); } diff --git a/src/test/java/eu/dissco/backend/utils/MachineAnnotationServiceUtils.java b/src/test/java/eu/dissco/backend/utils/MachineAnnotationServiceUtils.java index f2e370d6..002f5492 100644 --- a/src/test/java/eu/dissco/backend/utils/MachineAnnotationServiceUtils.java +++ b/src/test/java/eu/dissco/backend/utils/MachineAnnotationServiceUtils.java @@ -15,8 +15,9 @@ import eu.dissco.backend.domain.jsonapi.JsonApiLinksFull; import eu.dissco.backend.domain.jsonapi.JsonApiListResponseWrapper; import eu.dissco.backend.domain.jsonapi.JsonApiMeta; -import eu.dissco.backend.domain.jsonapi.JsonApiRequest; -import eu.dissco.backend.domain.jsonapi.JsonApiRequestWrapper; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest.MasSchedulingData; +import eu.dissco.backend.domain.openapi.shared.MasSchedulingRequest.MasSchedulingData.MasSchedulingAttributes; import eu.dissco.backend.schema.Agent; import eu.dissco.backend.schema.Agent.Type; import eu.dissco.backend.schema.MachineAnnotationService; @@ -27,7 +28,6 @@ import java.time.Instant; import java.util.Date; import java.util.List; -import java.util.Map; public class MachineAnnotationServiceUtils { @@ -57,25 +57,20 @@ public static JsonApiListResponseWrapper givenScheduledMasResponse(MasJobRecord return new JsonApiListResponseWrapper(masRecords, links, new JsonApiMeta(masRecords.size())); } - public static JsonApiRequestWrapper givenMasRequest() { - return givenMasRequest("MasRequest"); - } - - public static JsonApiRequestWrapper givenMasRequest(String type) { - var mass = Map.of("mass", List.of(givenMasJobRequest())); - var apiRequest = new JsonApiRequest(type, MAPPER.valueToTree(mass)); - return new JsonApiRequestWrapper(apiRequest); + public static MasSchedulingRequest givenMasRequest() { + return new MasSchedulingRequest(new MasSchedulingData("MasRequest", + new MasSchedulingAttributes(List.of(givenMasJobRequest())))); } public static MasJobRequest givenMasJobRequest() { - return givenMasJobRequest(false, null); + return givenMasJobRequest(false); } - public static MasJobRequest givenMasJobRequest(boolean batching, Long ttl) { + public static MasJobRequest givenMasJobRequest(boolean batching) { return new MasJobRequest( MAS_ID, - batching, - ttl); + batching + ); } public static MachineAnnotationService givenMas() {