diff --git a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/management/JobResource.java b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/management/JobResource.java index 0f0c0fc8458..b32943e3dcc 100644 --- a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/management/JobResource.java +++ b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/management/JobResource.java @@ -48,6 +48,7 @@ public class JobResource extends JobBaseResource { private static final String EXECUTE_ACTION = "execute"; private static final String MOVE_ACTION = "move"; private static final String MOVE_TO_HISTORY_JOB_ACTION = "moveToHistoryJob"; + private static final String RESCHEDULE_ACTION = "reschedule"; @Autowired protected CmmnRestResponseFactory restResponseFactory; @@ -228,27 +229,39 @@ public void executeJobAction(@ApiParam(name = "jobId") @PathVariable String jobI throw new FlowableObjectNotFoundException("Could not find a job with id '" + jobId + "'.", Job.class); } } - - @ApiOperation(value = "Move a single timer job", tags = { "Jobs" }, code = 204) + + @ApiOperation(value = "Execute a single job action (move or reschedule)", tags = { "Jobs" }, code = 204) @ApiResponses(value = { - @ApiResponse(code = 204, message = "Indicates the timer job was moved. Response-body is intentionally empty."), + @ApiResponse(code = 204, message = "Indicates the timer job action was executed. Response-body is intentionally empty."), @ApiResponse(code = 404, message = "Indicates the requested job was not found."), @ApiResponse(code = 500, message = "Indicates the an exception occurred while executing the job. The status-description contains additional detail about the error. The full error-stacktrace can be fetched later on if needed.") }) @PostMapping("/cmmn-management/timer-jobs/{jobId}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void executeTimerJobAction(@ApiParam(name = "jobId") @PathVariable String jobId, @RequestBody RestActionRequest actionRequest) { - if (actionRequest == null || !MOVE_ACTION.equals(actionRequest.getAction())) { - throw new FlowableIllegalArgumentException("Invalid action, only 'move' is supported."); + public void executeTimerJobAction(@ApiParam(name = "jobId") @PathVariable String jobId, @RequestBody TimerJobActionRequest actionRequest) { + if (actionRequest == null || !(MOVE_ACTION.equals(actionRequest.getAction()) || RESCHEDULE_ACTION.equals(actionRequest.getAction()))) { + throw new FlowableIllegalArgumentException("Invalid action, only 'move' or 'reschedule' are supported."); } Job job = getTimerJobById(jobId); - try { - managementService.moveTimerToExecutableJob(job.getId()); - } catch (FlowableObjectNotFoundException e) { - // Re-throw to have consistent error-messaging across REST-api - throw new FlowableObjectNotFoundException("Could not find a timer job with id '" + jobId + "'.", Job.class); + if (MOVE_ACTION.equals(actionRequest.getAction())) { + try { + managementService.moveTimerToExecutableJob(job.getId()); + } catch (FlowableObjectNotFoundException e) { + // Re-throw to have consistent error-messaging across REST-api + throw new FlowableObjectNotFoundException("Could not find a timer job with id '" + jobId + "'.", Job.class); + } + } else if (RESCHEDULE_ACTION.equals(actionRequest.getAction())) { + if (actionRequest.getDueDate() == null) { + throw new FlowableIllegalArgumentException("Invalid reschedule timer action. Reschedule timer actions must have a valid due date"); + } + try { + managementService.rescheduleTimeDateValueJob(job.getId(), actionRequest.getDueDate()); + } catch (FlowableObjectNotFoundException e) { + // Re-throw to have consistent error-messaging across REST-api + throw new FlowableObjectNotFoundException("Could not find a timer job with id '" + jobId + "'.", Job.class); + } } } diff --git a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/management/TimerJobActionRequest.java b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/management/TimerJobActionRequest.java new file mode 100644 index 00000000000..742932604bb --- /dev/null +++ b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/management/TimerJobActionRequest.java @@ -0,0 +1,28 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.cmmn.rest.service.api.management; + +import org.flowable.cmmn.rest.service.api.RestActionRequest; + +public class TimerJobActionRequest extends RestActionRequest { + + private String dueDate; + + public String getDueDate() { + return dueDate; + } + + public void setDueDate(String dueDate) { + this.dueDate = dueDate; + } +} diff --git a/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/management/JobResourceTest.java b/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/management/JobResourceTest.java index a56d9c4a609..1f854701c05 100644 --- a/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/management/JobResourceTest.java +++ b/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/management/JobResourceTest.java @@ -15,6 +15,10 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Date; import org.apache.http.HttpStatus; @@ -203,4 +207,32 @@ public void testExecuteJob() throws Exception { // Job should be executed assertThat(managementService.createJobQuery().caseInstanceId(caseInstance.getId()).singleResult()).isNull(); } + + /** + * Test rescheduling a single timer job + */ + @Test + @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/management/timerEventListenerCase.cmmn" }) + public void testRescheduleTimerJob() throws Exception { + CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("testTimerExpression").start(); + Job timerJob = managementService.createTimerJobQuery().caseInstanceId(caseInstance.getId()).singleResult(); + assertThat(timerJob).isNotNull(); + + ObjectNode requestNode = objectMapper.createObjectNode(); + requestNode.put("action", "reschedule"); + + LocalDateTime newDueDate = LocalDateTime.now().plusDays(1); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + String dueDateString = newDueDate.format(formatter); + requestNode.put("dueDate", dueDateString); + + HttpPost httpPost = new HttpPost(SERVER_URL_PREFIX + CmmnRestUrls.createRelativeResourceUrl(CmmnRestUrls.URL_TIMER_JOB, timerJob.getId())); + httpPost.setEntity(new StringEntity(requestNode.toString())); + CloseableHttpResponse response = executeRequest(httpPost, HttpStatus.SC_NO_CONTENT); + closeResponse(response); + + timerJob = managementService.createTimerJobQuery().caseInstanceId(caseInstance.getId()).singleResult(); + Date expectedDueDate = Date.from(newDueDate.atZone(ZoneId.systemDefault()).toInstant()); + assertThat(timerJob.getDuedate()).isCloseTo(expectedDueDate, 1000); + } } diff --git a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/management/JobResource.java b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/management/JobResource.java index b2a98599161..f9f0e5b2a86 100644 --- a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/management/JobResource.java +++ b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/management/JobResource.java @@ -50,6 +50,7 @@ public class JobResource extends JobBaseResource { private static final String EXECUTE_ACTION = "execute"; private static final String MOVE_ACTION = "move"; private static final String MOVE_TO_HISTORY_JOB_ACTION = "moveToHistoryJob"; + private static final String RESCHEDULE_ACTION = "reschedule"; @Autowired protected RestResponseFactory restResponseFactory; @@ -270,26 +271,38 @@ public void executeHistoryJob(@ApiParam(name = "jobId") @PathVariable String job } } - @ApiOperation(value = "Move a single timer job", tags = { "Jobs" }, code = 204) + @ApiOperation(value = "Execute a single job action (move or reschedule)", tags = { "Jobs" }, code = 204) @ApiResponses(value = { - @ApiResponse(code = 204, message = "Indicates the timer job was moved. Response-body is intentionally empty."), + @ApiResponse(code = 204, message = "Indicates the timer job action was executed. Response-body is intentionally empty."), @ApiResponse(code = 404, message = "Indicates the requested job was not found."), @ApiResponse(code = 500, message = "Indicates the an exception occurred while executing the job. The status-description contains additional detail about the error. The full error-stacktrace can be fetched later on if needed.") }) @PostMapping("/management/timer-jobs/{jobId}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void executeTimerJobAction(@ApiParam(name = "jobId") @PathVariable String jobId, @RequestBody RestActionRequest actionRequest) { - if (actionRequest == null || !MOVE_ACTION.equals(actionRequest.getAction())) { - throw new FlowableIllegalArgumentException("Invalid action, only 'move' is supported."); + public void executeTimerJobAction(@ApiParam(name = "jobId") @PathVariable String jobId, @RequestBody TimerJobActionRequest actionRequest) { + if (actionRequest == null || !(MOVE_ACTION.equals(actionRequest.getAction()) || RESCHEDULE_ACTION.equals(actionRequest.getAction()))) { + throw new FlowableIllegalArgumentException("Invalid action, only 'move' or 'reschedule' are supported."); } Job job = getTimerJobById(jobId); - try { - managementService.moveTimerToExecutableJob(job.getId()); - } catch (FlowableObjectNotFoundException e) { - // Re-throw to have consistent error-messaging across REST-api - throw new FlowableObjectNotFoundException("Could not find a timer job with id '" + jobId + "'.", Job.class); + if (MOVE_ACTION.equals(actionRequest.getAction())) { + try { + managementService.moveTimerToExecutableJob(job.getId()); + } catch (FlowableObjectNotFoundException e) { + // Re-throw to have consistent error-messaging across REST-api + throw new FlowableObjectNotFoundException("Could not find a timer job with id '" + jobId + "'.", Job.class); + } + } else if (RESCHEDULE_ACTION.equals(actionRequest.getAction())) { + if (actionRequest.getDueDate() == null) { + throw new FlowableIllegalArgumentException("Invalid reschedule timer action. Reschedule timer actions must have a valid due date."); + } + try { + managementService.rescheduleTimeDateJob(job.getId(), actionRequest.getDueDate()); + } catch (FlowableObjectNotFoundException e) { + // Re-throw to have consistent error-messaging across REST-api + throw new FlowableObjectNotFoundException("Could not find a timer job with id '" + jobId + "'.", Job.class); + } } } diff --git a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/management/TimerJobActionRequest.java b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/management/TimerJobActionRequest.java new file mode 100644 index 00000000000..16c4743a51d --- /dev/null +++ b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/management/TimerJobActionRequest.java @@ -0,0 +1,28 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.rest.service.api.management; + +import org.flowable.rest.service.api.RestActionRequest; + +public class TimerJobActionRequest extends RestActionRequest { + + private String dueDate; + + public String getDueDate() { + return dueDate; + } + + public void setDueDate(String dueDate) { + this.dueDate = dueDate; + } +} diff --git a/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/management/JobResourceTest.java b/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/management/JobResourceTest.java index e6e150defac..85fa49913e0 100644 --- a/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/management/JobResourceTest.java +++ b/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/management/JobResourceTest.java @@ -15,6 +15,9 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; @@ -24,6 +27,7 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; +import org.flowable.cmmn.api.runtime.CaseInstance; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.impl.cmd.ChangeDeploymentTenantIdCmd; @@ -471,4 +475,32 @@ public void deleteUnexistingSuspendedJob() throws Exception { CloseableHttpResponse response = executeRequest(httpDelete, HttpStatus.SC_NOT_FOUND); closeResponse(response); } + + /** + * Test rescheduling a single timer job + */ + @Test + @Deployment(resources = { "org/flowable/rest/service/api/management/JobResourceTest.testTimerProcess.bpmn20.xml" }) + public void testRescheduleTimerJob() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("timerProcess"); + Job timerJob = managementService.createTimerJobQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(timerJob).isNotNull(); + + ObjectNode requestNode = objectMapper.createObjectNode(); + requestNode.put("action", "reschedule"); + + LocalDateTime newDueDate = LocalDateTime.now().plusDays(1); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + String dueDateString = newDueDate.format(formatter); + requestNode.put("dueDate", dueDateString); + + HttpPost httpPost = new HttpPost(SERVER_URL_PREFIX + RestUrls.createRelativeResourceUrl(RestUrls.URL_TIMER_JOB, timerJob.getId())); + httpPost.setEntity(new StringEntity(requestNode.toString())); + CloseableHttpResponse response = executeRequest(httpPost, HttpStatus.SC_NO_CONTENT); + closeResponse(response); + + timerJob = managementService.createTimerJobQuery().processInstanceId(processInstance.getId()).singleResult(); + Date expectedDueDate = Date.from(newDueDate.atZone(ZoneId.systemDefault()).toInstant()); + assertThat(timerJob.getDuedate()).isCloseTo(expectedDueDate, 1000); + } }