Skip to content

Commit

Permalink
Add REST api for rescheduling a timer job (#3905)
Browse files Browse the repository at this point in the history
Co-authored-by: Christopher Welsch <christopher.welsch@flowable.com>
  • Loading branch information
WelschChristopher and Christopher Welsch committed May 31, 2024
1 parent c9a2d32 commit dd1710b
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}

0 comments on commit dd1710b

Please sign in to comment.