diff --git a/backend/web/src/main/java/org/opencdmp/controllers/DescriptionController.java b/backend/web/src/main/java/org/opencdmp/controllers/DescriptionController.java index 77784be83..9f852d5e9 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/DescriptionController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/DescriptionController.java @@ -14,8 +14,11 @@ import gr.cite.tools.validation.ValidationFilterAnnotation; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.xml.bind.JAXBException; @@ -27,7 +30,8 @@ import org.opencdmp.commons.enums.IsActive; import org.opencdmp.controllers.swagger.SwaggerHelpers; import org.opencdmp.controllers.swagger.annotation.Swagger400; import org.opencdmp.controllers.swagger.annotation.Swagger404; -import org.opencdmp.controllers.swagger.annotation.SwaggerErrorResponses; +import org.opencdmp.controllers.swagger.annotation.SwaggerCommonErrorResponses; +import org.opencdmp.controllers.swagger.annotation.OperationWithTenantHeader; import org.opencdmp.convention.ConventionService; import org.opencdmp.data.StorageFileEntity; import org.opencdmp.model.DescriptionValidationResult; @@ -76,7 +80,7 @@ import static org.opencdmp.authorization.AuthorizationFlags.Public; @RestController @RequestMapping(path = "api/description") @Tag(name = "Descriptions", description = "Manage descriptions") -@SwaggerErrorResponses +@SwaggerCommonErrorResponses public class DescriptionController { private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(DescriptionController.class)); @@ -117,7 +121,7 @@ public class DescriptionController { } @PostMapping("public/query") - @Operation(summary = "Query public descriptions") + @OperationWithTenantHeader(summary = "Query public descriptions") @Hidden public QueryResult publicQuery(@RequestBody DescriptionLookup lookup) throws MyApplicationException, MyForbiddenException { logger.debug("querying {}", PublicDescription.class.getSimpleName()); @@ -133,7 +137,7 @@ public class DescriptionController { } @GetMapping("public/{id}") - @Operation(summary = "Fetch a specific public description by id") + @OperationWithTenantHeader(summary = "Fetch a specific public description by id") @Hidden public PublicDescription publicGet(@PathVariable("id") UUID id, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException { logger.debug(new MapLogEntry("retrieving" + PublicDescription.class.getSimpleName()).And("id", id).And("fields", fieldSet)); @@ -155,7 +159,7 @@ public class DescriptionController { } @PostMapping("query") - @Operation(summary = "Query all descriptions", description = SwaggerHelpers.Description.endpoint_query, requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(description = SwaggerHelpers.Description.endpoint_query_request_body, content = @Content( + @OperationWithTenantHeader(summary = "Query all descriptions", description = SwaggerHelpers.Description.endpoint_query, requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(description = SwaggerHelpers.Description.endpoint_query_request_body, content = @Content( examples = { @ExampleObject( name = "Pagination and projection", @@ -182,7 +186,7 @@ public class DescriptionController { @GetMapping("{id}") - @Operation(summary = "Fetch a specific description by id") + @OperationWithTenantHeader(summary = "Fetch a specific description by id") @Swagger404 public Description get( @Parameter(name = "id", description = "The id of a description to fetch", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -207,7 +211,7 @@ public class DescriptionController { } @PostMapping("persist") - @Operation(summary = "Create a new or update an existing description") + @OperationWithTenantHeader(summary = "Create a new or update an existing description") @Swagger400 @Swagger404 @Transactional @@ -230,7 +234,7 @@ public class DescriptionController { } @PostMapping("persist-status") - @Operation(summary = "Update the status of an existing description") + @OperationWithTenantHeader(summary = "Update the status of an existing description") @Swagger400 @Swagger404 @Transactional @@ -253,7 +257,7 @@ public class DescriptionController { } @PostMapping("get-description-section-permissions") - @Operation(summary = "Fetch the section specific user permissions") + @OperationWithTenantHeader(summary = "Fetch the section specific user permissions") @Hidden @ValidationFilterAnnotation(validator = DescriptionSectionPermissionResolver.DescriptionSectionPermissionResolverPersistValidator.ValidatorName, argumentName = "model") public Map> getDescriptionSectionPermissions(@RequestBody DescriptionSectionPermissionResolver model) { @@ -268,7 +272,7 @@ public class DescriptionController { } @GetMapping("validate") - @Operation(summary = "Validate if a description is ready for finalization by id") + @OperationWithTenantHeader(summary = "Validate if a description is ready for finalization by id") @Hidden public List validate(@RequestParam("descriptionIds") List descriptionIds) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException { logger.debug(new MapLogEntry("validating" + Description.class.getSimpleName()).And("descriptionIds", descriptionIds)); @@ -285,7 +289,7 @@ public class DescriptionController { } @DeleteMapping("{id}") - @Operation(summary = "Delete a description by id") + @OperationWithTenantHeader(summary = "Delete a description by id") @Swagger404 @Transactional public void delete( @@ -299,7 +303,7 @@ public class DescriptionController { } @GetMapping("{id}/export/{type}") - @Operation(summary = "Export a description in various formats by id") + @OperationWithTenantHeader(summary = "Export a description in various formats by id") @Swagger404 public ResponseEntity export( @Parameter(name = "id", description = "The id of a description to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -311,7 +315,7 @@ public class DescriptionController { } @PostMapping("field-file/upload") - @Operation(summary = "Upload a file attachment on a field that supports it") + @OperationWithTenantHeader(summary = "Upload a file attachment on a field that supports it") @Swagger400 @Swagger404 @Transactional @@ -335,7 +339,7 @@ public class DescriptionController { } @GetMapping("{id}/field-file/{fileId}") - @Operation(summary = "Fetch a field file attachment as byte array") + @OperationWithTenantHeader(summary = "Fetch a field file attachment as byte array") @Swagger404 public ResponseEntity getFieldFile( @Parameter(name = "id", description = "The id of a description", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -362,7 +366,7 @@ public class DescriptionController { } @PostMapping("update-description-template") - @Operation(summary = "Change the template of a description") + @OperationWithTenantHeader(summary = "Change the template of a description") @Swagger400 @Swagger404 @Transactional @@ -379,7 +383,7 @@ public class DescriptionController { } @RequestMapping(method = RequestMethod.GET, value = "/xml/export/{id}", produces = "application/xml") - @Operation(summary = "Export a description in xml format by id") + @OperationWithTenantHeader(summary = "Export a description in xml format by id") @Swagger404 public @ResponseBody ResponseEntity getXml( @Parameter(name = "id", description = "The id of a description to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable UUID id diff --git a/backend/web/src/main/java/org/opencdmp/controllers/DmpController.java b/backend/web/src/main/java/org/opencdmp/controllers/DmpController.java index ce2bc8372..cfd819a6a 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/DmpController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/DmpController.java @@ -11,11 +11,14 @@ import gr.cite.tools.fieldset.FieldSet; import gr.cite.tools.logging.LoggerService; import gr.cite.tools.logging.MapLogEntry; import gr.cite.tools.validation.ValidationFilterAnnotation; +import io.swagger.annotations.ApiImplicitParam; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.xml.bind.JAXBException; @@ -25,9 +28,10 @@ import org.opencdmp.commons.enums.DmpAccessType; import org.opencdmp.commons.enums.DmpStatus; import org.opencdmp.commons.enums.IsActive; import org.opencdmp.controllers.swagger.SwaggerHelpers; +import org.opencdmp.controllers.swagger.annotation.OperationWithTenantHeader; import org.opencdmp.controllers.swagger.annotation.Swagger400; import org.opencdmp.controllers.swagger.annotation.Swagger404; -import org.opencdmp.controllers.swagger.annotation.SwaggerErrorResponses; +import org.opencdmp.controllers.swagger.annotation.SwaggerCommonErrorResponses; import org.opencdmp.filetransformerbase.models.misc.PreprocessingDmpModel; import org.opencdmp.model.DescriptionsToBeFinalized; import org.opencdmp.model.DmpUser; @@ -70,7 +74,7 @@ import static org.opencdmp.authorization.AuthorizationFlags.Public; @RestController @RequestMapping(path = "api/dmp") @Tag(name = "Plans", description = "Manage plans") -@SwaggerErrorResponses +@SwaggerCommonErrorResponses public class DmpController { private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(DmpController.class)); @@ -107,7 +111,7 @@ public class DmpController { } @PostMapping("public/query") - @Operation(summary = "Query public published plans") + @OperationWithTenantHeader(summary = "Query public published plans") @Hidden public QueryResult publicQuery(@RequestBody DmpLookup lookup) throws MyApplicationException, MyForbiddenException { logger.debug("querying {}", Dmp.class.getSimpleName()); @@ -123,7 +127,7 @@ public class DmpController { } @GetMapping("public/{id}") - @Operation(summary = "Fetch a specific public published plan by id") + @OperationWithTenantHeader(summary = "Fetch a specific public published plan by id") @Hidden public PublicDmp publicGet(@PathVariable("id") UUID id, FieldSet fieldSet, Locale locale) throws MyApplicationException, MyForbiddenException, MyNotFoundException { logger.debug(new MapLogEntry("retrieving" + Dmp.class.getSimpleName()).And("id", id).And("fields", fieldSet)); @@ -145,15 +149,21 @@ public class DmpController { } @PostMapping("query") - @Operation(summary = "Query all plans", description = SwaggerHelpers.Dmp.endpoint_query, requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(description = SwaggerHelpers.Dmp.endpoint_query_request_body, content = @Content(examples = @ExampleObject( - name = "Pagination and projection", - description = "Simple paginated request using a property projection list and pagination info", - value = SwaggerHelpers.Dmp.endpoint_query_request_body_example - ))), responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content(examples = @ExampleObject( - name = "First page", - description = "Example with the first page of paginated results", - value = SwaggerHelpers.Dmp.endpoint_query_response_example - )))) + @OperationWithTenantHeader(summary = "Query all plans", description = SwaggerHelpers.Dmp.endpoint_query, requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(description = SwaggerHelpers.Dmp.endpoint_query_request_body, content = @Content(examples = @ExampleObject( + name = "Pagination and projection", + description = "Simple paginated request using a property projection list and pagination info", + value = SwaggerHelpers.Dmp.endpoint_query_request_body_example + ))), responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = Dmp.class + ) + ), + examples = @ExampleObject( + name = "First page", + description = "Example with the first page of paginated results", + value = SwaggerHelpers.Dmp.endpoint_query_response_example + )))) public QueryResult Query(@RequestBody DmpLookup lookup) throws MyApplicationException, MyForbiddenException { logger.debug("querying {}", Dmp.class.getSimpleName()); @@ -167,7 +177,7 @@ public class DmpController { } @GetMapping("{id}") - @Operation(summary = "Fetch a specific plan by id") + @OperationWithTenantHeader(summary = "Fetch a specific plan by id") @Swagger404 public Dmp Get( @Parameter(name = "id", description = "The id of a plan to fetch", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -192,7 +202,7 @@ public class DmpController { } @PostMapping("persist") - @Operation(summary = "Create a new or update an existing plan") + @OperationWithTenantHeader(summary = "Create a new or update an existing plan") @Swagger400 @Swagger404 @Transactional @@ -214,7 +224,7 @@ public class DmpController { } @DeleteMapping("{id}") - @Operation(summary = "Delete a plan by id") + @OperationWithTenantHeader(summary = "Delete a plan by id") @Swagger404 @Transactional public void Delete( @@ -228,7 +238,7 @@ public class DmpController { } @PostMapping("finalize/{id}") - @Operation(summary = "Finalize a plan by id") + @OperationWithTenantHeader(summary = "Finalize a plan by id") @Swagger404 @Transactional public boolean finalize( @@ -248,7 +258,7 @@ public class DmpController { } @GetMapping("undo-finalize/{id}") - @Operation(summary = "Undo the finalization of a plan by id (only possible if it is not already deposited)") + @OperationWithTenantHeader(summary = "Undo the finalization of a plan by id (only possible if it is not already deposited)") @Swagger404 @Transactional public boolean undoFinalize( @@ -269,7 +279,7 @@ public class DmpController { } @GetMapping("validate/{id}") - @Operation(summary = "Validate if a plan is ready for finalization by id") + @OperationWithTenantHeader(summary = "Validate if a plan is ready for finalization by id") @Hidden public DmpValidationResult validate(@PathVariable("id") UUID id) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException { logger.debug(new MapLogEntry("validating" + Dmp.class.getSimpleName()).And("id", id)); @@ -286,7 +296,7 @@ public class DmpController { } @PostMapping("clone") - @Operation(summary = "Create a clone of an existing plan") + @OperationWithTenantHeader(summary = "Create a clone of an existing plan") @Swagger400 @Swagger404 @Transactional @@ -310,7 +320,7 @@ public class DmpController { } @PostMapping("new-version") - @Operation(summary = "Create a new version of an existing plan") + @OperationWithTenantHeader(summary = "Create a new version of an existing plan") @Swagger400 @Swagger404 @Transactional @@ -332,7 +342,7 @@ public class DmpController { } @PostMapping("{id}/assign-users") - @Operation(summary = "Assign users to the plan by id") + @OperationWithTenantHeader(summary = "Assign users to the plan by id") @Transactional @ValidationFilterAnnotation(validator = DmpUserPersist.DmpUserPersistValidator.ValidatorName, argumentName = "model") @Hidden @@ -350,7 +360,7 @@ public class DmpController { } @PostMapping("remove-user") - @Operation(summary = "Remove a user association with the plan") + @OperationWithTenantHeader(summary = "Remove a user association with the plan") @Transactional @ValidationFilterAnnotation(validator = DmpUserRemovePersist.DmpUserRemovePersistValidator.ValidatorName, argumentName = "model") @Hidden @@ -368,7 +378,7 @@ public class DmpController { } @GetMapping("{id}/export/{transformerId}/{type}") - @Operation(summary = "Export a plan in various formats by id") + @OperationWithTenantHeader(summary = "Export a plan in various formats by id") @Swagger404 public ResponseEntity export( @Parameter(name = "id", description = "The id of a plan to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable("id") UUID id, @@ -387,7 +397,7 @@ public class DmpController { } @PostMapping("{id}/invite-users") - @Operation(summary = "Send user invitations for the plan by id") + @OperationWithTenantHeader(summary = "Send user invitations for the plan by id") @Transactional @ValidationFilterAnnotation(validator = DmpUserInvitePersist.DmpUserInvitePersistValidator.ValidatorName, argumentName = "model") @Hidden @@ -404,7 +414,7 @@ public class DmpController { } @GetMapping("{id}/token/{token}/invite-accept") - @Operation(summary = "Accept an invitation token for a plan by id") + @OperationWithTenantHeader(summary = "Accept an invitation token for a plan by id") @Transactional @Hidden public boolean acceptInvitation(@PathVariable("id") UUID id, @PathVariable("token") String token) throws InvalidApplicationException, JAXBException, IOException { @@ -420,7 +430,7 @@ public class DmpController { } @RequestMapping(method = RequestMethod.GET, value = "/xml/export/{id}", produces = "application/xml") - @Operation(summary = "Export a plan in xml format by id") + @OperationWithTenantHeader(summary = "Export a plan in xml format by id") @Swagger404 public @ResponseBody ResponseEntity getXml( @Parameter(name = "id", description = "The id of a plan to export", example = "c0c163dc-2965-45a5-9608-f76030578609", required = true) @PathVariable UUID id @@ -436,7 +446,7 @@ public class DmpController { } @RequestMapping(method = RequestMethod.POST, value = "/xml/import") - @Operation(summary = "Import a plan from an xml file") + @OperationWithTenantHeader(summary = "Import a plan from an xml file") @Transactional public Dmp importXml( @RequestParam("file") MultipartFile file, @@ -455,7 +465,7 @@ public class DmpController { } @PostMapping("json/preprocessing") - @Operation(summary = "preprocessing a plan from an json file") + @OperationWithTenantHeader(summary = "preprocessing a plan from an json file") @Transactional public PreprocessingDmpModel preprocessing( @RequestParam("fileId") UUID fileId, @@ -474,7 +484,7 @@ public class DmpController { } @PostMapping("json/import") - @Operation(summary = "Import a plan from an json file") + @OperationWithTenantHeader(summary = "Import a plan from an json file") @ValidationFilterAnnotation(validator = DmpCommonModelConfig.DmpCommonModelConfigValidator.ValidatorName, argumentName = "model") @Transactional public Dmp importJson( diff --git a/backend/web/src/main/java/org/opencdmp/controllers/swagger/annotation/OperationWithTenantHeader.java b/backend/web/src/main/java/org/opencdmp/controllers/swagger/annotation/OperationWithTenantHeader.java new file mode 100644 index 000000000..8dde02481 --- /dev/null +++ b/backend/web/src/main/java/org/opencdmp/controllers/swagger/annotation/OperationWithTenantHeader.java @@ -0,0 +1,48 @@ +package org.opencdmp.controllers.swagger.annotation; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.extensions.Extension; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.servers.Server; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}) +@Operation +public @interface OperationWithTenantHeader { + @AliasFor(annotation = Operation.class, attribute = "method") String method() default ""; + @AliasFor(annotation = Operation.class, attribute = "tags") String[] tags() default {}; + @AliasFor(annotation = Operation.class, attribute = "summary") String summary() default ""; + @AliasFor(annotation = Operation.class, attribute = "description") String description() default ""; + @AliasFor(annotation = Operation.class, attribute = "requestBody") RequestBody requestBody() default @RequestBody; + @AliasFor(annotation = Operation.class, attribute = "externalDocs") ExternalDocumentation externalDocs() default @ExternalDocumentation; + @AliasFor(annotation = Operation.class, attribute = "operationId") String operationId() default ""; + @AliasFor(annotation = Operation.class, attribute = "parameters") Parameter[] parameters() default { + @Parameter( + name = "x-tenant", + description = "This is a header containing the tenant scope of the request. " + + "It is required on every authenticated request. " + + "If the request does not target a specific tenant resource, this header should be set to the value 'default'.", + required = true, + in = ParameterIn.HEADER, + schema = @Schema(implementation = String.class), + example = "default" + ) + }; + @AliasFor(annotation = Operation.class, attribute = "responses") ApiResponse[] responses() default {}; + @AliasFor(annotation = Operation.class, attribute = "deprecated") boolean deprecated() default false; + @AliasFor(annotation = Operation.class, attribute = "security") SecurityRequirement[] security() default {}; + @AliasFor(annotation = Operation.class, attribute = "servers") Server[] servers() default {}; + @AliasFor(annotation = Operation.class, attribute = "extensions") Extension[] extensions() default {}; + @AliasFor(annotation = Operation.class, attribute = "hidden") boolean hidden() default false; + @AliasFor(annotation = Operation.class, attribute = "ignoreJsonView") boolean ignoreJsonView() default false; +} diff --git a/backend/web/src/main/java/org/opencdmp/controllers/swagger/annotation/SwaggerErrorResponses.java b/backend/web/src/main/java/org/opencdmp/controllers/swagger/annotation/SwaggerCommonErrorResponses.java similarity index 82% rename from backend/web/src/main/java/org/opencdmp/controllers/swagger/annotation/SwaggerErrorResponses.java rename to backend/web/src/main/java/org/opencdmp/controllers/swagger/annotation/SwaggerCommonErrorResponses.java index 8bf210489..cbf27233c 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/swagger/annotation/SwaggerErrorResponses.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/swagger/annotation/SwaggerCommonErrorResponses.java @@ -7,6 +7,6 @@ import java.lang.annotation.*; @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}) @Swagger500 @Swagger403 -public @interface SwaggerErrorResponses { +public @interface SwaggerCommonErrorResponses { } diff --git a/docs/docs/documentation/for-devs/apis/swagger.md b/docs/docs/documentation/for-devs/apis/swagger.md index 49d6f8924..87667b7fb 100644 --- a/docs/docs/documentation/for-devs/apis/swagger.md +++ b/docs/docs/documentation/for-devs/apis/swagger.md @@ -12,13 +12,17 @@ import Admonition from '@theme/Admonition'; The swagger UI is available at the `/swagger-ui/index.html` url. It contains documentation for the following API endpoints. - + - **/api/public/dmps/\*\*** - **/api/public/descriptions/\*\*** + +

These endpoints do not require authentication.

+
+
- + - **/api/dmp/\*\*** - **/api/description/\*\***