diff --git a/libraries/datahandler/src/main/thrift/components.thrift b/libraries/datahandler/src/main/thrift/components.thrift index ef977fb905..d0f8537384 100644 --- a/libraries/datahandler/src/main/thrift/components.thrift +++ b/libraries/datahandler/src/main/thrift/components.thrift @@ -407,6 +407,9 @@ struct ComponentDTO { 51: optional string mailinglist, 52: optional string wiki, 53: optional string blog, + + // Moderation comment passed during PATCH request (not persisted on Component) + 90: optional string comment, } struct ReleaseLink{ diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/component/ComponentController.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/component/ComponentController.java index ce8cf9256c..8ff1ff4d5d 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/component/ComponentController.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/component/ComponentController.java @@ -80,6 +80,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @@ -392,29 +393,73 @@ public ResponseEntity>> searchByExternalI public ResponseEntity> patchComponent( @Parameter(description = "The id of the component to be updated.") @PathVariable("id") String id, - @Parameter(description = "The component with updated fields.") - @RequestBody ComponentDTO updateComponentDto, - @Parameter(description = "Comment message.") - @RequestParam(value = "comment", required = false) String comment + @Parameter(description = "Updated component fields. Add 'comment' field in body for moderation request.") + @RequestBody ComponentDTO updateComponentDto ) throws TException { + if (isNullOrEmpty(id)) { + throw new BadRequestClientException("Component ID cannot be null or empty"); + } + + if (updateComponentDto == null) { + throw new BadRequestClientException("Component data cannot be null"); + } + User user = restControllerHelper.getSw360UserFromAuthentication(); Component sw360Component = componentService.getComponentForUserById(id, user); + + String comment = extractModerationComment(updateComponentDto); + validateModerationRequest(sw360Component, user, comment); + user.setCommentMadeDuringModerationRequest(comment); - if (!restControllerHelper.isWriteActionAllowed(sw360Component, user) - && (comment == null || comment.isBlank())) { - throw new BadRequestClientException(RESPONSE_BODY_FOR_MODERATION_REQUEST_WITH_COMMIT.toString()); + + processAttachments(updateComponentDto, user); + + sw360Component = restControllerHelper.updateComponent(sw360Component, updateComponentDto); + RequestStatus updateComponentStatus = componentService.updateComponent(sw360Component, user); + + handleUpdateStatus(updateComponentStatus); + + HalResource halResource = createHalComponent(sw360Component, user); + return new ResponseEntity<>(halResource, HttpStatus.OK); + } + + private String extractModerationComment(ComponentDTO updateComponentDto) { + try { + String comment = updateComponentDto.getComment(); + return (comment != null && !comment.isBlank()) ? comment.trim() : null; + } catch (Exception e) { + log.debug("Comment field not available in ComponentDTO", e); + return null; } - if (updateComponentDto.getAttachments() != null) { - updateComponentDto.getAttachments().forEach(attachment -> wrapSW360Exception( - () -> this.attachmentService.fillCheckedAttachmentData(attachment, user))); + } + + private void validateModerationRequest(Component component, User user, String comment) { + if (!restControllerHelper.isWriteActionAllowed(component, user) && + (comment == null || comment.isBlank())) { + throw new BadRequestClientException(RESPONSE_BODY_FOR_MODERATION_REQUEST_WITH_COMMIT.get("message")); } - sw360Component = this.restControllerHelper.updateComponent(sw360Component, updateComponentDto); - RequestStatus updateComponentStatus = componentService.updateComponent(sw360Component, user); - HalResource userHalResource = createHalComponent(sw360Component, user); - if (updateComponentStatus == RequestStatus.SENT_TO_MODERATOR) { - return new ResponseEntity(RESPONSE_BODY_FOR_MODERATION_REQUEST, HttpStatus.ACCEPTED); + } + + private void processAttachments(ComponentDTO updateComponentDto, User user) { + if (updateComponentDto.getAttachments() != null && !updateComponentDto.getAttachments().isEmpty()) { + updateComponentDto.getAttachments().forEach(attachment -> + wrapSW360Exception(() -> attachmentService.fillCheckedAttachmentData(attachment, user)) + ); + } + } + + private void handleUpdateStatus(RequestStatus status) { + switch (status) { + case SENT_TO_MODERATOR: + throw new ResponseStatusException(HttpStatus.ACCEPTED, RESPONSE_BODY_FOR_MODERATION_REQUEST.get("message")); + case SUCCESS: + return; + case ACCESS_DENIED: + throw new AccessDeniedException("Access denied for component update"); + case FAILURE: + default: + throw new RuntimeException("Component update failed with status: " + status); } - return new ResponseEntity<>(userHalResource, HttpStatus.OK); } @PreAuthorize("hasAuthority('WRITE')") 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 9b8e282246..43440076ec 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 @@ -652,6 +652,7 @@ static abstract class ComponentMixin extends Component { "licenseIdsSize", "licenseIdsIterator", "createdBy", + "comment", "setId", "setRevision", "setType", @@ -672,6 +673,7 @@ static abstract class ComponentMixin extends Component { "setVcs", "setPackageManager", "setRelease", + "setComment", "createdByIsSet", "createdOnIsSet", "versionIsSet", @@ -693,6 +695,7 @@ static abstract class ComponentMixin extends Component { "homepageUrlIsSet", "hashIsSet", "packageManagerIsSet", + "commentIsSet", "typeIsSet" }) static abstract class PackageMixin extends Package { diff --git a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ComponentSpecTest.java b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ComponentSpecTest.java index 5d96e03033..4fc34d6a2f 100644 --- a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ComponentSpecTest.java +++ b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ComponentSpecTest.java @@ -331,6 +331,7 @@ public void before() throws TException, IOException { ) ); given(this.componentServiceMock.deleteComponent(eq(angularComponent.getId()), any())).willReturn(RequestStatus.SUCCESS); + given(this.componentServiceMock.updateComponent(any(), any())).willReturn(RequestStatus.SUCCESS); given(this.componentServiceMock.searchByExternalIds(eq(externalIds), any())).willReturn((new HashSet<>(componentList))); given(this.componentServiceMock.convertToEmbeddedWithExternalIds(eq(angularComponent))).willReturn( new Component("Angular")