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 239853e88d..27e49c100a 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 @@ -275,6 +275,18 @@ 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 af911c50ec..8923cb1f08 100644 --- a/rest/resource-server/src/docs/asciidoc/clearingRequests.adoc +++ b/rest/resource-server/src/docs/asciidoc/clearingRequests.adoc @@ -76,3 +76,18 @@ include::{snippets}/should_document_get_clearingrequests_by_state/curl-request.a ===== Example response include::{snippets}/should_document_get_clearingrequests_by_state/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 dcc0650a41..9059dd4335 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,20 +17,29 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; +import java.util.Map; +import java.time.format.DateTimeFormatter; 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; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import org.apache.thrift.TException; +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.ClearingRequestState; +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; @@ -41,6 +50,7 @@ import org.springframework.hateoas.server.RepresentationModelProcessor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; @@ -70,6 +80,9 @@ 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()); + + 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)) { + if (!SW360Utils.isValidDate(clearingRequest.getAgreedClearingDate(), 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); + + 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 cd8202584d..8f1bf58dfd 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 @@ -17,6 +17,7 @@ import org.apache.thrift.transport.THttpClient; import org.apache.thrift.transport.TTransportException; import org.eclipse.sw360.datahandler.thrift.ClearingRequestState; +import org.eclipse.sw360.datahandler.thrift.RequestStatus; import org.eclipse.sw360.datahandler.thrift.SW360Exception; import org.eclipse.sw360.datahandler.thrift.moderation.ModerationService; import org.eclipse.sw360.datahandler.thrift.projects.ClearingRequest; @@ -88,4 +89,19 @@ public Set getMyClearingRequests(User sw360User, ClearingReques return clearingrequests; } + 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 48b77220fe..8efa9dadc7 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 @@ -653,6 +653,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 8e521d8042..60b7cf7345 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 @@ -13,11 +13,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; 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.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.formParameters; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; 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; @@ -66,9 +68,13 @@ public class ClearingRequestSpecTest extends TestRestDocsSpecBase { @MockBean private Sw360ClearingRequestService clearingRequestServiceMock; + ClearingRequest clearingRequest; + ClearingRequest cr1; + ClearingRequest cr2; + @Before public void before() throws TException, IOException { - ClearingRequest clearingRequest = new ClearingRequest(); + clearingRequest = new ClearingRequest(); clearingRequest.setId("CR-101"); clearingRequest.setAgreedClearingDate("12-07-2020"); clearingRequest.setClearingState(ClearingRequestState.ACCEPTED); @@ -82,7 +88,7 @@ public void before() throws TException, IOException { Set clearingrequests = new HashSet<>(); Set clearingrequestsbystate = new HashSet<>(); - ClearingRequest cr1 = new ClearingRequest(); + cr1 = new ClearingRequest(); cr1.setId("CR-2"); cr1.setAgreedClearingDate("12-10-2020"); cr1.setClearingState(ClearingRequestState.ACCEPTED); @@ -93,7 +99,7 @@ public void before() throws TException, IOException { cr1.setRequestedClearingDate("10-08-2020"); cr1.setRequestingUser("test.user@sw60.org"); - ClearingRequest cr2 = new ClearingRequest(); + cr2 = new ClearingRequest(); cr2.setId("CR-3"); cr2.setAgreedClearingDate("24-10-2020"); cr2.setClearingState(ClearingRequestState.NEW); @@ -244,4 +250,43 @@ public void should_document_get_clearingrequests_by_state() throws Exception { ))); } + @Test + public void should_document_patch_clearingrequest() throws Exception { + ClearingRequest updateClearingRequest = new ClearingRequest() + .setClearingTeam("clearing.team@sw60.org") + .setClearingState(ClearingRequestState.SANITY_CHECK); + + String accessToken = TestHelper.getAccessToken(mockMvc, testUserId, testUserPassword); + + mockMvc.perform(patch("/api/clearingrequest/" + clearingRequest.getId()) + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(updateClearingRequest)) + .header("Authorization", "Bearer " + accessToken) + .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") + ))); + } }