diff --git a/pom.xml b/pom.xml index 702c746b09..76f7b17e60 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,7 @@ 17.0.${patchlevel} 0-SNAPSHOT + 1.0.0 11 @@ -150,6 +151,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 +932,4 @@ - \ No newline at end of file + diff --git a/rest/resource-server/pom.xml b/rest/resource-server/pom.xml index bcb8fe7add..8cf29e24ea 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} + @@ -316,6 +336,29 @@ + + com.internetitem + write-properties-file-maven-plugin + 1.0.1 + + + one + compile + + write-properties-file + + + restInfo.properties + + + sw360RestVersion + ${rest.version} + + + + + + @@ -331,4 +374,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..e5ddb55d8e 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,8 @@ import org.springframework.web.filter.ForwardedHeaderFilter; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import java.util.*; + @SpringBootApplication @Import(Sw360CORSFilter.class) public class Sw360ResourceServer extends SpringBootServletInitializer { @@ -44,6 +53,8 @@ public class Sw360ResourceServer extends SpringBootServletInitializer { private int defaultPageSize; private static final String SW360_PROPERTIES_FILE_PATH = "/sw360.properties"; + private static final String VERSION_INFO_PROPERTIES_FILE = "/restInfo.properties"; + private static final String VERSION_INFO_KEY = "sw360RestVersion"; private static final String CURIE_NAMESPACE = "sw360"; private static final String APPLICATION_ID = "rest"; @@ -60,6 +71,9 @@ 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"; + private static final Map versionInfo; static { Properties props = CommonUtils.loadProperties(Sw360ResourceServer.class, SW360_PROPERTIES_FILE_PATH); @@ -76,6 +90,17 @@ 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"); + + versionInfo = new HashMap<>(); + Properties properties = CommonUtils.loadProperties(Sw360ResourceServer.class, VERSION_INFO_PROPERTIES_FILE, false); + versionInfo.putAll(properties); + + SpringDocUtils.getConfig() + .replaceWithClass(org.springframework.data.domain.Pageable.class, + OpenAPIPaginationHelper.class) + .replaceWithClass(org.springframework.data.domain.PageRequest.class, + OpenAPIPaginationHelper.class); } @Bean @@ -119,4 +144,32 @@ 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."); + Object restVersion = versionInfo.get(VERSION_INFO_KEY); + String restVersionString = "1.0.0"; + if (restVersion != null) { + restVersionString = restVersion.toString(); + } + 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")) + .version(restVersionString)) + .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 4097ab21ee..9a2f5520e9 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 @@ -50,6 +50,7 @@ import org.eclipse.sw360.rest.resourceserver.moderationrequest.ModerationPatch; import org.eclipse.sw360.rest.resourceserver.project.EmbeddedProject; import org.eclipse.sw360.rest.resourceserver.project.EmbeddedProjectDTO; +import org.springdoc.core.SpringDocUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -112,6 +113,53 @@ public Sw360Module() { setMixInAnnotation(ProjectDTO.class, Sw360Module.ProjectDTOMixin.class); setMixInAnnotation(EmbeddedProjectDTO.class, Sw360Module.EmbeddedProjectDTOMixin.class); setMixInAnnotation(ReleaseNode.class, Sw360Module.ReleaseNodeMixin.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(ComponentDTO.class, Sw360Module.ComponentDTOMixin.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) + .replaceWithClass(ProjectDTO.class, Sw360Module.ProjectDTOMixin.class) + .replaceWithClass(EmbeddedProjectDTO.class, Sw360Module.EmbeddedProjectDTOMixin.class) + .replaceWithClass(ReleaseNode.class, Sw360Module.ReleaseNodeMixin.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 79cc88fc8f..57b91446fb 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 @@ -22,7 +22,13 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.Gson; +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; @@ -61,12 +67,12 @@ 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; import org.eclipse.sw360.datahandler.thrift.projects.ProjectLink; import org.eclipse.sw360.datahandler.thrift.projects.ProjectProjectRelationship; -import org.eclipse.sw360.datahandler.thrift.projects.ProjectService; import org.eclipse.sw360.datahandler.thrift.projects.ProjectDTO; import org.eclipse.sw360.datahandler.thrift.users.User; import org.eclipse.sw360.datahandler.thrift.vendors.Vendor; @@ -102,8 +108,6 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.FileCopyUtils; import org.springframework.util.MultiValueMap; -import org.springframework.hateoas.Link; -import org.eclipse.sw360.rest.resourceserver.release.ReleaseController; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -111,6 +115,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; @@ -127,7 +132,6 @@ import java.util.HashSet; import java.util.InvalidPropertiesFormatException; import java.util.List; -import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; @@ -147,6 +151,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"; @@ -204,14 +210,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(); @@ -313,19 +330,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(); @@ -354,9 +386,18 @@ public ResponseEntity>> getProjectsFiltered mapOfProjects, true, sw360Projects, false); } + @Operation( + description = "Get all releases of license clearing.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/{id}/licenseClearing", method = RequestMethod.GET) - public ResponseEntity licenseClearing(@PathVariable("id") String id, @RequestParam(value = "transitive", required = true) String transitive, HttpServletRequest request) - throws URISyntaxException, TException { + public ResponseEntity licenseClearing( + @Parameter(description = "Project ID", example = "376576") + @PathVariable("id") String id, + @Parameter(description = "Get the transitive releases.") + @RequestParam(value = "transitive", required = true) boolean transitive, + HttpServletRequest request + ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); Project sw360Project = projectService.getProjectForUserById(id, sw360User); @@ -377,8 +418,13 @@ public ResponseEntity licenseClearing(@PathVariable("id") String id, @RequestPar return new ResponseEntity<>(userHalResource, HttpStatus.OK); } + @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); @@ -386,9 +432,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(); @@ -430,25 +481,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) { @@ -480,9 +543,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"); @@ -512,10 +583,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) { @@ -525,9 +610,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); @@ -536,9 +636,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); @@ -547,9 +662,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); @@ -557,16 +687,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); @@ -580,7 +717,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); } @@ -598,17 +735,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); @@ -622,7 +769,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); } @@ -638,10 +785,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); @@ -660,14 +814,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); @@ -719,9 +888,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); @@ -729,7 +906,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"); } @@ -762,10 +939,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(); @@ -829,8 +1016,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<>(); @@ -856,13 +1050,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); @@ -936,7 +1147,7 @@ public void downloadLicenseInfo(@PathVariable("id") String id, } private Set getExcludedLicenses(Set excludedLicenseIds, - List licenseInfoParsingResult) { + List licenseInfoParsingResult) { Predicate filteredLicense = licenseNameWithText -> excludedLicenseIds .contains(licenseNameWithText.getLicenseName()); @@ -946,9 +1157,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()); @@ -956,10 +1173,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(); @@ -972,25 +1197,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) { @@ -1009,10 +1254,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); @@ -1030,10 +1282,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; @@ -1054,32 +1315,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() @@ -1117,9 +1401,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; @@ -1166,9 +1458,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; @@ -1202,9 +1523,34 @@ public ResponseEntity importSBOMonProject(@PathVariable(value = "id", require return new ResponseEntity>(halResource, HttpStatus.OK); } + @Operation( + summary = "Get a single project with dependencies network.", + responses = { + @ApiResponse( + responseCode = "200", description = "Project successfully imported.", + content = { + @Content(mediaType = "application/json", + schema = @Schema(implementation = ProjectDTO.class)) + } + ), + @ApiResponse( + responseCode = "500", description = "Project release relationship is not enabled.", + content = { + @Content(mediaType = "application/json", + examples = @ExampleObject( + value = "Please enable flexible project release relationship " + + "configuration to use this function (enable.flexible.project.release.relationship = true)" + )) + } + ), + }, + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/network/{id}", method = RequestMethod.GET) public ResponseEntity getProjectWithNetwork( - @PathVariable("id") String id) throws TException { + @Parameter(description = "Project ID", example = "376576") + @PathVariable("id") String id + ) throws TException { if (!SW360Constants.ENABLE_FLEXIBLE_PROJECT_RELEASE_RELATIONSHIP) { return new ResponseEntity<>(SW360Constants.PLEASE_ENABLE_FLEXIBLE_PROJECT_RELEASE_RELATIONSHIP, HttpStatus.INTERNAL_SERVER_ERROR); } @@ -1216,8 +1562,16 @@ public ResponseEntity getProjectWithNetwork( } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + summary = "Create a project with dependencies network.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL+ "/network", method = RequestMethod.POST) - public ResponseEntity createProjectWithNetwork(@RequestBody Map reqBodyMap) throws TException { + public ResponseEntity createProjectWithNetwork( + @Parameter(description = "Project with `dependencyNetwork` set.", + schema = @Schema(implementation = Project.class)) + @RequestBody Map reqBodyMap + ) throws TException { if (!SW360Constants.ENABLE_FLEXIBLE_PROJECT_RELEASE_RELATIONSHIP) { return new ResponseEntity<>(SW360Constants.PLEASE_ENABLE_FLEXIBLE_PROJECT_RELEASE_RELATIONSHIP, HttpStatus.INTERNAL_SERVER_ERROR); } @@ -1245,10 +1599,18 @@ public ResponseEntity createProjectWithNetwork(@RequestBody Map } @PreAuthorize("hasAuthority('WRITE')") + @Operation( + summary = "Update a project with dependencies network.", + tags = {"Projects"} + ) @RequestMapping(value = PROJECTS_URL + "/network/{id}", method = RequestMethod.PATCH) public ResponseEntity patchProjectWithNetwork( + @Parameter(description = "Project ID", example = "376576") @PathVariable("id") String id, - @RequestBody Map reqBodyMap) throws TException { + @Parameter(description = "Project with `dependencyNetwork` set.", + schema = @Schema(implementation = Project.class)) + @RequestBody Map reqBodyMap + ) throws TException { if (!SW360Constants.ENABLE_FLEXIBLE_PROJECT_RELEASE_RELATIONSHIP) { return new ResponseEntity<>(SW360Constants.PLEASE_ENABLE_FLEXIBLE_PROJECT_RELEASE_RELATIONSHIP, HttpStatus.INTERNAL_SERVER_ERROR); } @@ -1475,6 +1837,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(); @@ -1483,13 +1849,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); 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 17c79ab819..e235e41bc4 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 @@ -247,9 +247,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 { @@ -355,7 +355,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 3dc2214f20..203333d716 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 @@ -452,8 +452,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( @@ -674,7 +674,7 @@ public void before() throws TException, IOException { given(this.vulnerabilityMockService.fillVulnerabilityMetadata(any(), any())).willReturn(vulIdToRelIdToRatings); given(this.vulnerabilityMockService.updateProjectVulnerabilityRating(any(), any())).willReturn(RequestStatus.SUCCESS); - given(this.projectServiceMock.getReleasesFromProjectIds(any(), any(), any(), any())).willReturn(Set.of(rel)); + given(this.projectServiceMock.getReleasesFromProjectIds(any(), anyBoolean(), any(), any())).willReturn(Set.of(rel)); } @Test @@ -1977,7 +1977,7 @@ public void should_document_create_summary_administration() throws Exception { subsectionWithPath("_links").description("<> to other resources")) )); } - + @Test public void should_document_get_project_report() throws Exception{ String accessToken = TestHelper.getAccessToken(mockMvc, testUserId, testUserPassword); diff --git a/rest/resource-server/src/test/resources/application.properties b/rest/resource-server/src/test/resources/application.properties index 34448edeb4..3a3ed9de27 100644 --- a/rest/resource-server/src/test/resources/application.properties +++ b/rest/resource-server/src/test/resources/application.properties @@ -12,4 +12,5 @@ spring.profiles.active=SECURITY_MOCK management.endpoints.web.base-path=/ management.endpoint.health.show-details=always management.endpoint.info.enabled=true -management.endpoints.web.exposure.include=health,info \ No newline at end of file +management.endpoints.web.exposure.include=health,info +server.servlet.context-path=/