From 75e3bc89918fbb97f1c6c9a389c39af25ff2b175 Mon Sep 17 00:00:00 2001 From: "sameed.ahmad" Date: Tue, 4 Jun 2024 10:32:14 +0530 Subject: [PATCH] feat(rest): Add endpoint to handle updation of clearing requests. Signed-off-by: sameed.ahmad --- .../sw360/datahandler/common/SW360Utils.java | 11 ++ .../src/docs/asciidoc/clearingRequests.adoc | 13 +++ .../ClearingRequestController.java | 105 ++++++++++++++++++ .../Sw360ClearingRequestService.java | 11 ++ .../core/RestControllerHelper.java | 10 ++ .../restdocs/ClearingRequestSpecTest.java | 52 ++++++++- 6 files changed, 201 insertions(+), 1 deletion(-) diff --git a/libraries/datahandler/src/main/java/org/eclipse/sw360/datahandler/common/SW360Utils.java b/libraries/datahandler/src/main/java/org/eclipse/sw360/datahandler/common/SW360Utils.java index 291385696d..29c8c44abe 100644 --- a/libraries/datahandler/src/main/java/org/eclipse/sw360/datahandler/common/SW360Utils.java +++ b/libraries/datahandler/src/main/java/org/eclipse/sw360/datahandler/common/SW360Utils.java @@ -277,6 +277,17 @@ public static boolean isValidDate(String date, DateTimeFormatter format, Long gr } } + public static boolean isValidDate(String currRequestedClearingDate, String newRequestedClearingDate, DateTimeFormatter format) { + try { + LocalDate currLocalDate = LocalDate.parse(currRequestedClearingDate, format); + LocalDate requestedLocalDate = LocalDate.parse(newRequestedClearingDate, format); + + return requestedLocalDate.isAfter(currLocalDate); + } catch (DateTimeParseException e) { + return false; + } + } + public static String printFullname(Release release) { if (release == null || isNullOrEmpty(release.getName())) { return "New Release"; diff --git a/rest/resource-server/src/docs/asciidoc/clearingRequests.adoc b/rest/resource-server/src/docs/asciidoc/clearingRequests.adoc index eb93a4f57c..dfac638cea 100644 --- a/rest/resource-server/src/docs/asciidoc/clearingRequests.adoc +++ b/rest/resource-server/src/docs/asciidoc/clearingRequests.adoc @@ -115,4 +115,17 @@ include::{snippets}/should_document_add_comment_to_clearingrequest/response-fiel include::{snippets}/should_document_add_comment_to_clearingrequest/http-response.adoc[] +[[resources-clearingRequest-update]] +==== Update a clearingRequest + +A `PATCH` request is used to update an existing clearingRequest + +===== Response structure +include::{snippets}/should_document_patch_clearingrequest/response-fields.adoc[] + +===== Example request +include::{snippets}/should_document_patch_clearingrequest/curl-request.adoc[] + +===== Example response +include::{snippets}/should_document_patch_clearingrequest/http-response.adoc[] diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/clearingrequest/ClearingRequestController.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/clearingrequest/ClearingRequestController.java index 2f7f9084ae..99c5ca3862 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/clearingrequest/ClearingRequestController.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/clearingrequest/ClearingRequestController.java @@ -17,6 +17,8 @@ import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; +import java.util.Map; +import java.time.format.DateTimeFormatter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -24,6 +26,8 @@ import jakarta.servlet.http.HttpServletRequest; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; @@ -35,11 +39,16 @@ import org.eclipse.sw360.datahandler.resourcelists.PaginationResult; import org.eclipse.sw360.datahandler.thrift.ClearingRequestState; import org.eclipse.sw360.datahandler.thrift.Comment; +import org.eclipse.sw360.datahandler.common.CommonUtils; +import org.eclipse.sw360.datahandler.common.SW360Utils; +import org.eclipse.sw360.datahandler.permissions.PermissionUtils; +import org.eclipse.sw360.datahandler.thrift.RequestStatus; import org.eclipse.sw360.datahandler.thrift.projects.ClearingRequest; import org.eclipse.sw360.datahandler.thrift.projects.Project; import org.eclipse.sw360.datahandler.thrift.users.User; import org.eclipse.sw360.rest.resourceserver.core.HalResource; import org.eclipse.sw360.rest.resourceserver.core.RestControllerHelper; +import org.eclipse.sw360.rest.resourceserver.moderationrequest.Sw360ModerationRequestService; import org.eclipse.sw360.rest.resourceserver.project.Sw360ProjectService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; @@ -84,6 +93,8 @@ public class ClearingRequestController implements RepresentationModelProcessor patchClearingRequest( + @Parameter(description = "id of the clearing request") + @PathVariable("id") String id, + @Parameter(description = "The updated fields of clearing request.", + schema = @Schema(implementation = ClearingRequest.class)) + @RequestBody Map reqBodyMap, + HttpServletRequest request + ) throws TException { + + try{ + User sw360User = restControllerHelper.getSw360UserFromAuthentication(); + + ClearingRequest clearingRequest = sw360ClearingRequestService.getClearingRequestById(id, sw360User); + String projectId = clearingRequest.getProjectId(); + + ClearingRequest updatedClearingRequest = convertToClearingRequest(reqBodyMap); + updatedClearingRequest.setId(clearingRequest.getId()); + updatedClearingRequest.setProjectId(clearingRequest.getProjectId()); + updatedClearingRequest.setTimestamp(clearingRequest.getTimestamp()); + updatedClearingRequest.setProjectBU(clearingRequest.getProjectBU()); + updatedClearingRequest.setComments(clearingRequest.getComments()); + updatedClearingRequest.setModifiedOn(System.currentTimeMillis()); + + if(CommonUtils.isNotNullEmptyOrWhitespace(updatedClearingRequest.getRequestingUser()) && PermissionUtils.isAdmin(sw360User)){ + User updatedRequestingUser = restControllerHelper.getUserByEmailOrNull(updatedClearingRequest.getRequestingUser()); + if (updatedRequestingUser == null) { + return new ResponseEntity("Requesting user is not a valid", HttpStatus.BAD_REQUEST); + }else{ + updatedClearingRequest.setRequestingUser(updatedRequestingUser.getEmail()); + } + } + + if (CommonUtils.isNotNullEmptyOrWhitespace(updatedClearingRequest.getRequestedClearingDate())) { + if (!clearingRequest.getRequestingUser().equals(sw360User.getEmail())) { + return new ResponseEntity("Requested Clearing Date can only be updated by the requesting user", HttpStatus.FORBIDDEN); + } + if (!SW360Utils.isValidDate(clearingRequest.getRequestedClearingDate(), updatedClearingRequest.getRequestedClearingDate(), DateTimeFormatter.ISO_LOCAL_DATE)) { + return new ResponseEntity("Invalid clearing date requested", HttpStatus.BAD_REQUEST); + } + } + + if ((updatedClearingRequest.getClearingType() != null || updatedClearingRequest.getPriority() != null ) && + !(PermissionUtils.isClearingAdmin(sw360User) || PermissionUtils.isAdmin(sw360User))) { + return new ResponseEntity("Update not allowed for field ClearingType, Priority with user role", HttpStatus.FORBIDDEN); + } + + if (updatedClearingRequest.getClearingTeam() != null) { + User updatedClearingTeam = restControllerHelper.getUserByEmailOrNull(updatedClearingRequest.getClearingTeam()); + if (updatedClearingTeam == null) { + return new ResponseEntity("ClearingTeam is not a valid user", HttpStatus.BAD_REQUEST); + } + } + + if (updatedClearingRequest.getAgreedClearingDate() != null) { + if (PermissionUtils.isClearingAdmin(sw360User) || PermissionUtils.isAdmin(sw360User)) { + String currentAgreedClearingDate = CommonUtils.isNotNullEmptyOrWhitespace(clearingRequest.getAgreedClearingDate()) ? clearingRequest.getAgreedClearingDate() : "1980-01-01"; + if (!SW360Utils.isValidDate(currentAgreedClearingDate, updatedClearingRequest.getAgreedClearingDate(), DateTimeFormatter.ISO_LOCAL_DATE)) { + return new ResponseEntity("Invalid agreed clearing date requested", HttpStatus.BAD_REQUEST); + } + } else { + return new ResponseEntity("Update not allowed for field Agreed Clearing Date with user role", HttpStatus.FORBIDDEN); + } + } + + clearingRequest = this.restControllerHelper.updateClearingRequest(clearingRequest, updatedClearingRequest); + + String baseURL = restControllerHelper.getBaseUrl(request); + RequestStatus updateCRStatus = sw360ClearingRequestService.updateClearingRequest(clearingRequest, sw360User, baseURL, projectId); + HalResource halClearingRequest = createHalClearingRequestWithAllDetails(clearingRequest, sw360User, true); + + if (updateCRStatus == RequestStatus.ACCESS_DENIED) { + return new ResponseEntity("Edit action is not allowed for this user role", HttpStatus.FORBIDDEN); + } + + return new ResponseEntity<>(halClearingRequest, HttpStatus.OK); + }catch (Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + private ClearingRequest convertToClearingRequest(Map requestBody){ + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.registerModule(sw360Module); + return mapper.convertValue(requestBody, ClearingRequest.class); + } } diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/clearingrequest/Sw360ClearingRequestService.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/clearingrequest/Sw360ClearingRequestService.java index ce743420f9..7dceef0549 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/clearingrequest/Sw360ClearingRequestService.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/clearingrequest/Sw360ClearingRequestService.java @@ -109,6 +109,17 @@ public ClearingRequest addCommentToClearingRequest(String crId, Comment comment, return getClearingRequestById(crId, sw360User); } + public RequestStatus updateClearingRequest(ClearingRequest clearingRequest, User sw360User, String baseUrl, String projectId) throws TException { + ModerationService.Iface sw360ModerationClient = getThriftModerationClient(); + String projectUrl = baseUrl + "/projects/-/project/detail/" + projectId; + RequestStatus requestStatus; + requestStatus = sw360ModerationClient.updateClearingRequest(clearingRequest, sw360User, projectUrl); + + if (requestStatus == RequestStatus.FAILURE) { + throw new RuntimeException("Clearing Request with id '" + clearingRequest.getId() + " cannot be updated."); + } + return requestStatus; + } } diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/RestControllerHelper.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/RestControllerHelper.java index d3aa27e22e..ef2422d445 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/RestControllerHelper.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/core/RestControllerHelper.java @@ -659,6 +659,16 @@ public Package updatePackage(Package packageToUpdate, Package requestBodyPackage return packageToUpdate; } + public ClearingRequest updateClearingRequest(ClearingRequest crToUpdate, ClearingRequest requestBodyCR) { + for(ClearingRequest._Fields field: ClearingRequest._Fields.values()) { + Object fieldValue = requestBodyCR.getFieldValue(field); + if (fieldValue != null) { + crToUpdate.setFieldValue(field, fieldValue); + } + } + return crToUpdate; + } + public User updateUserProfile(User userToUpdate, Map requestBodyUser, ImmutableSet setOfUserProfileFields) { for (User._Fields field : setOfUserProfileFields) { Object fieldValue = requestBodyUser.get(field.getFieldName()); diff --git a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ClearingRequestSpecTest.java b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ClearingRequestSpecTest.java index 139075d30b..2b29066ad9 100644 --- a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ClearingRequestSpecTest.java +++ b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ClearingRequestSpecTest.java @@ -15,9 +15,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.IOException; @@ -56,6 +60,7 @@ public class ClearingRequestSpecTest extends TestRestDocsSpecBase { @MockBean private Sw360ClearingRequestService clearingRequestServiceMock; + ClearingRequest clearingRequest = new ClearingRequest(); ClearingRequest cr1 = new ClearingRequest(); ClearingRequest cr2 = new ClearingRequest(); @@ -63,6 +68,7 @@ public class ClearingRequestSpecTest extends TestRestDocsSpecBase { @Before public void before() throws TException, IOException { + clearingRequest.setId("CR-101"); clearingRequest.setAgreedClearingDate("12-07-2020"); clearingRequest.setClearingState(ClearingRequestState.ACCEPTED); @@ -399,4 +405,48 @@ public void should_add_comment_to_clearing_request() throws Exception { ) )); } + + @Test + public void should_document_patch_clearingrequest () throws Exception { + ClearingRequest updateClearingRequest = new ClearingRequest() + .setClearingTeam("clearing.team@sw60.org") + .setClearingState(ClearingRequestState.SANITY_CHECK); + + mockMvc.perform(patch("/api/clearingrequest/" + clearingRequest.getId()) + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(updateClearingRequest)) + .header("Authorization", TestHelper.generateAuthHeader(testUserId, testUserPassword)) + .accept(MediaTypes.HAL_JSON)) + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( + requestFields( + fieldWithPath("clearingTeam").description("The clearing team email id."), + fieldWithPath("clearingState").description("The clearing state of request") + ), + responseFields( + fieldWithPath("id").description("The id of the clearing request"), + fieldWithPath("agreedClearingDate").description("The agreed clearing date of the request, on / before which CR should be cleared"), + fieldWithPath("clearingState").description("The clearing state of the request. Possible values are: " + Arrays.asList(ClearingRequestState.values())), + fieldWithPath("clearingTeam").description("The clearing team email id."), + fieldWithPath("projectBU").description("The Business Unit / Group of the Project, for which the clearing request is created"), + fieldWithPath("projectId").description("The id of the Project, for which the clearing request is created"), + fieldWithPath("requestedClearingDate").description("The requested clearing date of releases"), + fieldWithPath("requestingUser").description("The user who created the clearing request"), + fieldWithPath("requestingUserComment").description("The comment from the requesting user"), + fieldWithPath("priority").description("The priority of the clearing request. Possible values are: " + Arrays.asList(ClearingRequestPriority.values())), + subsectionWithPath("comments").description("The clearing request comments"), + subsectionWithPath("comments[].text").description("The clearing request comment text"), + subsectionWithPath("comments[].commentedBy").description("The user who added the comment on the clearing request"), + subsectionWithPath("_embedded.sw360:project").description("The Project associated with the ClearingRequest"), + subsectionWithPath("_embedded.clearingTeam").description("Clearing team user detail"), + subsectionWithPath("_embedded.requestingUser").description("Requesting user detail"), + subsectionWithPath("_links").description("Links to other resources"), + fieldWithPath("clearingType").description("The type of clearing, e.g., DEEP"), + fieldWithPath("_embedded.totalRelease").description("Total number of releases"), + fieldWithPath("_embedded.openRelease").description("Number of open releases"), + fieldWithPath("_embedded.lastUpdatedOn").description("Last updated date for the clearing request"), + fieldWithPath("_embedded.createdOn").description("Creation date for the clearing request") + + ))); + } }