From d8740cf3024ac8725b6089c1953a7b75f74d08b5 Mon Sep 17 00:00:00 2001 From: Gaurav Mishra Date: Fri, 10 Mar 2023 20:50:25 +0530 Subject: [PATCH] doc(rest): Generate OpenAPI docs for Project Use `org.springdoc.springdoc-openapi-ui` to generate openAPI docs as well as swagger UI. Signed-off-by: Gaurav Mishra --- pom.xml | 6 +- rest/resource-server/pom.xml | 22 +- .../resourceserver/Sw360ResourceServer.java | 48 +- .../core/JacksonCustomizations.java | 44 ++ .../core/OpenAPIPaginationHelper.java | 38 ++ .../ModerationRequestController.java | 2 - .../project/ProjectController.java | 458 +++++++++++++++--- .../project/Sw360ProjectService.java | 7 +- .../src/main/resources/application.yml | 15 +- .../restdocs/ProjectSpecTest.java | 4 +- 10 files changed, 565 insertions(+), 79 deletions(-) create mode 100644 rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/OpenAPIPaginationHelper.java diff --git a/pom.xml b/pom.xml index e932113466..435ae57047 100644 --- a/pom.xml +++ b/pom.xml @@ -150,6 +150,10 @@ 2.0.6.RELEASE 1.1.1.RELEASE 2.5.1.RELEASE + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 5.3.27 0.16.0 1.28.5 @@ -927,4 +931,4 @@ - \ No newline at end of file + diff --git a/rest/resource-server/pom.xml b/rest/resource-server/pom.xml index bcb8fe7add..54a063766e 100644 --- a/rest/resource-server/pom.xml +++ b/rest/resource-server/pom.xml @@ -206,6 +206,26 @@ jose4j 0.9.3 + + org.springdoc + springdoc-openapi-ui + ${springdoc-openapi-ui.version} + + + org.springdoc + springdoc-openapi-hateoas + ${springdoc-openapi-hateos.version} + + + org.springdoc + springdoc-openapi-security + ${springdoc-openapi-security.version} + + + org.springdoc + springdoc-openapi-webmvc-core + ${springdoc-openapi-webmvc.version} + @@ -331,4 +351,4 @@ - \ No newline at end of file + diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java index 5b9555e348..7c98ccb381 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java @@ -10,15 +10,22 @@ package org.eclipse.sw360.rest.resourceserver; -import java.util.Properties; -import java.util.Set; - +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.OAuthFlow; +import io.swagger.v3.oas.models.security.OAuthFlows; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; import org.eclipse.sw360.datahandler.common.CommonUtils; import org.eclipse.sw360.datahandler.thrift.users.UserGroup; import org.eclipse.sw360.rest.common.PropertyUtils; import org.eclipse.sw360.rest.common.Sw360CORSFilter; +import org.eclipse.sw360.rest.resourceserver.core.OpenAPIPaginationHelper; import org.eclipse.sw360.rest.resourceserver.core.RestControllerHelper; import org.eclipse.sw360.rest.resourceserver.security.apiToken.ApiTokenAuthenticationFilter; +import org.springdoc.core.SpringDocUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; @@ -34,6 +41,10 @@ import org.springframework.web.filter.ForwardedHeaderFilter; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import java.util.List; +import java.util.Properties; +import java.util.Set; + @SpringBootApplication @Import(Sw360CORSFilter.class) public class Sw360ResourceServer extends SpringBootServletInitializer { @@ -60,6 +71,8 @@ public class Sw360ResourceServer extends SpringBootServletInitializer { public static final UserGroup CONFIG_ADMIN_ACCESS_USERGROUP; private static final String DEFAULT_WRITE_ACCESS_USERGROUP = UserGroup.SW360_ADMIN.name(); private static final String DEFAULT_ADMIN_ACCESS_USERGROUP = UserGroup.SW360_ADMIN.name(); + private static final String SERVER_PATH_URL; + private static final String APPLICATION_NAME = "/resource"; static { Properties props = CommonUtils.loadProperties(Sw360ResourceServer.class, SW360_PROPERTIES_FILE_PATH); @@ -76,6 +89,13 @@ public class Sw360ResourceServer extends SpringBootServletInitializer { System.getProperty("RunRestForceUpdateTest", props.getProperty("rest.force.update.enabled", "false"))); CONFIG_WRITE_ACCESS_USERGROUP = UserGroup.valueOf(props.getProperty("rest.write.access.usergroup", DEFAULT_WRITE_ACCESS_USERGROUP)); CONFIG_ADMIN_ACCESS_USERGROUP = UserGroup.valueOf(props.getProperty("rest.admin.access.usergroup", DEFAULT_ADMIN_ACCESS_USERGROUP)); + SERVER_PATH_URL = props.getProperty("backend.url", "http://localhost:8080"); + + SpringDocUtils.getConfig() + .replaceWithClass(org.springframework.data.domain.Pageable.class, + OpenAPIPaginationHelper.class) + .replaceWithClass(org.springframework.data.domain.PageRequest.class, + OpenAPIPaginationHelper.class); } @Bean @@ -119,4 +139,26 @@ public FilterRegistrationBean forwardedHeaderFilter() { bean.setFilter(new ForwardedHeaderFilter()); return bean; } + + @Bean + public OpenAPI customOpenAPI() { + Server server = new Server(); + server.setUrl(SERVER_PATH_URL + APPLICATION_NAME + REST_BASE_PATH); + server.setDescription("Current instance."); + return new OpenAPI() + .components(new Components() + .addSecuritySchemes("tokenAuth", + new SecurityScheme().type(SecurityScheme.Type.APIKEY).name("Authorization") + .in(SecurityScheme.In.HEADER) + .description("Enter the token with the `Token ` prefix, e.g. \"Token abcde12345\".")) + .addSecuritySchemes("oauth", + new SecurityScheme().type(SecurityScheme.Type.OAUTH2) + .flows(new OAuthFlows().password(new OAuthFlow() + .tokenUrl(SERVER_PATH_URL + "/authorization/oauth/token") + .refreshUrl(SERVER_PATH_URL + "/authorization/oauth/token")) + ))) + .info(new Info().title("SW360 API").license(new License().name("EPL-2.0") + .url("https://github.com/eclipse-sw360/sw360/blob/main/LICENSE"))) + .servers(List.of(server)); + } } diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/JacksonCustomizations.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/JacksonCustomizations.java index 4820008d6a..2cf1e87741 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/JacksonCustomizations.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/JacksonCustomizations.java @@ -48,6 +48,7 @@ import org.eclipse.sw360.rest.resourceserver.moderationrequest.EmbeddedModerationRequest; import org.eclipse.sw360.rest.resourceserver.moderationrequest.ModerationPatch; import org.eclipse.sw360.rest.resourceserver.project.EmbeddedProject; +import org.springdoc.core.SpringDocUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -105,6 +106,49 @@ public Sw360Module() { setMixInAnnotation(EmbeddedModerationRequest.class, Sw360Module.EmbeddedModerationRequestMixin.class); setMixInAnnotation(ImportBomRequestPreparation.class, Sw360Module.ImportBomRequestPreparationMixin.class); setMixInAnnotation(ModerationPatch.class, Sw360Module.ModerationPatchMixin.class); + + // Make spring doc aware of the mixin(s) + SpringDocUtils.getConfig() + .replaceWithClass(Project.class, Sw360Module.ProjectMixin.class) + .replaceWithClass(MultiStatus.class, MultiStatusMixin.class) + .replaceWithClass(User.class, Sw360Module.UserMixin.class) + .replaceWithClass(Component.class, Sw360Module.ComponentMixin.class) + .replaceWithClass(Release.class, Sw360Module.ReleaseMixin.class) + .replaceWithClass(ReleaseLink.class, Sw360Module.ReleaseLinkMixin.class) + .replaceWithClass(ClearingReport.class, Sw360Module.ClearingReportMixin.class) + .replaceWithClass(Attachment.class, Sw360Module.AttachmentMixin.class) + .replaceWithClass(AttachmentDTO.class, Sw360Module.AttachmentDTOMixin.class) + .replaceWithClass(UsageAttachment.class, Sw360Module.UsageAttachmentMixin.class) + .replaceWithClass(ProjectUsage.class, Sw360Module.ProjectUsageMixin.class) + .replaceWithClass(Vendor.class, Sw360Module.VendorMixin.class) + .replaceWithClass(License.class, Sw360Module.LicenseMixin.class) + .replaceWithClass(Obligation.class, Sw360Module.ObligationMixin.class) + .replaceWithClass(Vulnerability.class, Sw360Module.VulnerabilityMixin.class) + .replaceWithClass(VulnerabilityState.class, Sw360Module.VulnerabilityStateMixin.class) + .replaceWithClass(ReleaseVulnerabilityRelationDTO.class, Sw360Module.ReleaseVulnerabilityRelationDTOMixin.class) + .replaceWithClass(VulnerabilityDTO.class, Sw360Module.VulnerabilityDTOMixin.class) + .replaceWithClass(VulnerabilityApiDTO.class, Sw360Module.VulnerabilityApiDTOMixin.class) + .replaceWithClass(EccInformation.class, Sw360Module.EccInformationMixin.class) + .replaceWithClass(EmbeddedProject.class, Sw360Module.EmbeddedProjectMixin.class) + .replaceWithClass(ExternalToolProcess.class, Sw360Module.ExternalToolProcessMixin.class) + .replaceWithClass(ExternalToolProcessStep.class, Sw360Module.ExternalToolProcessStepMixin.class) + .replaceWithClass(COTSDetails.class, Sw360Module.COTSDetailsMixin.class) + .replaceWithClass(ClearingInformation.class, Sw360Module.ClearingInformationMixin.class) + .replaceWithClass(Repository.class, Sw360Module.RepositoryMixin.class) + .replaceWithClass(SearchResult.class, Sw360Module.SearchResultMixin.class) + .replaceWithClass(ChangeLogs.class, Sw360Module.ChangeLogsMixin.class) + .replaceWithClass(ChangedFields.class, Sw360Module.ChangedFieldsMixin.class) + .replaceWithClass(ReferenceDocData.class, Sw360Module.ReferenceDocDataMixin.class) + .replaceWithClass(ClearingRequest.class, Sw360Module.ClearingRequestMixin.class) + .replaceWithClass(Comment.class, Sw360Module.CommentMixin.class) + .replaceWithClass(ProjectReleaseRelationship.class, Sw360Module.ProjectReleaseRelationshipMixin.class) + .replaceWithClass(ReleaseVulnerabilityRelation.class, Sw360Module.ReleaseVulnerabilityRelationMixin.class) + .replaceWithClass(VerificationStateInfo.class, Sw360Module.VerificationStateInfoMixin.class) + .replaceWithClass(ProjectProjectRelationship.class, Sw360Module.ProjectProjectRelationshipMixin.class) + .replaceWithClass(ModerationRequest.class, Sw360Module.ModerationRequestMixin.class) + .replaceWithClass(EmbeddedModerationRequest.class, Sw360Module.EmbeddedModerationRequestMixin.class) + .replaceWithClass(ImportBomRequestPreparation.class, Sw360Module.ImportBomRequestPreparationMixin.class) + .replaceWithClass(ModerationPatch.class, Sw360Module.ModerationPatchMixin.class); } @JsonInclude(JsonInclude.Include.NON_NULL) diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/OpenAPIPaginationHelper.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/OpenAPIPaginationHelper.java new file mode 100644 index 0000000000..40bc7e71a3 --- /dev/null +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/OpenAPIPaginationHelper.java @@ -0,0 +1,38 @@ +/* + * Copyright Siemens AG, 2023. Part of the SW360 Portal Project. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.sw360.rest.resourceserver.core; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Pojo class to show correct options for pagination in OpenAPI doc. + */ +//@JsonMixin +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +@Schema +public class OpenAPIPaginationHelper { + @Schema(description = "Page number to fetch, starts from 0", type = "int", + defaultValue = "0", name = "page") + private int pageNumber; + @Schema(description = "Number of entries per page", type = "int", + defaultValue = "10", name = "page_entries") + private int pageEntries; + @Schema(description = "Sorting of entries", type = "string", + example = "name,desc", name = "sort") + private String sort; +} diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/moderationrequest/ModerationRequestController.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/moderationrequest/ModerationRequestController.java index 19e5c9a33e..393f13eb55 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/moderationrequest/ModerationRequestController.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/moderationrequest/ModerationRequestController.java @@ -49,7 +49,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.HttpClientErrorException; import javax.servlet.http.HttpServletRequest; @@ -63,7 +62,6 @@ import static org.eclipse.sw360.rest.resourceserver.moderationrequest.Sw360ModerationRequestService.isOpenModerationRequest; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; -@RestController @BasePathAwareController @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class ModerationRequestController implements RepresentationModelProcessor { diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java index 0637cb2982..c3398238f2 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java @@ -20,6 +20,13 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; +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.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.apache.commons.lang.StringUtils; @@ -57,6 +64,7 @@ import org.eclipse.sw360.datahandler.thrift.licenseinfo.LicenseInfoParsingResult; import org.eclipse.sw360.datahandler.thrift.licenseinfo.LicenseNameWithText; import org.eclipse.sw360.datahandler.thrift.licenseinfo.OutputFormatInfo; +import org.eclipse.sw360.datahandler.thrift.licenseinfo.OutputFormatVariant; import org.eclipse.sw360.datahandler.thrift.licenses.License; import org.eclipse.sw360.datahandler.thrift.projects.Project; import org.eclipse.sw360.datahandler.thrift.projects.ProjectClearingState; @@ -103,6 +111,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @@ -117,7 +126,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -136,6 +144,8 @@ @BasePathAwareController @RequiredArgsConstructor(onConstructor = @__(@Autowired)) +@RestController +@SecurityRequirement(name = "tokenAuth") public class ProjectController implements RepresentationModelProcessor { public static final String PROJECTS_URL = "/projects"; public static final String SW360_ATTACHMENT_USAGES = "sw360:attachmentUsages"; @@ -186,14 +196,25 @@ public class ProjectController implements RepresentationModelProcessor>> getProjectsForUser( Pageable pageable, + @Parameter(description = "The name of the project") @RequestParam(value = "name", required = false) String name, + @Parameter(description = "The type of the project") @RequestParam(value = "type", required = false) String projectType, + @Parameter(description = "The group of the project") @RequestParam(value = "group", required = false) String group, + @Parameter(description = "The tag of the project") @RequestParam(value = "tag", required = false) String tag, + @Parameter(description = "Flag to get projects with all details.") @RequestParam(value = "allDetails", required = false) boolean allDetails, + @Parameter(description = "List project by lucene search") @RequestParam(value = "luceneSearch", required = false) boolean luceneSearch, HttpServletRequest request) throws TException, URISyntaxException, PaginationParameterException, ResourceClassNotFoundException { User sw360User = restControllerHelper.getSw360UserFromAuthentication(); @@ -295,19 +316,34 @@ private ResponseEntity>> getProjectResponse return new ResponseEntity<>(resources, status); } + @Operation( + description = "List all projects associated to the user.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/myprojects", method = RequestMethod.GET) public ResponseEntity>> getProjectsFilteredForUser( Pageable pageable, + @Parameter(description = "Projects with current user as creator.") @RequestParam(value = "createdBy", required = false, defaultValue = "true") boolean createdBy, + @Parameter(description = "Projects with current user as moderator.") @RequestParam(value = "moderator", required = false, defaultValue = "true") boolean moderator, + @Parameter(description = "Projects with current user as contributor.") @RequestParam(value = "contributor", required = false, defaultValue = "true") boolean contributor, + @Parameter(description = "Projects with current user as owner.") @RequestParam(value = "projectOwner", required = false, defaultValue = "true") boolean projectOwner, + @Parameter(description = "Projects with current user as lead architect.") @RequestParam(value = "leadArchitect", required = false, defaultValue = "true") boolean leadArchitect, + @Parameter(description = "Projects with current user as project responsible.") @RequestParam(value = "projectResponsible", required = false, defaultValue = "true") boolean projectResponsible, + @Parameter(description = "Projects with current user as security responsible.") @RequestParam(value = "securityResponsible", required = false, defaultValue = "true") boolean securityResponsible, + @Parameter(description = "Projects with state as open.") @RequestParam(value = "stateOpen", required = false, defaultValue = "true") boolean stateOpen, + @Parameter(description = "Projects with state as closed.") @RequestParam(value = "stateClosed", required = false, defaultValue = "true") boolean stateClosed, + @Parameter(description = "Projects with state as in progress.") @RequestParam(value = "stateInProgress", required = false, defaultValue = "true") boolean stateInProgress, + @Parameter(description = "Flag to get projects with all details.") @RequestParam(value = "allDetails", required = false) boolean allDetails, HttpServletRequest request) throws TException, URISyntaxException, PaginationParameterException, ResourceClassNotFoundException { User sw360User = restControllerHelper.getSw360UserFromAuthentication(); @@ -336,8 +372,13 @@ public ResponseEntity>> getProjectsFiltered mapOfProjects, true, sw360Projects, false); } + @Operation( + description = "Get a single project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}", method = RequestMethod.GET) public ResponseEntity> getProject( + @Parameter(description = "Project ID", example = "376576") @PathVariable("id") String id) throws TException { User sw360User = restControllerHelper.getSw360UserFromAuthentication(); Project sw360Project = projectService.getProjectForUserById(id, sw360User); @@ -345,9 +386,14 @@ public ResponseEntity> getProject( return new ResponseEntity<>(userHalResource, HttpStatus.OK); } + @Operation( + description = "Get linked projects of a single project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/linkedProjects", method = RequestMethod.GET) public ResponseEntity> getLinkedProject(Pageable pageable, - @PathVariable("id") String id,@RequestParam(value = "transitive", required = false) String transitive, HttpServletRequest request) + @Parameter(description = "Project ID", example = "376576") + @PathVariable("id") String id,@RequestParam(value = "transitive", required = false) String transitive, HttpServletRequest request) throws TException, URISyntaxException, PaginationParameterException, ResourceClassNotFoundException { User sw360User = restControllerHelper.getSw360UserFromAuthentication(); @@ -389,25 +435,37 @@ public ResponseEntity> getLinkedProject(Pageable pa return new ResponseEntity<>(resources, status); } + @Operation( + description = "Delete a single project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}", method = RequestMethod.DELETE) - public ResponseEntity deleteProject(@PathVariable("id") String id) throws TException { + public ResponseEntity deleteProject( + @Parameter(description = "Project ID") + @PathVariable("id") String id) throws TException { User sw360User = restControllerHelper.getSw360UserFromAuthentication(); RequestStatus requestStatus = projectService.deleteProject(id, sw360User); - if(requestStatus == RequestStatus.SUCCESS) { + if (requestStatus == RequestStatus.SUCCESS) { return new ResponseEntity<>(HttpStatus.OK); - } else if(requestStatus == RequestStatus.IN_USE) { + } else if (requestStatus == RequestStatus.IN_USE) { return new ResponseEntity<>(HttpStatus.CONFLICT); } else if (requestStatus == RequestStatus.SENT_TO_MODERATOR) { - return new ResponseEntity<>(RESPONSE_BODY_FOR_MODERATION_REQUEST,HttpStatus.ACCEPTED); + return new ResponseEntity<>(RESPONSE_BODY_FOR_MODERATION_REQUEST, HttpStatus.ACCEPTED); } else { return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + description = "Create a project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL, method = RequestMethod.POST) - public ResponseEntity createProject(@RequestBody Map reqBodyMap) - throws URISyntaxException, TException { + public ResponseEntity createProject( + @Parameter(schema = @Schema(implementation = Project.class)) + @RequestBody Map reqBodyMap + ) throws URISyntaxException, TException { Project project = convertToProject(reqBodyMap); if (project.getReleaseIdToUsage() != null) { @@ -434,9 +492,17 @@ public ResponseEntity createProject(@RequestBody Map reqBodyMap) } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + description = "Create a duplicate project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/duplicate/{id}", method = RequestMethod.POST) - public ResponseEntity> createDuplicateProject(@PathVariable("id") String id, - @RequestBody Map reqBodyMap) throws TException { + public ResponseEntity> createDuplicateProject( + @Parameter(description = "Project ID to copy.") + @PathVariable("id") String id, + @Parameter(schema = @Schema(implementation = Project.class)) + @RequestBody Map reqBodyMap + ) throws TException { if (!reqBodyMap.containsKey("name") && !reqBodyMap.containsKey("version")) { throw new HttpMessageNotReadableException( "Field name or version should be present in request body to create duplicate of a project"); @@ -466,10 +532,24 @@ public ResponseEntity> createDuplicateProject(@PathVariable } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + summary = "Link releases to the project.", + description = "Pass an array of release ids to be linked as request body.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/releases", method = RequestMethod.POST) public ResponseEntity linkReleases( + @Parameter(description = "Project ID.") @PathVariable("id") String id, - @RequestBody Object releasesInRequestBody) throws URISyntaxException, TException { + @Parameter(description = "Array of release IDs to be linked.", + examples = { + @ExampleObject(value = "[\"3765276512\",\"5578999\",\"3765276513\"]"), + @ExampleObject(value = "[\"/releases/5578999\"]") + // TODO: Add example for MAP value + } + ) + @RequestBody Object releasesInRequestBody + ) throws URISyntaxException, TException { RequestStatus linkReleasesStatus = addOrPatchReleasesToProject(id, releasesInRequestBody, false); HttpStatus status = HttpStatus.CREATED; if (linkReleasesStatus == RequestStatus.SENT_TO_MODERATOR) { @@ -479,9 +559,24 @@ public ResponseEntity linkReleases( } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + summary = "Append new releases to existing releases in a project.", + description = "Pass an array of release ids or a map of release id to usage to be linked as request body.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/releases", method = RequestMethod.PATCH) - public ResponseEntity patchReleases(@PathVariable("id") String id, @RequestBody Object releaseURIs) - throws URISyntaxException, TException { + public ResponseEntity patchReleases( + @Parameter(description = "Project ID.") + @PathVariable("id") String id, + @Parameter(description = "Array of release IDs to be linked.", + examples = { + @ExampleObject(value = "[\"3765276512\",\"5578999\",\"3765276513\"]"), + @ExampleObject(value = "[\"/releases/5578999\"]") + // TODO: Add example for MAP value + } + ) + @RequestBody Object releaseURIs + ) throws URISyntaxException, TException { RequestStatus patchReleasesStatus = addOrPatchReleasesToProject(id, releaseURIs, true); if (patchReleasesStatus == RequestStatus.SENT_TO_MODERATOR) { return new ResponseEntity<>(RESPONSE_BODY_FOR_MODERATION_REQUEST, HttpStatus.ACCEPTED); @@ -490,9 +585,24 @@ public ResponseEntity patchReleases(@PathVariable("id") String id, @RequestBody } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + summary = "Add/link packages to the project.", + description = "Pass a set of package ids to be linked as request body.", + responses = { + @ApiResponse(responseCode = "201", description = "Packages are linked to the project."), + @ApiResponse(responseCode = "202", description = "Moderation request is created.") + }, + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/link/packages", method = RequestMethod.PATCH) - public ResponseEntity linkPackages(@PathVariable("id") String id, - @RequestBody Set packagesInRequestBody) throws URISyntaxException, TException { + public ResponseEntity linkPackages( + @Parameter(description = "Project ID.", example = "376576") + @PathVariable("id") String id, + @Parameter(description = "Set of package IDs to be linked.", + example = "[\"3765276512\",\"5578999\",\"3765276513\"]" + ) + @RequestBody Set packagesInRequestBody + ) throws URISyntaxException, TException { RequestStatus linkPackageStatus = linkOrUnlinkPackages(id, packagesInRequestBody, true); if (linkPackageStatus == RequestStatus.SENT_TO_MODERATOR) { return new ResponseEntity<>(RESPONSE_BODY_FOR_MODERATION_REQUEST, HttpStatus.ACCEPTED); @@ -501,9 +611,24 @@ public ResponseEntity linkPackages(@PathVariable("id") String id, } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + summary = "Remove/unlink packages from the project.", + description = "Pass a set of package ids to be unlinked as request body.", + responses = { + @ApiResponse(responseCode = "201", description = "Packages are unlinked from the project."), + @ApiResponse(responseCode = "202", description = "Moderation request is created.") + }, + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/unlink/packages", method = RequestMethod.PATCH) - public ResponseEntity patchPackages(@PathVariable("id") String id, - @RequestBody Set packagesInRequestBody) throws URISyntaxException, TException { + public ResponseEntity patchPackages( + @Parameter(description = "Project ID.", example = "376576") + @PathVariable("id") String id, + @Parameter(description = "Set of package IDs to be linked.", + example = "[\"3765276512\",\"5578999\",\"3765276513\"]" + ) + @RequestBody Set packagesInRequestBody + ) throws URISyntaxException, TException { RequestStatus patchPackageStatus = linkOrUnlinkPackages(id, packagesInRequestBody, false); if (patchPackageStatus == RequestStatus.SENT_TO_MODERATOR) { return new ResponseEntity<>(RESPONSE_BODY_FOR_MODERATION_REQUEST, HttpStatus.ACCEPTED); @@ -511,16 +636,23 @@ public ResponseEntity patchPackages(@PathVariable("id") String id, return new ResponseEntity<>(HttpStatus.CREATED); } + @Operation( + description = "Get releases of a single project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/releases", method = RequestMethod.GET) public ResponseEntity>> getProjectReleases( Pageable pageable, + @Parameter(description = "Project ID.") @PathVariable("id") String id, - @RequestParam(value = "transitive", required = false) String transitive,HttpServletRequest request) throws TException, URISyntaxException, PaginationParameterException, ResourceClassNotFoundException { + @Parameter(description = "Get the transitive releases?") + @RequestParam(value = "transitive", required = false, defaultValue = "false") boolean transitive, + HttpServletRequest request + ) throws TException, URISyntaxException, PaginationParameterException, ResourceClassNotFoundException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final Set releaseIds = projectService.getReleaseIds(id, sw360User, transitive); final Set releaseIdsInBranch = new HashSet<>(); - boolean isTransitive = Boolean.parseBoolean(transitive); List releases = releaseIds.stream().map(relId -> wrapTException(() -> { final Release sw360Release = releaseService.getReleaseForUserById(relId, sw360User); @@ -534,7 +666,7 @@ public ResponseEntity>> getProjectReleases( .map(sw360Release -> wrapTException(() -> { final Release embeddedRelease = restControllerHelper.convertToEmbeddedRelease(sw360Release); final HalResource releaseResource = new HalResource<>(embeddedRelease); - if (isTransitive) { + if (transitive) { projectService.addEmbeddedlinkedRelease(sw360Release, sw360User, releaseResource, releaseService, releaseIdsInBranch); } @@ -552,17 +684,27 @@ public ResponseEntity>> getProjectReleases( return new ResponseEntity<>(resources, status); } + @Operation( + description = "Get releases of multiple projects.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/releases", method = RequestMethod.GET) public ResponseEntity>> getProjectsReleases( Pageable pageable, + @Parameter(description = "List of project IDs to get release for.", example = "[\"376576\",\"376570\"]") @RequestBody List projectIds, - @RequestParam(value = "transitive", required = false) String transitive,@RequestParam(value = "clearingState", required = false) String clState, HttpServletRequest request) throws TException, URISyntaxException, PaginationParameterException, ResourceClassNotFoundException { + @Parameter(description = "Get the transitive releases") + @RequestParam(value = "transitive", required = false) boolean transitive, + @Parameter(description = "The clearing state of the release.", + schema = @Schema(implementation = ClearingState.class)) + @RequestParam(value = "clearingState", required = false) String clState, + HttpServletRequest request + ) throws URISyntaxException, PaginationParameterException, ResourceClassNotFoundException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final Set releaseIdsInBranch = new HashSet<>(); - boolean isTransitive = Boolean.parseBoolean(transitive); - Set releases = projectService.getReleasesFromProjectIds(projectIds, transitive, sw360User,releaseService); + Set releases = projectService.getReleasesFromProjectIds(projectIds, transitive, sw360User, releaseService); if (null != clState) { ClearingState cls = ThriftEnumUtils.stringToEnum(clState, ClearingState.class); @@ -576,7 +718,7 @@ public ResponseEntity>> getProjectsReleases .map(sw360Release -> wrapTException(() -> { final Release embeddedRelease = restControllerHelper.convertToEmbeddedReleaseWithDet(sw360Release); final HalResource releaseResource = new HalResource<>(embeddedRelease); - if (isTransitive) { + if (transitive) { projectService.addEmbeddedlinkedRelease(sw360Release, sw360User, releaseResource, releaseService, releaseIdsInBranch); } @@ -592,10 +734,17 @@ public ResponseEntity>> getProjectsReleases return new ResponseEntity<>(resources, status); } + @Operation( + description = "Get all releases with ECC information of a single project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/releases/ecc", method = RequestMethod.GET) public ResponseEntity>> getECCsOfReleases( + @Parameter(description = "Project ID.") @PathVariable("id") String id, - @RequestParam(value = "transitive", required = false) String transitive) throws TException { + @Parameter(description = "Get the transitive ECC") + @RequestParam(value = "transitive", required = false) boolean transitive + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final Set releaseIds = projectService.getReleaseIds(id, sw360User, transitive); @@ -614,14 +763,29 @@ public ResponseEntity>> getECCsOfReleases( return new ResponseEntity<>(resources, status); } + @Operation( + description = "Get vulnerabilities of a single project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/vulnerabilities", method = RequestMethod.GET) public ResponseEntity>> getVulnerabilitiesOfReleases( Pageable pageable, - @PathVariable("id") String id, @RequestParam(value = "priority") Optional priority, + @Parameter(description = "Project ID.") + @PathVariable("id") String id, + @Parameter(description = "The priority of vulnerability.", + examples = {@ExampleObject(value = "1 - critical"), @ExampleObject(value = "2 - major")} + ) + @RequestParam(value = "priority") Optional priority, + @Parameter(description = "The relevance of project of the vulnerability.", + schema = @Schema(implementation = VulnerabilityRatingForProject.class) + ) @RequestParam(value = "projectRelevance") Optional projectRelevance, + @Parameter(description = "The release Id of vulnerability.") @RequestParam(value = "releaseId") Optional releaseId, + @Parameter(description = "The external Id of vulnerability.") @RequestParam(value = "externalId") Optional externalId, - HttpServletRequest request) throws URISyntaxException, PaginationParameterException, ResourceClassNotFoundException { + HttpServletRequest request + ) throws URISyntaxException, PaginationParameterException, ResourceClassNotFoundException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final List allVulnerabilityDTOs = vulnerabilityService.getVulnerabilitiesByProjectId(id, sw360User); @@ -673,9 +837,17 @@ public ResponseEntity>> getVulnera return new ResponseEntity<>(resources, status); } + @Operation( + description = "Patch vulnerabilities of a single project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/vulnerabilities", method = RequestMethod.PATCH, consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity>> updateVulnerabilitiesOfReleases( - @PathVariable("id") String id, @RequestBody List vulnDTOs) { + @Parameter(description = "Project ID.") + @PathVariable("id") String id, + @Parameter(description = "Vulnerability list") + @RequestBody List vulnDTOs + ) { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); List actualVDto = vulnerabilityService.getVulnerabilitiesByProjectId(id, sw360User); @@ -683,7 +855,7 @@ public ResponseEntity>> updateVuln Set externalIdsFromRequestDto = vulnDTOs.stream().map(VulnerabilityDTO::getExternalId).collect(Collectors.toSet()); Set commonExtIds = Sets.intersection(actualExternalId, externalIdsFromRequestDto); - if(CommonUtils.isNullOrEmptyCollection(commonExtIds) || commonExtIds.size() != externalIdsFromRequestDto.size()) { + if (CommonUtils.isNullOrEmptyCollection(commonExtIds) || commonExtIds.size() != externalIdsFromRequestDto.size()) { throw new HttpMessageNotReadableException("External ID is not valid"); } @@ -716,10 +888,20 @@ public ResponseEntity>> updateVuln } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + summary = "Link releases to the project with usage.", + description = "Pass a map of release id to usage to be linked as request body.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/release/{releaseId}", method = RequestMethod.PATCH) public ResponseEntity> patchProjectReleaseUsage( - @PathVariable("id") String id, @PathVariable("releaseId") String releaseId, - @RequestBody ProjectReleaseRelationship requestBodyProjectReleaseRelationship) throws TException { + @Parameter(description = "Project ID.") + @PathVariable("id") String id, + @Parameter(description = "Release ID.") + @PathVariable("releaseId") String releaseId, + @Parameter(description = "Map of release id to usage.") + @RequestBody ProjectReleaseRelationship requestBodyProjectReleaseRelationship + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final Project sw360Project = projectService.getProjectForUserById(id, sw360User); Map releaseIdToUsage = sw360Project.getReleaseIdToUsage(); @@ -783,8 +965,15 @@ public ProjectVulnerabilityRating updateProjectVulnerabilityRatingFromRequest(Op return projectVulnerabilityRating; } + @Operation( + description = "Get license of releases of a single project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/licenses", method = RequestMethod.GET) - public ResponseEntity>> getLicensesOfReleases(@PathVariable("id") String id) throws TException { + public ResponseEntity>> getLicensesOfReleases( + @Parameter(description = "Project ID.") + @PathVariable("id") String id + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final Project project = projectService.getProjectForUserById(id, sw360User); final List> licenseResources = new ArrayList<>(); @@ -810,13 +999,30 @@ public ResponseEntity>> getLicensesOfReleas return new ResponseEntity<>(resources, status); } + @Operation( + summary = "Download license info for the project.", + description = "Set the request parameter `&template=` for variant `REPORT` to choose " + + "specific template.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/licenseinfo", method = RequestMethod.GET) - public void downloadLicenseInfo(@PathVariable("id") String id, - @RequestParam("generatorClassName") String generatorClassName, - @RequestParam("variant") String variant, - @RequestParam(value = "externalIds", required=false) String externalIds, - @RequestParam(value = "template", required = false ) String template, - HttpServletResponse response) throws TException, IOException { + public void downloadLicenseInfo( + @Parameter(description = "Project ID.", example = "376576") + @PathVariable("id") String id, + @Parameter(description = "Output generator class", + schema = @Schema(type = "string", + allowableValues = {"DocxGenerator", "XhtmlGenerator", + "TextGenerator"} + )) + @RequestParam("generatorClassName") String generatorClassName, + @Parameter(description = "Variant of the report", + schema = @Schema(implementation = OutputFormatVariant.class)) + @RequestParam("variant") String variant, + @Parameter(description = "The external Ids of the project", example = "376577") + @RequestParam(value = "externalIds", required = false) String externalIds, + @RequestParam(value = "template", required = false) String template, + HttpServletResponse response + ) throws TException, IOException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final Project sw360Project = projectService.getProjectForUserById(id, sw360User); @@ -890,7 +1096,7 @@ public void downloadLicenseInfo(@PathVariable("id") String id, } private Set getExcludedLicenses(Set excludedLicenseIds, - List licenseInfoParsingResult) { + List licenseInfoParsingResult) { Predicate filteredLicense = licenseNameWithText -> excludedLicenseIds .contains(licenseNameWithText.getLicenseName()); @@ -900,9 +1106,15 @@ private Set getExcludedLicenses(Set excludedLicense .flatMap(streamLicenseNameWithTexts).filter(filteredLicense).collect(Collectors.toSet()); } + @Operation( + description = "Get all attachment information of a project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/attachments", method = RequestMethod.GET) public ResponseEntity>> getProjectAttachments( - @PathVariable("id") String id) throws TException { + @Parameter(description = "Project ID.") + @PathVariable("id") String id + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final Project sw360Project = projectService.getProjectForUserById(id, sw360User); final CollectionModel> resources = attachmentService.getResourcesFromList(sw360Project.getAttachments()); @@ -910,10 +1122,18 @@ public ResponseEntity>> getProjectAttach } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + description = "Update and attachment usage for project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/attachment/{attachmentId}", method = RequestMethod.PATCH) - public ResponseEntity> patchProjectAttachmentInfo(@PathVariable("id") String id, - @PathVariable("attachmentId") String attachmentId, @RequestBody Attachment attachmentData) - throws TException { + public ResponseEntity> patchProjectAttachmentInfo( + @Parameter(description = "Project ID.") + @PathVariable("id") String id, + @Parameter(description = "Attachment ID.") + @PathVariable("attachmentId") String attachmentId, + @RequestBody Attachment attachmentData + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final Project sw360Project = projectService.getProjectForUserById(id, sw360User); Set attachments = sw360Project.getAttachments(); @@ -926,25 +1146,45 @@ public ResponseEntity> patchProjectAttachmentInfo(@PathV return new ResponseEntity<>(attachmentResource, HttpStatus.OK); } + @Operation( + summary = "Download an attachment of a project", + description = "Download an attachment of a project. Set the Accept-Header `application/*`. " + + "Only this Accept-Header is supported.", + responses = @ApiResponse( + content = {@Content(mediaType = "application/*")} + ), + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{projectId}/attachments/{attachmentId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public void downloadAttachmentFromProject( + @Parameter(description = "Project ID.") @PathVariable("projectId") String projectId, + @Parameter(description = "Attachment ID.") @PathVariable("attachmentId") String attachmentId, - HttpServletResponse response) throws TException { + HttpServletResponse response + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final Project project = projectService.getProjectForUserById(projectId, sw360User); this.attachmentService.downloadAttachmentWithContext(project, attachmentId, response, sw360User); } + @Operation( + description = "Download clearing reports as a zip.", + responses = @ApiResponse( + content = {@Content(mediaType = "application/zip")} + ), + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{projectId}/attachments/clearingReports", method = RequestMethod.GET, produces = "application/zip") public void downloadClearingReports( + @Parameter(description = "Project ID.") @PathVariable("projectId") String projectId, - HttpServletResponse response) throws TException { + HttpServletResponse response + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final Project project = projectService.getProjectForUserById(projectId, sw360User); final String filename = "Clearing-Reports-" + project.getName() + ".zip"; - final Set attachments = project.getAttachments(); final Set clearingAttachments = new HashSet<>(); for (final Attachment attachment : attachments) { @@ -963,10 +1203,17 @@ public void downloadClearingReports( } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + description = "Update a project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}", method = RequestMethod.PATCH) public ResponseEntity> patchProject( + @Parameter(description = "Project ID.") @PathVariable("id") String id, - @RequestBody Map reqBodyMap) throws TException { + @Parameter(description = "Updated values", schema = @Schema(implementation = Project.class)) + @RequestBody Map reqBodyMap + ) throws TException { User user = restControllerHelper.getSw360UserFromAuthentication(); Project sw360Project = projectService.getProjectForUserById(id, user); Project updateProject = convertToProject(reqBodyMap); @@ -979,10 +1226,19 @@ public ResponseEntity> patchProject( return new ResponseEntity<>(userHalResource, HttpStatus.OK); } + @Operation( + description = "Add attachments to a project.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{projectId}/attachments", method = RequestMethod.POST, consumes = {"multipart/mixed", "multipart/form-data"}) - public ResponseEntity addAttachmentToProject(@PathVariable("projectId") String projectId, - @RequestPart("file") MultipartFile file, - @RequestPart("attachment") Attachment newAttachment) throws TException { + public ResponseEntity addAttachmentToProject( + @Parameter(description = "Project ID.") + @PathVariable("projectId") String projectId, + @Parameter(description = "File to attach") + @RequestPart("file") MultipartFile file, + @Parameter(description = "Attachment description") + @RequestPart("attachment") Attachment newAttachment + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); final Project project = projectService.getProjectForUserById(projectId, sw360User); Attachment attachment = null; @@ -1003,32 +1259,55 @@ public ResponseEntity addAttachmentToProject(@PathVariable("project return new ResponseEntity<>(halResource, status); } + @Operation( + summary = "Get all projects corresponding to external ids.", + description = "The request parameter supports MultiValueMap (allows to add duplicate keys with different " + + "values). It's possible to search for projects only by the external id key by leaving the value.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/searchByExternalIds", method = RequestMethod.GET) - public ResponseEntity searchByExternalIds(@RequestParam MultiValueMap externalIdsMultiMap) throws TException { + public ResponseEntity searchByExternalIds( + @Parameter(description = "External ID map for filter.", + example = "{\"project-ext\": \"515432\", \"project-ext\": \"7657\", \"portal-id\": \"13319-XX3\"}" + ) + @RequestParam MultiValueMap externalIdsMultiMap + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); return restControllerHelper.searchByExternalIds(externalIdsMultiMap, projectService, sw360User); } - @RequestMapping(value = PROJECTS_URL + "/usedBy" + "/{id}", method = RequestMethod.GET) - public ResponseEntity>> getUsedByProjectDetails(@PathVariable("id") String id) throws TException{ + @Operation( + description = "Get all the projects where the project is used.", + tags = {"Projects"} + ) + @RequestMapping(value = PROJECTS_URL + "/usedBy/{id}", method = RequestMethod.GET) + public ResponseEntity>> getUsedByProjectDetails( + @Parameter(description = "Project ID to search.") + @PathVariable("id") String id + ) throws TException { User user = restControllerHelper.getSw360UserFromAuthentication(); - //Project sw360Project = projectService.getProjectForUserById(id, user); Set sw360Projects = projectService.searchLinkingProjects(id, user); List> projectResources = new ArrayList<>(); sw360Projects.forEach(p -> { - Project embeddedProject = restControllerHelper.convertToEmbeddedProject(p); - projectResources.add(EntityModel.of(embeddedProject)); - }); + Project embeddedProject = restControllerHelper.convertToEmbeddedProject(p); + projectResources.add(EntityModel.of(embeddedProject)); + }); CollectionModel> resources = restControllerHelper.createResources(projectResources); HttpStatus status = resources == null ? HttpStatus.NO_CONTENT : HttpStatus.OK; return new ResponseEntity<>(resources, status); } + @Operation( + description = "Get all attachmentUsages of the projects.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/attachmentUsage", method = RequestMethod.GET) - public @ResponseBody ResponseEntity> getAttachmentUsage(@PathVariable("id") String id) - throws TException, TTransportException { + public @ResponseBody ResponseEntity> getAttachmentUsage( + @Parameter(description = "Project ID.") + @PathVariable("id") String id + ) throws TException { List attachmentUsages = attachmentService.getAllAttachmentUsage(id); String prefix = "{\"" + SW360_ATTACHMENT_USAGES + "\":["; String serializedUsages = attachmentUsages.stream() @@ -1066,9 +1345,17 @@ public ResponseEntity>> getUsedByProjectDet } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + description = "Import SBOM in SPDX format.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/import/SBOM", method = RequestMethod.POST, consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) - public ResponseEntity importSBOM(@RequestParam(value = "type", required = true) String type, - @RequestBody MultipartFile file) throws TException { + public ResponseEntity importSBOM( + @Parameter(description = "Type of SBOM", example = "SPDX") + @RequestParam(value = "type", required = true) String type, + @Parameter(description = "SBOM file") + @RequestBody MultipartFile file + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); Attachment attachment = null; final RequestSummary requestSummary; @@ -1115,9 +1402,38 @@ public ResponseEntity importSBOM(@RequestParam(value = "type", required = tru } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + summary = "Import SBOM on a project.", + description = "Import a SBOM on a project. Currently only CycloneDX(.xml/" + + ".json) files are supported.", + responses = { + @ApiResponse( + responseCode = "200", description = "Project successfully imported.", + content = { + @Content(mediaType = "application/json", + schema = @Schema(implementation = Project.class)) + } + ), + @ApiResponse( + responseCode = "409", description = "A project with same name and version already exists.", + content = { + @Content(mediaType = "application/json", + examples = @ExampleObject( + value = "A project with same name and version already exists. " + + "The projectId is: 376576" + )) + } + ), + }, + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/import/SBOM", method = RequestMethod.POST, consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) - public ResponseEntity importSBOMonProject(@PathVariable(value = "id", required = true) String id, - @RequestBody MultipartFile file) throws TException { + public ResponseEntity importSBOMonProject( + @Parameter(description = "Project ID", example = "376576") + @PathVariable(value = "id", required = true) String id, + @Parameter(description = "SBOM file") + @RequestBody MultipartFile file + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); Attachment attachment = null; final RequestSummary requestSummary; @@ -1332,6 +1648,10 @@ public static TSerializer getJsonSerializer() { return null; } + @Operation( + description = "Get project count of a user.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/projectcount", method = RequestMethod.GET) public void getUserProjectCount(HttpServletResponse response) throws TException { User sw360User = restControllerHelper.getSw360UserFromAuthentication(); @@ -1340,13 +1660,20 @@ public void getUserProjectCount(HttpServletResponse response) throws TException resultJson.addProperty("status", "success"); resultJson.addProperty("count", projectService.getMyAccessibleProjectCounts(sw360User)); response.getWriter().write(resultJson.toString()); - }catch (IOException e) { + } catch (IOException e) { throw new SW360Exception(e.getMessage()); } } + + @Operation( + description = "Get summary and administration page of project tab.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/summaryAdministration", method = RequestMethod.GET) public ResponseEntity> getAdministration( - @PathVariable("id") String id) throws TException { + @Parameter(description = "Project ID", example = "376576") + @PathVariable("id") String id + ) throws TException { User sw360User = restControllerHelper.getSw360UserFromAuthentication(); Project sw360Project = projectService.getProjectForUserById(id, sw360User); Map sortedExternalURLs = CommonUtils.getSortedMap(sw360Project.getExternalUrls(), true); @@ -1359,5 +1686,4 @@ public ResponseEntity> getAdministration( return new ResponseEntity<>(userHalResource, HttpStatus.OK); } - } diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java index 218ea6c178..5ff19f41f4 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java @@ -246,9 +246,9 @@ public List searchProjectByType(String type, User sw360User) throws TEx return getAllRequiredProjects(projectData, sw360User); } - public Set getReleaseIds(String projectId, User sw360User, String transitive) throws TException { + public Set getReleaseIds(String projectId, User sw360User, boolean transitive) throws TException { ProjectService.Iface sw360ProjectClient = getThriftProjectClient(); - if (Boolean.parseBoolean(transitive)) { + if (transitive) { List releaseClearingStatusData = sw360ProjectClient.getReleaseClearingStatuses(projectId, sw360User); return releaseClearingStatusData.stream().map(r -> r.release.getId()).collect(Collectors.toSet()); } else { @@ -354,7 +354,8 @@ protected List createLinkedProjects(Project project, return linkedProjects.stream().map(projectLinkMapper).collect(Collectors.toList()); } - public Set getReleasesFromProjectIds(List projectIds, String transitive, final User sw360User, Sw360ReleaseService releaseService) { + public Set getReleasesFromProjectIds(List projectIds, boolean transitive, final User sw360User, + Sw360ReleaseService releaseService) { final List>> callableTasksToGetReleases = new ArrayList>>(); projectIds.stream().forEach(id -> { diff --git a/rest/resource-server/src/main/resources/application.yml b/rest/resource-server/src/main/resources/application.yml index c248bfb4bf..7857eb76f1 100644 --- a/rest/resource-server/src/main/resources/application.yml +++ b/rest/resource-server/src/main/resources/application.yml @@ -11,6 +11,8 @@ server: port: 8091 + servlet: + context-path: /resource/api management: endpoints: @@ -64,4 +66,15 @@ blacklist: sw360: rest: api: - endpoints: /resource/api/users:POST \ No newline at end of file + endpoints: /resource/api/users:POST + +springdoc: + api-docs: + enabled: true + path: /api-docs + show-oauth2-endpoints: true + swagger-ui: + enabled: true + path: /api/swagger-ui + default-consumes-media-type: application/json + default-produces-media-type: application/hal+json diff --git a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java index 32f118240b..e600429686 100644 --- a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java +++ b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java @@ -450,8 +450,8 @@ public void before() throws TException, IOException { given(this.projectServiceMock.searchProjectByType(any(), any())).willReturn(new ArrayList(projectList)); given(this.projectServiceMock.searchProjectByGroup(any(), any())).willReturn(new ArrayList(projectList)); given(this.projectServiceMock.refineSearch(any(), any())).willReturn(projectListByName); - given(this.projectServiceMock.getReleaseIds(eq(project.getId()), any(), eq("false"))).willReturn(releaseIds); - given(this.projectServiceMock.getReleaseIds(eq(project.getId()), any(), eq("true"))).willReturn(releaseIdsTransitive); + given(this.projectServiceMock.getReleaseIds(eq(project.getId()), any(), eq(false))).willReturn(releaseIds); + given(this.projectServiceMock.getReleaseIds(eq(project.getId()), any(), eq(true))).willReturn(releaseIdsTransitive); given(this.projectServiceMock.deleteProject(eq(project.getId()), any())).willReturn(RequestStatus.SUCCESS); given(this.projectServiceMock.updateProjectReleaseRelationship(any(), any(), any())).willReturn(projectReleaseRelationshipResponseBody); given(this.projectServiceMock.convertToEmbeddedWithExternalIds(eq(project))).willReturn(