diff --git a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/CmmnRestUrls.java b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/CmmnRestUrls.java index bf3f8f84edf..081f5bdccc2 100644 --- a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/CmmnRestUrls.java +++ b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/CmmnRestUrls.java @@ -40,6 +40,7 @@ public final class CmmnRestUrls { public static final String SEGMENT_CASE_INSTANCE_RESOURCE = "case-instances"; public static final String SEGMENT_PLAN_ITEM_INSTANCE_RESOURCE = "plan-item-instances"; public static final String SEGMENT_VARIABLES = "variables"; + public static final String SEGMENT_VARIABLES_ASYNC = "variables-async"; public static final String SEGMENT_VARIABLE_INSTANCE_RESOURCE = "variable-instances"; public static final String SEGMENT_EVENT_SUBSCRIPTIONS = "event-subscriptions"; public static final String SEGMENT_SUBTASKS = "subtasks"; @@ -242,11 +243,21 @@ public final class CmmnRestUrls { * URL template for case instance variable collection: cmmn-runtime/case-instances/{0:processInstanceId}/variables */ public static final String[] URL_CASE_INSTANCE_VARIABLE_COLLECTION = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_CASE_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES }; + + /** + * URL template for case instance variable collection: cmmn-runtime/case-instances/{0:processInstanceId}/variables-async + */ + public static final String[] URL_CASE_INSTANCE_VARIABLE_ASYNC_COLLECTION = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_CASE_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES_ASYNC }; /** * URL template for a single case instance variable: cmmn-runtime/case-instances /{0:caseInstanceId}/variables/{1:variableName} */ public static final String[] URL_CASE_INSTANCE_VARIABLE = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_CASE_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES, "{1}" }; + + /** + * URL template for a single case instance variable: cmmn-runtime/case-instances /{0:caseInstanceId}/variables-async/{1:variableName} + */ + public static final String[] URL_CASE_INSTANCE_VARIABLE_ASYNC = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_CASE_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES_ASYNC, "{1}" }; /** * URL template for a single case instance variable data: cmmn-runtime/case-instances/{0:processInstanceId}/variables/{1:variableName}/data @@ -289,17 +300,25 @@ public final class CmmnRestUrls { */ public static final String[] URL_PLAN_ITEM_INSTANCE = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PLAN_ITEM_INSTANCE_RESOURCE, "{0}" }; - /** * URL template for plan item instance variables: cmmn-runtime/plan-item-instances/{0:planItemInstanceId}/variables */ - public static final String[] URL_PLAN_ITEM_INSTANCE_VARIABLES = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PLAN_ITEM_INSTANCE_RESOURCE, "{0}", - SEGMENT_VARIABLES }; + public static final String[] URL_PLAN_ITEM_INSTANCE_VARIABLES = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PLAN_ITEM_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES }; + + /** + * URL template for plan item instance variables: cmmn-runtime/plan-item-instances/{0:planItemInstanceId}/variables-async + */ + public static final String[] URL_PLAN_ITEM_INSTANCE_VARIABLES_ASYNC = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PLAN_ITEM_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES_ASYNC }; + /** * URL template for a single plan item instance variable: cmmn-runtime/plan-item-instances/{0:planItemInstanceId}/variables/{1:variableName} */ - public static final String[] URL_PLAN_ITEM_INSTANCE_VARIABLE = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PLAN_ITEM_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES, - "{1}" }; + public static final String[] URL_PLAN_ITEM_INSTANCE_VARIABLE = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PLAN_ITEM_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES, "{1}" }; + + /** + * URL template for a single plan item instance variable: cmmn-runtime/plan-item-instances/{0:planItemInstanceId}/variables-async/{1:variableName} + */ + public static final String[] URL_PLAN_ITEM_INSTANCE_VARIABLE_ASYNC = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PLAN_ITEM_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES_ASYNC, "{1}" }; /** * URL template for a single case instance: cmmn-runtime/plan-item-instances/{0:planItemInstanceId}/variables/{1:variableName}/data diff --git a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/BaseVariableResource.java b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/BaseVariableResource.java index 78e9664c7ac..3bf2332e792 100644 --- a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/BaseVariableResource.java +++ b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/BaseVariableResource.java @@ -24,9 +24,6 @@ import java.util.List; import java.util.Map; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - import org.apache.commons.io.IOUtils; import org.flowable.cmmn.api.runtime.CaseInstance; import org.flowable.cmmn.api.runtime.PlanItemInstance; @@ -48,6 +45,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + /** * @author Tijs Rademakers */ @@ -180,21 +180,22 @@ protected List processCaseVariables(CaseInstance caseInstance) { return result; } - protected Object createVariable(CaseInstance caseInstance, HttpServletRequest request, HttpServletResponse response) { - return createVariable(caseInstance.getId(), CmmnRestResponseFactory.VARIABLE_CASE, request, response, RestVariableScope.GLOBAL, + protected Object createVariable(CaseInstance caseInstance, boolean async, HttpServletRequest request, HttpServletResponse response) { + return createVariable(caseInstance.getId(), CmmnRestResponseFactory.VARIABLE_CASE, async, request, response, RestVariableScope.GLOBAL, createVariableInterceptor(caseInstance)); } - protected Object createVariable(PlanItemInstance planItemInstance, HttpServletRequest request, HttpServletResponse response) { - return createVariable(planItemInstance.getId(), CmmnRestResponseFactory.VARIABLE_PLAN_ITEM, request, response, RestVariableScope.LOCAL, + protected Object createVariable(PlanItemInstance planItemInstance, boolean async, HttpServletRequest request, HttpServletResponse response) { + return createVariable(planItemInstance.getId(), CmmnRestResponseFactory.VARIABLE_PLAN_ITEM, async, request, response, RestVariableScope.LOCAL, createVariableInterceptor(planItemInstance)); } - protected Object createVariable(String instanceId, int variableType, HttpServletRequest request, HttpServletResponse response, RestVariableScope scope, - VariableInterceptor variableInterceptor) { + protected Object createVariable(String instanceId, int variableType, boolean async, HttpServletRequest request, HttpServletResponse response, + RestVariableScope scope, VariableInterceptor variableInterceptor) { + Object result = null; if (request instanceof MultipartHttpServletRequest) { - result = setBinaryVariable((MultipartHttpServletRequest) request, instanceId, variableType, true, scope, variableInterceptor); + result = setBinaryVariable((MultipartHttpServletRequest) request, instanceId, variableType, true, async, scope, variableInterceptor); } else { List inputVariables = new ArrayList<>(); @@ -229,19 +230,32 @@ protected Object createVariable(String instanceId, int variableType, HttpServlet if (!variablesToSet.isEmpty()) { variableInterceptor.createVariables(variablesToSet); - Map setVariables; + Map setVariables = null; if (variableType == CmmnRestResponseFactory.VARIABLE_PLAN_ITEM || scope == RestVariableScope.LOCAL) { - runtimeService.setLocalVariables(instanceId, variablesToSet); - setVariables = runtimeService.getLocalVariables(instanceId, variablesToSet.keySet()); + if (async) { + runtimeService.setLocalVariablesAsync(instanceId, variablesToSet); + + } else { + runtimeService.setLocalVariables(instanceId, variablesToSet); + setVariables = runtimeService.getLocalVariables(instanceId, variablesToSet.keySet()); + } + } else { - runtimeService.setVariables(instanceId, variablesToSet); - setVariables = runtimeService.getVariables(instanceId, variablesToSet.keySet()); + if (async) { + runtimeService.setVariablesAsync(instanceId, variablesToSet); + + } else { + runtimeService.setVariables(instanceId, variablesToSet); + setVariables = runtimeService.getVariables(instanceId, variablesToSet.keySet()); + } } - for (RestVariable inputVariable : inputVariables) { - String variableName = inputVariable.getName(); - Object variableValue = setVariables.get(variableName); - resultVariables.add(restResponseFactory.createRestVariable(variableName, variableValue, scope, instanceId, variableType, false)); + if (!async) { + for (RestVariable inputVariable : inputVariables) { + String variableName = inputVariable.getName(); + Object variableValue = setVariables.get(variableName); + resultVariables.add(restResponseFactory.createRestVariable(variableName, variableValue, scope, instanceId, variableType, false)); + } } } } @@ -265,23 +279,29 @@ public void deleteAllVariables(CaseInstance caseInstance) { runtimeService.removeVariables(caseInstance.getId(), currentVariables); } - protected RestVariable setSimpleVariable(RestVariable restVariable, String instanceId, boolean isNew, RestVariableScope scope, int variableType, VariableInterceptor variableInterceptor) { + protected RestVariable setSimpleVariable(RestVariable restVariable, String instanceId, boolean isNew, boolean async, RestVariableScope scope, int variableType, VariableInterceptor variableInterceptor) { if (restVariable.getName() == null) { throw new FlowableIllegalArgumentException("Variable name is required"); } Object actualVariableValue = restResponseFactory.getVariableValue(restVariable); - setVariable(instanceId, restVariable.getName(), actualVariableValue, scope, isNew, variableInterceptor); + setVariable(instanceId, restVariable.getName(), actualVariableValue, scope, isNew, async, variableInterceptor); - RestVariable variable = getVariableFromRequestWithoutAccessCheck(instanceId, restVariable.getName(), variableType, false); - // We are setting the scope because the fetched variable does not have it - variable.setVariableScope(scope); + RestVariable variable = null; + + if (!async) { + variable = getVariableFromRequestWithoutAccessCheck(instanceId, restVariable.getName(), variableType, false); + + // We are setting the scope because the fetched variable does not have it + variable.setVariableScope(scope); + } + return variable; } protected RestVariable setBinaryVariable(MultipartHttpServletRequest request, String instanceId, int responseVariableType, boolean isNew, - RestVariableScope scope, VariableInterceptor variableInterceptor) { + boolean async, RestVariableScope scope, VariableInterceptor variableInterceptor) { // Validate input and set defaults if (request.getFileMap().size() == 0) { @@ -338,31 +358,39 @@ protected RestVariable setBinaryVariable(MultipartHttpServletRequest request, St if (variableType.equals(CmmnRestResponseFactory.BYTE_ARRAY_VARIABLE_TYPE)) { // Use raw bytes as variable value byte[] variableBytes = IOUtils.toByteArray(file.getInputStream()); - setVariable(instanceId, variableName, variableBytes, scope, isNew, variableInterceptor); + setVariable(instanceId, variableName, variableBytes, scope, isNew, async, variableInterceptor); } else if (isSerializableVariableAllowed) { // Try deserializing the object ObjectInputStream stream = new ObjectInputStream(file.getInputStream()); Object value = stream.readObject(); - setVariable(instanceId, variableName, value, scope, isNew, variableInterceptor); + setVariable(instanceId, variableName, value, scope, isNew, async, variableInterceptor); stream.close(); } else { throw new FlowableContentNotSupportedException("Serialized objects are not allowed"); } - RestVariable restVariable = getVariableFromRequestWithoutAccessCheck(instanceId, variableName, responseVariableType, false); - // We are setting the scope because the fetched variable does not have it - restVariable.setVariableScope(scope); + RestVariable restVariable = null; + + if (!async) { + restVariable = getVariableFromRequestWithoutAccessCheck(instanceId, variableName, responseVariableType, false); + + // We are setting the scope because the fetched variable does not have it + restVariable.setVariableScope(scope); + } + return restVariable; + } catch (IOException ioe) { throw new FlowableIllegalArgumentException("Could not process multipart content", ioe); + } catch (ClassNotFoundException ioe) { throw new FlowableContentNotSupportedException( "The provided body contains a serialized object for which the class was not found: " + ioe.getMessage()); } } - protected void setVariable(String instanceId, String name, Object value, RestVariableScope scope, boolean isNew, VariableInterceptor variableInterceptor) { + protected void setVariable(String instanceId, String name, Object value, RestVariableScope scope, boolean isNew, boolean async, VariableInterceptor variableInterceptor) { if (isNew) { variableInterceptor.createVariables(Collections.singletonMap(name, value)); } else { @@ -374,9 +402,19 @@ protected void setVariable(String instanceId, String name, Object value, RestVar if (isNew && runtimeService.hasLocalVariable(instanceId, name)) { throw new FlowableConflictException("Local variable '" + name + "' is already present on plan item instance '" + instanceId + "'."); } - runtimeService.setLocalVariable(instanceId, name, value); + + if (async) { + runtimeService.setLocalVariableAsync(instanceId, name, value); + } else { + runtimeService.setLocalVariable(instanceId, name, value); + } + } else { - runtimeService.setVariable(instanceId, name, value); + if (async) { + runtimeService.setVariableAsync(instanceId, name, value); + } else { + runtimeService.setVariable(instanceId, name, value); + } } } diff --git a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/CaseInstanceVariableCollectionResource.java b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/CaseInstanceVariableCollectionResource.java index d1ec8456eef..d3d0da42776 100644 --- a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/CaseInstanceVariableCollectionResource.java +++ b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/CaseInstanceVariableCollectionResource.java @@ -15,9 +15,6 @@ import java.util.List; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - import org.flowable.cmmn.api.runtime.CaseInstance; import org.flowable.cmmn.rest.service.api.engine.variable.RestVariable; import org.springframework.http.HttpStatus; @@ -37,6 +34,8 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * @author Tijs Rademakers @@ -85,7 +84,35 @@ public List getVariables(@ApiParam(name = "caseInstanceId") @PathV public Object createOrUpdateExecutionVariable(@ApiParam(name = "caseInstanceId") @PathVariable String caseInstanceId, HttpServletRequest request, HttpServletResponse response) { CaseInstance caseInstance = getCaseInstanceFromRequestWithoutAccessCheck(caseInstanceId); - return createVariable(caseInstance, request, response); + return createVariable(caseInstance, false, request, response); + } + + @ApiOperation(value = "Update a multiple/single (non)binary variable on a case instance asynchronously", tags = { "Case Instance Variables" }, nickname = "createOrUpdateCaseVariableAsync", + notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable or an array of RestVariable) or by passing a multipart/form-data Object.\n" + + "Nonexistent variables are created on the case-instance and existing ones are overridden without any error.\n" + + "Any number of variables can be passed into the request body array.\n" + + "Note that scope is ignored, only global variables can be set in a case instance.\n" + + "NB: Swagger V2 specification doesn't support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") + @ApiImplicitParams({ + @ApiImplicitParam(name = "body", type = "org.flowable.rest.cmmn.service.api.engine.variable.RestVariable", value = "Create a variable on a case instance", paramType = "body", example = "{\n" + + " \"name\":\"intProcVar\"\n" + + " \"type\":\"integer\"\n" + + " \"value\":123,\n" + + " }"), + @ApiImplicitParam(name = "file", dataType = "file", paramType = "form"), + @ApiImplicitParam(name = "name", dataType = "string", paramType = "form", example = "Simple content item"), + @ApiImplicitParam(name = "type", dataType = "string", paramType = "form", example = "integer"), + }) + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Indicates the job to update the variables has been created."), + @ApiResponse(code = 400, message = "Indicates the request body is incomplete or contains illegal values. The status description contains additional information about the error."), + @ApiResponse(code = 415, message = "Indicates the serializable data contains an object for which no class is present in the JVM running the Flowable engine and therefore cannot be deserialized.") + + }) + @PutMapping(value = "/cmmn-runtime/case-instances/{caseInstanceId}/variables-async", consumes = {"application/json", "multipart/form-data"}) + public void createOrUpdateExecutionVariableAsync(@ApiParam(name = "caseInstanceId") @PathVariable String caseInstanceId, HttpServletRequest request, HttpServletResponse response) { + CaseInstance caseInstance = getCaseInstanceFromRequestWithoutAccessCheck(caseInstanceId); + createVariable(caseInstance, true, request, response); } @ApiOperation(value = "Create variables or new binary variable on a case instance", tags = { "Case Instance Variables" }, nickname = "createCaseInstanceVariable", @@ -111,11 +138,38 @@ public Object createOrUpdateExecutionVariable(@ApiParam(name = "caseInstanceId") @ApiResponse(code = 409, message = "Indicates the case instance was found but already contains a variable with the given name (only thrown when POST method is used). Use the update-method instead."), }) - - @PostMapping(value = "/cmmn-runtime/case-instances/{caseInstanceId}/variables", produces = "application/json", consumes = {"application/json", "multipart/form-data", "text/plain"}) + @PostMapping(value = "/cmmn-runtime/case-instances/{caseInstanceId}/variables", consumes = {"application/json", "multipart/form-data", "text/plain"}) public Object createExecutionVariable(@ApiParam(name = "caseInstanceId") @PathVariable String caseInstanceId, HttpServletRequest request, HttpServletResponse response) { CaseInstance caseInstance = getCaseInstanceFromRequestWithoutAccessCheck(caseInstanceId); - return createVariable(caseInstance, request, response); + return createVariable(caseInstance, false, request, response); + } + + @ApiOperation(value = "Create variables or new binary variable on a case instance asynchronously", tags = { "Case Instance Variables" }, nickname = "createCaseInstanceVariableAsync", + notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable or an array of RestVariable) or by passing a multipart/form-data Object.\n" + + "Nonexistent variables are created on the case-instance and existing ones are overridden without any error.\n" + + "Any number of variables can be passed into the request body array.\n" + + "Note that scope is ignored, only global variables can be set in a case instance.\n" + + "NB: Swagger V2 specification doesn't support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") + @ApiImplicitParams({ + @ApiImplicitParam(name = "body", type = "org.flowable.rest.cmmn.service.api.engine.variable.RestVariable", value = "Create a variable on a case instance", paramType = "body", example = "{\n" + + " \"name\":\"intProcVar\"\n" + + " \"type\":\"integer\"\n" + + " \"value\":123,\n" + + " }"), + @ApiImplicitParam(name = "file", dataType = "file", paramType = "form"), + @ApiImplicitParam(name = "name", dataType = "string", paramType = "form", example = "Simple content item"), + @ApiImplicitParam(name = "type", dataType = "string", paramType = "form", example = "integer"), + }) + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Indicates the job to create the variables has been created."), + @ApiResponse(code = 400, message = "Indicates the request body is incomplete or contains illegal values. The status description contains additional information about the error."), + @ApiResponse(code = 409, message = "Indicates the case instance contains a variable with the given name (only thrown when POST method is used). Use the update-method instead."), + + }) + @PostMapping(value = "/cmmn-runtime/case-instances/{caseInstanceId}/variables-async", produces = "application/json", consumes = {"application/json", "multipart/form-data", "text/plain"}) + public void createExecutionVariableAsync(@ApiParam(name = "caseInstanceId") @PathVariable String caseInstanceId, HttpServletRequest request, HttpServletResponse response) { + CaseInstance caseInstance = getCaseInstanceFromRequestWithoutAccessCheck(caseInstanceId); + createVariable(caseInstance, true, request, response); } @ApiOperation(value = "Delete all variables", tags = { "Case Instance Variables" }, nickname = "deleteCaseVariable", code = 204) diff --git a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/CaseInstanceVariableResource.java b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/CaseInstanceVariableResource.java index 025753c98f1..addb466d2e4 100644 --- a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/CaseInstanceVariableResource.java +++ b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/CaseInstanceVariableResource.java @@ -15,8 +15,6 @@ import java.util.Collections; -import jakarta.servlet.http.HttpServletRequest; - import org.flowable.cmmn.api.runtime.CaseInstance; import org.flowable.cmmn.rest.service.api.CmmnRestResponseFactory; import org.flowable.cmmn.rest.service.api.engine.variable.RestVariable; @@ -44,6 +42,7 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; +import jakarta.servlet.http.HttpServletRequest; /** * @author Tijs Rademakers @@ -96,7 +95,7 @@ public RestVariable updateVariable(@ApiParam(name = "caseInstanceId") @PathVaria RestVariable result = null; if (request instanceof MultipartHttpServletRequest) { result = setBinaryVariable((MultipartHttpServletRequest) request, caseInstance.getId(), CmmnRestResponseFactory.VARIABLE_CASE, false, - RestVariable.RestVariableScope.GLOBAL, createVariableInterceptor(caseInstance)); + false, RestVariable.RestVariableScope.GLOBAL, createVariableInterceptor(caseInstance)); if (!result.getName().equals(variableName)) { throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); @@ -117,10 +116,59 @@ public RestVariable updateVariable(@ApiParam(name = "caseInstanceId") @PathVaria throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); } - result = setSimpleVariable(restVariable, caseInstance.getId(), false, RestVariable.RestVariableScope.GLOBAL, CmmnRestResponseFactory.VARIABLE_CASE, createVariableInterceptor(caseInstance)); + result = setSimpleVariable(restVariable, caseInstance.getId(), false, false, RestVariable.RestVariableScope.GLOBAL, CmmnRestResponseFactory.VARIABLE_CASE, createVariableInterceptor(caseInstance)); } return result; } + + @ApiOperation(value = "Update a single variable on a case instance asynchronously", tags = { "Case Instance Variables" }, nickname = "updateCaseInstanceVariableAsync", + notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable) or by passing a multipart/form-data Object.\n" + + "Nonexistent variables are created on the case instance and existing ones are overridden without any error.\n" + + "Note that scope is ignored, only global variables can be set in a case instance.\n" + + "NB: Swagger V2 specification doesn't support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") + @ApiImplicitParams({ + @ApiImplicitParam(name = "body", type = "org.flowable.rest.cmmn.service.api.engine.variable.RestVariable", value = "Create a variable on a case instance", paramType = "body", example = "{\n" + + " \"name\":\"intProcVar\"\n" + + " \"type\":\"integer\"\n" + + " \"value\":123,\n" + + " }"), + @ApiImplicitParam(name = "file", dataType = "file", paramType = "form"), + @ApiImplicitParam(name = "name", dataType = "string", paramType = "form", example = "Simple content item"), + @ApiImplicitParam(name = "type", dataType = "string", paramType = "form", example = "integer"), + }) + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Indicates the job to update the variable has been created."), + @ApiResponse(code = 404, message = "Indicates the case instance does not have a variable with the given name. Status description contains additional information about the error.") + }) + @PutMapping(value = "/cmmn-runtime/case-instances/{caseInstanceId}/variables-async/{variableName}", consumes = {"application/json", "multipart/form-data"}) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void updateVariableAsync(@ApiParam(name = "caseInstanceId") @PathVariable("caseInstanceId") String caseInstanceId, @ApiParam(name = "variableName") @PathVariable("variableName") String variableName, + HttpServletRequest request) { + + CaseInstance caseInstance = getCaseInstanceFromRequestWithoutAccessCheck(caseInstanceId); + + if (request instanceof MultipartHttpServletRequest) { + setBinaryVariable((MultipartHttpServletRequest) request, caseInstance.getId(), CmmnRestResponseFactory.VARIABLE_CASE, false, + true, RestVariable.RestVariableScope.GLOBAL, createVariableInterceptor(caseInstance)); + + } else { + RestVariable restVariable = null; + try { + restVariable = objectMapper.readValue(request.getInputStream(), RestVariable.class); + } catch (Exception e) { + throw new FlowableIllegalArgumentException("request body could not be transformed to a RestVariable instance.", e); + } + + if (restVariable == null) { + throw new FlowableException("Invalid body was supplied"); + } + if (!restVariable.getName().equals(variableName)) { + throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); + } + + setSimpleVariable(restVariable, caseInstance.getId(), false, true, RestVariable.RestVariableScope.GLOBAL, CmmnRestResponseFactory.VARIABLE_CASE, createVariableInterceptor(caseInstance)); + } + } @ApiOperation(value = "Delete a variable", tags = { "Case Instance Variables" }, nickname = "deleteCaseInstanceVariable", code = 204) @ApiResponses(value = { diff --git a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/PlanItemInstanceVariableResource.java b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/PlanItemInstanceVariableResource.java index 796e9695751..97e63230073 100644 --- a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/PlanItemInstanceVariableResource.java +++ b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/PlanItemInstanceVariableResource.java @@ -15,8 +15,6 @@ import java.util.Collections; -import jakarta.servlet.http.HttpServletRequest; - import org.flowable.cmmn.api.runtime.PlanItemInstance; import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.rest.service.api.CmmnRestResponseFactory; @@ -43,6 +41,7 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; +import jakarta.servlet.http.HttpServletRequest; /** * @author Christopher Welsch @@ -54,7 +53,6 @@ public class PlanItemInstanceVariableResource extends BaseVariableResource { @Autowired protected CmmnEngineConfiguration cmmnEngineConfiguration; - // FIXME OASv3 to solve Multiple Endpoint issue @ApiOperation(value = "Update a variable on a plan item", tags = { "Plan Item Instances" }, nickname = "updatePlanItemVariable", notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable) or by passing a multipart/form-data Object.\n" + "NB: Swagger V2 specification does not support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") @@ -86,7 +84,7 @@ public RestVariable updateVariable(@ApiParam(name = "planItemInstanceId") @PathV RestVariable result = null; if (request instanceof MultipartHttpServletRequest) { result = setBinaryVariable((MultipartHttpServletRequest) request, planItem.getId(), CmmnRestResponseFactory.VARIABLE_PLAN_ITEM, false, - RestVariable.RestVariableScope.LOCAL, createVariableInterceptor(planItem)); + false, RestVariable.RestVariableScope.LOCAL, createVariableInterceptor(planItem)); if (!result.getName().equals(variableName)) { throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); @@ -107,10 +105,61 @@ public RestVariable updateVariable(@ApiParam(name = "planItemInstanceId") @PathV throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); } - result = setSimpleVariable(restVariable, planItem.getId(), false, RestVariable.RestVariableScope.LOCAL, CmmnRestResponseFactory.VARIABLE_PLAN_ITEM, createVariableInterceptor(planItem)); + result = setSimpleVariable(restVariable, planItem.getId(), false, false, RestVariable.RestVariableScope.LOCAL, CmmnRestResponseFactory.VARIABLE_PLAN_ITEM, createVariableInterceptor(planItem)); } return result; } + + @ApiOperation(value = "Update a variable on a plan item asynchronously", tags = { "Plan Item Instances" }, nickname = "updatePlanItemVariableAsync", + notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable) or by passing a multipart/form-data Object.\n" + + "NB: Swagger V2 specification does not support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") + @ApiImplicitParams({ + @ApiImplicitParam(name = "body", type = "org.flowable.rest.service.api.engine.variable.RestVariable", value = "Update a variable on a plan item instance", paramType = "body", example = + "{\n" + + " \"name\":\"intProcVar\"\n" + + " \"type\":\"integer\"\n" + + " \"value\":123,\n" + + " \"scope\":\"global\"\n" + + " }"), + @ApiImplicitParam(name = "file", dataType = "file", paramType = "form"), + @ApiImplicitParam(name = "name", dataType = "string", paramType = "form", example = "intProcVar"), + @ApiImplicitParam(name = "type", dataType = "string", paramType = "form", example = "integer"), + @ApiImplicitParam(name = "scope", dataType = "string", paramType = "form", example = "global"), + + }) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Indicates the job to update a variable is created."), + @ApiResponse(code = 404, message = "Indicates the plan item instance does not have a variable with the given name. Status description contains additional information about the error.") + }) + @PutMapping(value = "/cmmn-runtime/plan-item-instances/{planItemInstanceId}/variables-async/{variableName}", consumes = {"application/json", "multipart/form-data" }) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void updateVariableAsync(@ApiParam(name = "planItemInstanceId") @PathVariable("planItemInstanceId") String planItemInstanceId, + @ApiParam(name = "variableName") @PathVariable("variableName") String variableName, HttpServletRequest request) { + + PlanItemInstance planItem = getPlanItemInstanceFromRequest(planItemInstanceId); + + if (request instanceof MultipartHttpServletRequest) { + setBinaryVariable((MultipartHttpServletRequest) request, planItem.getId(), CmmnRestResponseFactory.VARIABLE_PLAN_ITEM, false, + true, RestVariable.RestVariableScope.LOCAL, createVariableInterceptor(planItem)); + + } else { + RestVariable restVariable = null; + try { + restVariable = objectMapper.readValue(request.getInputStream(), RestVariable.class); + } catch (Exception e) { + throw new FlowableIllegalArgumentException("Error converting request body to RestVariable instance", e); + } + + if (restVariable == null) { + throw new FlowableException("Invalid body was supplied"); + } + if (!restVariable.getName().equals(variableName)) { + throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); + } + + setSimpleVariable(restVariable, planItem.getId(), false, true, RestVariable.RestVariableScope.LOCAL, CmmnRestResponseFactory.VARIABLE_PLAN_ITEM, createVariableInterceptor(planItem)); + } + } @ApiOperation(value = "Delete a variable for a plan item instance", tags = { "Plan Item Instances" }, nickname = "deletePlanItemVariable", code = 204) @ApiResponses(value = { diff --git a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/planitem/PlanItemInstanceVariableCollectionResource.java b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/planitem/PlanItemInstanceVariableCollectionResource.java index a8ac56187d4..d7f31a9d089 100644 --- a/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/planitem/PlanItemInstanceVariableCollectionResource.java +++ b/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/planitem/PlanItemInstanceVariableCollectionResource.java @@ -13,9 +13,6 @@ package org.flowable.cmmn.rest.service.api.runtime.planitem; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - import org.flowable.cmmn.api.runtime.PlanItemInstance; import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.rest.service.api.runtime.caze.BaseVariableResource; @@ -32,6 +29,8 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * @author Christopher Welsch @@ -43,7 +42,6 @@ public class PlanItemInstanceVariableCollectionResource extends BaseVariableReso @Autowired protected CmmnEngineConfiguration cmmnEngineConfiguration; - // FIXME OASv3 to solve Multiple Endpoint issue @ApiOperation(value = "Create a variable on a plan item", tags = { "Plan Item Instances" }, nickname = "createPlanItemInstanceVariable", notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable) or by passing a multipart/form-data Object.\n" + "NB: Swagger V2 specification does not support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") @@ -73,7 +71,37 @@ public Object createPlanItemInstanceVariable(@ApiParam(name = "planItemInstanceI HttpServletRequest request, HttpServletResponse response) { PlanItemInstance planItem = getPlanItemInstanceFromRequest(planItemInstanceId); - return createVariable(planItem, request, response); + return createVariable(planItem, false, request, response); + } + + @ApiOperation(value = "Create a variable on a plan item asynchronously", tags = { "Plan Item Instances" }, nickname = "createPlanItemInstanceVariableAsync", + notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable) or by passing a multipart/form-data Object.\n" + + "NB: Swagger V2 specification does not support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") + @ApiImplicitParams({ + @ApiImplicitParam(name = "body", type = "org.flowable.cmmn.rest.service.api.engine.variable.RestVariable", value = "Create a variable on a plan item instance", paramType = "body", example = + "{\n" + + " \"name\":\"intProcVar\"\n" + + " \"type\":\"integer\"\n" + + " \"value\":123,\n" + + " \"scope\":\"global\"\n" + + " }"), + @ApiImplicitParam(name = "file", dataType = "file", paramType = "form"), + @ApiImplicitParam(name = "name", dataType = "string", paramType = "form", example = "intProcVar"), + @ApiImplicitParam(name = "type", dataType = "string", paramType = "form", example = "integer"), + @ApiImplicitParam(name = "scope", dataType = "string", paramType = "form", example = "global"), + }) + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Indicates the job to create a variable is created."), + @ApiResponse(code = 400, message = "Indicates the request body is incomplete or contains illegal values. The status description contains additional information about the error."), + @ApiResponse(code = 404, message = "Indicates the plan item instance does not have a variable with the given name. Status description contains additional information about the error."), + @ApiResponse(code = 409, message = "Indicates the plan item instance already contains a variable with the given name. Use the update-method instead.") + }) + @PostMapping(value = "/cmmn-runtime/plan-item-instances/{planItemInstanceId}/variables-async", consumes = {"application/json", "multipart/form-data" }) + public void createPlanItemInstanceVariableAsync(@ApiParam(name = "planItemInstanceId") @PathVariable("planItemInstanceId") String planItemInstanceId, + HttpServletRequest request, HttpServletResponse response) { + + PlanItemInstance planItem = getPlanItemInstanceFromRequest(planItemInstanceId); + createVariable(planItem, true, request, response); } } diff --git a/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/CaseInstanceVariableResourceTest.java b/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/CaseInstanceVariableResourceTest.java index 5c2c2f806cb..edd4c2de97c 100644 --- a/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/CaseInstanceVariableResourceTest.java +++ b/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/CaseInstanceVariableResourceTest.java @@ -40,6 +40,7 @@ import org.flowable.cmmn.rest.service.BaseSpringRestTestCase; import org.flowable.cmmn.rest.service.HttpMultipartHelper; import org.flowable.cmmn.rest.service.api.CmmnRestUrls; +import org.flowable.job.api.Job; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -427,4 +428,37 @@ public void testUpdateBinaryCaseVariable() throws Exception { assertThat(variableValue).isInstanceOf(byte[].class); assertThat(new String((byte[]) variableValue)).isEqualTo("This is binary content"); } + + @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) + public void testUpdateCaseVariableAsync() throws Exception { + CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("oneHumanTaskCase") + .variables(Collections.singletonMap("overlappingVariable", (Object) "processValue")).start(); + runtimeService.setVariable(caseInstance.getId(), "myVar", "value"); + + // Update variable + ObjectNode requestNode = objectMapper.createObjectNode(); + requestNode.put("name", "myVar"); + requestNode.put("value", "updatedValue"); + requestNode.put("type", "string"); + + HttpPut httpPut = new HttpPut( + SERVER_URL_PREFIX + CmmnRestUrls.createRelativeResourceUrl(CmmnRestUrls.URL_CASE_INSTANCE_VARIABLE_ASYNC, caseInstance.getId(), "myVar")); + httpPut.setEntity(new StringEntity(requestNode.toString())); + CloseableHttpResponse response = executeRequest(httpPut, HttpStatus.SC_NO_CONTENT); + closeResponse(response); + + assertThat(runtimeService.getVariable(caseInstance.getId(), "myVar")).isEqualTo("value"); + + Job job = managementService.createJobQuery().caseInstanceId(caseInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + assertThat(runtimeService.getVariable(caseInstance.getId(), "myVar")).isEqualTo("updatedValue"); + + // Try updating with mismatch between URL and body variableName + requestNode.put("name", "unexistingVariable"); + httpPut.setEntity(new StringEntity(requestNode.toString())); + closeResponse(executeRequest(httpPut, HttpStatus.SC_BAD_REQUEST)); + } } diff --git a/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/CaseInstanceVariablesCollectionResourceTest.java b/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/CaseInstanceVariablesCollectionResourceTest.java index 44afa8650e2..4a5d596729a 100644 --- a/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/CaseInstanceVariablesCollectionResourceTest.java +++ b/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/CaseInstanceVariablesCollectionResourceTest.java @@ -37,6 +37,7 @@ import org.flowable.cmmn.rest.service.BaseSpringRestTestCase; import org.flowable.cmmn.rest.service.HttpMultipartHelper; import org.flowable.cmmn.rest.service.api.CmmnRestUrls; +import org.flowable.job.api.Job; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -87,7 +88,7 @@ public void testGetCaseVariables() throws Exception { * Test creating a single case variable. POST cmmn-runtime/case-instance/{caseInstanceId}/variables */ @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) - public void testCreateSingleProcessInstanceVariable() throws Exception { + public void testCreateSingleCaseInstanceVariable() throws Exception { CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("oneHumanTaskCase").start(); ArrayNode requestNode = objectMapper.createArrayNode(); @@ -121,7 +122,7 @@ public void testCreateSingleProcessInstanceVariable() throws Exception { * Test creating a single case variable using a binary stream. POST cmmn-runtime/case-instances/{caseInstanceId}/variables */ @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) - public void testCreateSingleBinaryProcessVariable() throws Exception { + public void testCreateSingleBinaryCaseVariable() throws Exception { CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("oneHumanTaskCase").start(); InputStream binaryContent = new ByteArrayInputStream("This is binary content".getBytes()); @@ -159,7 +160,7 @@ public void testCreateSingleBinaryProcessVariable() throws Exception { * Test creating a single process variable using a binary stream containing a serializable. POST runtime/process-instances/{processInstanceId}/variables */ @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) - public void testCreateSingleSerializableProcessVariable() throws Exception { + public void testCreateSingleSerializableCaseVariable() throws Exception { CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("oneHumanTaskCase").start(); TestSerializableVariable serializable = new TestSerializableVariable(); @@ -256,7 +257,7 @@ public void testCreateSingleCaseVariableEdgeCases() throws Exception { * Test creating a single case variable, testing default types when omitted. POST cmmn-runtime/case-instances/{caseInstanceId}/variables */ @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) - public void testCreateSingleProcessVariableDefaultTypes() throws Exception { + public void testCreateSingleCaseVariableDefaultTypes() throws Exception { CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("oneHumanTaskCase").start(); // String type detection @@ -304,7 +305,7 @@ public void testCreateSingleProcessVariableDefaultTypes() throws Exception { * Test creating multiple case variables in a single call. POST cmmn-runtime/case-instance/{caseInstanceId}/variables */ @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) - public void testCreateMultipleProcessVariables() throws Exception { + public void testCreateMultipleCaseVariables() throws Exception { CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("oneHumanTaskCase").start(); ArrayNode requestNode = objectMapper.createArrayNode(); @@ -420,6 +421,145 @@ public void testCreateMultipleCaseVariablesWithOverride() throws Exception { entry("stringVariable2", "another string value") ); } + + @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) + public void testCreateSingleCaseInstanceVariableAsync() throws Exception { + CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("oneHumanTaskCase").start(); + + ArrayNode requestNode = objectMapper.createArrayNode(); + ObjectNode variableNode = requestNode.addObject(); + variableNode.put("name", "myVariable"); + variableNode.put("value", "simple string value"); + variableNode.put("type", "string"); + + // Create a new local variable + HttpPost httpPost = new HttpPost( + SERVER_URL_PREFIX + CmmnRestUrls.createRelativeResourceUrl(CmmnRestUrls.URL_CASE_INSTANCE_VARIABLE_ASYNC_COLLECTION, caseInstance.getId())); + httpPost.setEntity(new StringEntity(requestNode.toString())); + CloseableHttpResponse response = executeBinaryRequest(httpPost, HttpStatus.SC_CREATED); + closeResponse(response); + + assertThat(runtimeService.hasVariable(caseInstance.getId(), "myVariable")).isFalse(); + + Job job = managementService.createJobQuery().caseInstanceId(caseInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + assertThat(runtimeService.hasVariable(caseInstance.getId(), "myVariable")).isTrue(); + assertThat(runtimeService.getVariable(caseInstance.getId(), "myVariable")).isEqualTo("simple string value"); + } + + @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) + public void testCreateSingleBinaryCaseVariableAsync() throws Exception { + CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("oneHumanTaskCase").start(); + + InputStream binaryContent = new ByteArrayInputStream("This is binary content".getBytes()); + + // Add name, type and scope + Map additionalFields = new HashMap<>(); + additionalFields.put("name", "binaryVariable"); + additionalFields.put("type", "binary"); + + HttpPost httpPost = new HttpPost( + SERVER_URL_PREFIX + CmmnRestUrls.createRelativeResourceUrl(CmmnRestUrls.URL_CASE_INSTANCE_VARIABLE_ASYNC_COLLECTION, caseInstance.getId())); + httpPost.setEntity(HttpMultipartHelper.getMultiPartEntity("value", "application/octet-stream", binaryContent, additionalFields)); + CloseableHttpResponse response = executeBinaryRequest(httpPost, HttpStatus.SC_CREATED); + closeResponse(response); + + assertThat(runtimeService.hasVariable(caseInstance.getId(), "binaryVariable")).isFalse(); + + Job job = managementService.createJobQuery().caseInstanceId(caseInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + // Check actual value of variable in engine + Object variableValue = runtimeService.getVariable(caseInstance.getId(), "binaryVariable"); + assertThat(variableValue).isInstanceOf(byte[].class); + assertThat(new String((byte[]) variableValue)).isEqualTo("This is binary content"); + } + + @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) + public void testCreateMultipleCaseVariablesAsync() throws Exception { + CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("oneHumanTaskCase").start(); + + ArrayNode requestNode = objectMapper.createArrayNode(); + + // String variable + ObjectNode stringVarNode = requestNode.addObject(); + stringVarNode.put("name", "stringVariable"); + stringVarNode.put("value", "simple string value"); + stringVarNode.put("type", "string"); + + // Integer + ObjectNode integerVarNode = requestNode.addObject(); + integerVarNode.put("name", "integerVariable"); + integerVarNode.put("value", 1234); + integerVarNode.put("type", "integer"); + + // Short + ObjectNode shortVarNode = requestNode.addObject(); + shortVarNode.put("name", "shortVariable"); + shortVarNode.put("value", 123); + shortVarNode.put("type", "short"); + + // Long + ObjectNode longVarNode = requestNode.addObject(); + longVarNode.put("name", "longVariable"); + longVarNode.put("value", 4567890L); + longVarNode.put("type", "long"); + + // Double + ObjectNode doubleVarNode = requestNode.addObject(); + doubleVarNode.put("name", "doubleVariable"); + doubleVarNode.put("value", 123.456); + doubleVarNode.put("type", "double"); + + // Boolean + ObjectNode booleanVarNode = requestNode.addObject(); + booleanVarNode.put("name", "booleanVariable"); + booleanVarNode.put("value", Boolean.TRUE); + booleanVarNode.put("type", "boolean"); + + // Date + Calendar varCal = Calendar.getInstance(); + String isoString = getISODateString(varCal.getTime()); + + ObjectNode dateVarNode = requestNode.addObject(); + dateVarNode.put("name", "dateVariable"); + dateVarNode.put("value", isoString); + dateVarNode.put("type", "date"); + + // Create local variables with a single request + HttpPost httpPost = new HttpPost( + SERVER_URL_PREFIX + CmmnRestUrls.createRelativeResourceUrl(CmmnRestUrls.URL_CASE_INSTANCE_VARIABLE_ASYNC_COLLECTION, caseInstance.getId())); + httpPost.setEntity(new StringEntity(requestNode.toString())); + CloseableHttpResponse response = executeRequest(httpPost, HttpStatus.SC_CREATED); + closeResponse(response); + + assertThat(runtimeService.hasVariable(caseInstance.getId(), "stringVariable")).isFalse(); + assertThat(runtimeService.hasVariable(caseInstance.getId(), "integerVariable")).isFalse(); + + Job job = managementService.createJobQuery().caseInstanceId(caseInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + // Check if engine has correct variables set + Map variables = runtimeService.getVariables(caseInstance.getId()); + + assertThat(variables) + .containsOnly( + entry("stringVariable", "simple string value"), + entry("integerVariable", 1234), + entry("shortVariable", (short) 123), + entry("longVariable", 4567890L), + entry("doubleVariable", 123.456), + entry("booleanVariable", Boolean.TRUE), + entry("dateVariable", longDateFormat.parse(isoString)) + ); + } /** * Test deleting all case variables. DELETE cmmn-runtime/case-instance/{caseInstanceId}/variables diff --git a/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/PlanItemInstanceVariableCollectionResourceTest.java b/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/PlanItemInstanceVariableCollectionResourceTest.java index bafffada05a..7291e912a72 100644 --- a/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/PlanItemInstanceVariableCollectionResourceTest.java +++ b/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/PlanItemInstanceVariableCollectionResourceTest.java @@ -34,6 +34,7 @@ import org.flowable.cmmn.rest.service.BaseSpringRestTestCase; import org.flowable.cmmn.rest.service.HttpMultipartHelper; import org.flowable.cmmn.rest.service.api.CmmnRestUrls; +import org.flowable.job.api.Job; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -137,5 +138,37 @@ public void testCreateBinaryPlanItemInstanceVariable() throws Exception { assertThat(variableValue).isInstanceOf(byte[].class); assertThat(new String((byte[]) variableValue)).isEqualTo("This is binary content"); } + + @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) + public void testCreatePlanItemInstanceVariableAsync() throws Exception { + CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("oneHumanTaskCase").businessKey("myBusinessKey").start(); + + List planItems = runtimeService.createPlanItemInstanceQuery().caseInstanceId(caseInstance.getId()).list(); + assertThat(planItems).hasSize(1); + PlanItemInstance planItem = planItems.get(0); + + String url = buildUrl(CmmnRestUrls.URL_PLAN_ITEM_INSTANCE_VARIABLES_ASYNC, planItem.getId()); + ArrayNode requestNode = objectMapper.createArrayNode(); + ObjectNode body = requestNode.addObject(); + + body.put("name", "testLocalVar"); + body.put("value", "newTestValue"); + body.put("type", "string"); + + HttpPost postRequest = new HttpPost(url); + postRequest.setEntity(new StringEntity(requestNode.toString())); + CloseableHttpResponse response = executeRequest(postRequest, HttpStatus.SC_CREATED); + closeResponse(response); + + assertThat(runtimeService.hasLocalVariable(planItem.getId(), "testLocalVar")).isFalse(); + + Job job = managementService.createJobQuery().caseInstanceId(caseInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + // Check resulting instance + assertThat(runtimeService.getLocalVariable(planItem.getId(), "testLocalVar")).isEqualTo("newTestValue"); + } } diff --git a/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/PlanItemVariableResourceTest.java b/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/PlanItemVariableResourceTest.java index 088469564d2..72cb7fa85a0 100644 --- a/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/PlanItemVariableResourceTest.java +++ b/modules/flowable-cmmn-rest/src/test/java/org/flowable/cmmn/rest/service/api/runtime/PlanItemVariableResourceTest.java @@ -37,6 +37,7 @@ import org.flowable.cmmn.rest.service.BaseSpringRestTestCase; import org.flowable.cmmn.rest.service.HttpMultipartHelper; import org.flowable.cmmn.rest.service.api.CmmnRestUrls; +import org.flowable.job.api.Job; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -83,7 +84,6 @@ public void testUpdatePlanItemInstanceVariable() throws Exception { // Check resulting instance assertThat(runtimeService.getLocalVariable(planItem.getId(), "testLocalVar")).isEqualTo("newTestValue"); - } @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) @@ -108,6 +108,39 @@ public void testUpdatePlanItemInstanceVariableExceptions() throws Exception { CloseableHttpResponse response = executeRequest(httpPut, HttpStatus.SC_BAD_REQUEST); closeResponse(response); } + + @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) + public void testUpdatePlanItemInstanceVariableAsync() throws Exception { + CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().caseDefinitionKey("oneHumanTaskCase").businessKey("myBusinessKey").start(); + + List planItems = runtimeService.createPlanItemInstanceQuery().caseInstanceId(caseInstance.getId()).list(); + assertThat(planItems).hasSize(1); + PlanItemInstance planItem = planItems.get(0); + + runtimeService.setLocalVariable(planItem.getId(), "testLocalVar", "testVarValue"); + + String url = buildUrl(CmmnRestUrls.URL_PLAN_ITEM_INSTANCE_VARIABLE_ASYNC, planItem.getId(), "testLocalVar"); + ObjectNode body = objectMapper.createObjectNode(); + + body.put("name", "testLocalVar"); + body.put("value", "newTestValue"); + body.put("type", "string"); + + HttpPut putRequest = new HttpPut(url); + putRequest.setEntity(new StringEntity(body.toString())); + CloseableHttpResponse response = executeRequest(putRequest, HttpStatus.SC_NO_CONTENT); + closeResponse(response); + + assertThat(runtimeService.getLocalVariable(planItem.getId(), "testLocalVar")).isEqualTo("testVarValue"); + + Job job = managementService.createJobQuery().caseInstanceId(caseInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + // Check resulting instance + assertThat(runtimeService.getLocalVariable(planItem.getId(), "testLocalVar")).isEqualTo("newTestValue"); + } @CmmnDeployment(resources = { "org/flowable/cmmn/rest/service/api/repository/oneHumanTaskCase.cmmn" }) public void testDeleteExecutionVariable() throws Exception { diff --git a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/RestUrls.java b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/RestUrls.java index 0ed63c6d7c8..6701da99909 100644 --- a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/RestUrls.java +++ b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/RestUrls.java @@ -43,6 +43,7 @@ public final class RestUrls { public static final String SEGMENT_ACTIVITY_INSTANCE_RESOURCE = "activity-instances"; public static final String SEGMENT_PROCESS_INSTANCE_RESOURCE = "process-instances"; public static final String SEGMENT_VARIABLES = "variables"; + public static final String SEGMENT_VARIABLES_ASYNC = "variables-async"; public static final String SEGMENT_VARIABLE_INSTANCE_RESOURCE = "variable-instances"; public static final String SEGMENT_EVENT_SUBSCRIPTIONS = "event-subscriptions"; public static final String SEGMENT_SUBTASKS = "subtasks"; @@ -295,6 +296,11 @@ public final class RestUrls { * URL template for a single variables for an execution: runtime/executions/{0:executionId}/variables/{1:variableName} */ public static final String[] URL_EXECUTION_VARIABLE = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_EXECUTION_RESOURCE, "{0}", SEGMENT_VARIABLES, "{1}" }; + + /** + * URL template for a single variables for an execution: runtime/executions/{0:executionId}/variables-async/{1:variableName} + */ + public static final String[] URL_EXECUTION_VARIABLE_ASYNC = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_EXECUTION_RESOURCE, "{0}", SEGMENT_VARIABLES_ASYNC, "{1}" }; /** * URL template for a single variables for an execution: runtime/executions/{0:executionId}/variables/{1:variableName}/data @@ -345,11 +351,21 @@ public final class RestUrls { * URL template for process instance variable collection: runtime/process-instances/{0:processInstanceId}/variables */ public static final String[] URL_PROCESS_INSTANCE_VARIABLE_COLLECTION = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PROCESS_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES }; + + /** + * URL template for process instance variable collection: runtime/process-instances/{0:processInstanceId}/variables-async + */ + public static final String[] URL_PROCESS_INSTANCE_VARIABLE_ASYNC_COLLECTION = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PROCESS_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES_ASYNC }; /** * URL template for a single process instance variable: runtime/process-instances /{0:processInstanceId}/variables/{1:variableName} */ public static final String[] URL_PROCESS_INSTANCE_VARIABLE = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PROCESS_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES, "{1}" }; + + /** + * URL template for a single process instance variable: runtime/process-instances /{0:processInstanceId}/variables-async/{1:variableName} + */ + public static final String[] URL_PROCESS_INSTANCE_VARIABLE_ASYNC = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PROCESS_INSTANCE_RESOURCE, "{0}", SEGMENT_VARIABLES_ASYNC, "{1}" }; /** * URL template for a single process instance variable data: runtime/process -instances/{0:processInstanceId}/variables/{1:variableName}/data diff --git a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseExecutionVariableResource.java b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseExecutionVariableResource.java index af76e9037fc..9613bd700db 100644 --- a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseExecutionVariableResource.java +++ b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseExecutionVariableResource.java @@ -20,8 +20,6 @@ import java.util.Collections; import java.util.Map; -import jakarta.servlet.http.HttpServletResponse; - import org.apache.commons.io.IOUtils; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableIllegalArgumentException; @@ -40,6 +38,8 @@ import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + /** * @author Frederik Heremans */ @@ -99,7 +99,7 @@ protected byte[] getVariableDataByteArray(Execution execution, String variableNa } } - protected RestVariable setBinaryVariable(MultipartHttpServletRequest request, Execution execution, boolean isNew) { + protected RestVariable setBinaryVariable(MultipartHttpServletRequest request, Execution execution, boolean isNew, boolean async) { // Validate input and set defaults if (request.getFileMap().size() == 0) { @@ -157,21 +157,28 @@ protected RestVariable setBinaryVariable(MultipartHttpServletRequest request, Ex if (variableType.equals(RestResponseFactory.BYTE_ARRAY_VARIABLE_TYPE)) { // Use raw bytes as variable value byte[] variableBytes = IOUtils.toByteArray(file.getInputStream()); - setVariable(execution, variableName, variableBytes, scope, isNew); + setVariable(execution, variableName, variableBytes, scope, isNew, async); } else if (isSerializableVariableAllowed) { // Try deserializing the object ObjectInputStream stream = new ObjectInputStream(file.getInputStream()); Object value = stream.readObject(); - setVariable(execution, variableName, value, scope, isNew); + setVariable(execution, variableName, value, scope, isNew, async); stream.close(); + } else { throw new FlowableContentNotSupportedException("Serialized objects are not allowed"); } - RestVariable variable = getVariableFromRequestWithoutAccessCheck(execution, variableName, scope, false); - // We are setting the scope because the fetched variable does not have it - variable.setVariableScope(scope); + RestVariable variable = null; + + if (!async) { + variable = getVariableFromRequestWithoutAccessCheck(execution, variableName, scope, false); + + // We are setting the scope because the fetched variable does not have it + variable.setVariableScope(scope); + } + return variable; } catch (IOException ioe) { @@ -182,7 +189,7 @@ protected RestVariable setBinaryVariable(MultipartHttpServletRequest request, Ex } - protected RestVariable setSimpleVariable(RestVariable restVariable, Execution execution, boolean isNew) { + protected RestVariable setSimpleVariable(RestVariable restVariable, Execution execution, boolean isNew, boolean async) { if (restVariable.getName() == null) { throw new FlowableIllegalArgumentException("Variable name is required"); } @@ -194,12 +201,17 @@ protected RestVariable setSimpleVariable(RestVariable restVariable, Execution ex } Object actualVariableValue = restResponseFactory.getVariableValue(restVariable); - setVariable(execution, restVariable.getName(), actualVariableValue, scope, isNew); + setVariable(execution, restVariable.getName(), actualVariableValue, scope, isNew, async); - return getVariableFromRequestWithoutAccessCheck(execution, restVariable.getName(), scope, false); + RestVariable newRestVariable = null; + if (!async) { + newRestVariable = getVariableFromRequestWithoutAccessCheck(execution, restVariable.getName(), scope, false); + } + + return newRestVariable; } - protected void setVariable(Execution execution, String name, Object value, RestVariableScope scope, boolean isNew) { + protected void setVariable(Execution execution, String name, Object value, RestVariableScope scope, boolean isNew, boolean async) { // Create can only be done on new variables. Existing variables should // be updated using PUT boolean hasVariable = hasVariableOnScope(execution, name, scope); @@ -220,12 +232,24 @@ protected void setVariable(Execution execution, String name, Object value, RestV } if (scope == RestVariableScope.LOCAL) { - runtimeService.setVariableLocal(execution.getId(), name, value); + if (async) { + runtimeService.setVariableLocalAsync(execution.getId(), name, value); + } else { + runtimeService.setVariableLocal(execution.getId(), name, value); + } } else { + String executionId = null; if (execution.getParentId() != null) { - runtimeService.setVariable(execution.getParentId(), name, value); + executionId = execution.getParentId(); + + } else { + executionId = execution.getId(); + } + + if (async) { + runtimeService.setVariableAsync(executionId, name, value); } else { - runtimeService.setVariable(execution.getId(), name, value); + runtimeService.setVariable(executionId, name, value); } } } diff --git a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseVariableCollectionResource.java b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseVariableCollectionResource.java index 7b1d9c77678..c857bb83084 100644 --- a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseVariableCollectionResource.java +++ b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseVariableCollectionResource.java @@ -19,9 +19,6 @@ import java.util.List; import java.util.Map; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.flowable.common.rest.exception.FlowableConflictException; import org.flowable.engine.runtime.Execution; @@ -33,6 +30,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + /** * @author Tijs Rademakers */ @@ -80,11 +80,10 @@ public void deleteAllLocalVariables(Execution execution) { runtimeService.removeVariablesLocal(execution.getId(), currentVariables); } - protected Object createExecutionVariable(Execution execution, boolean override, HttpServletRequest request, HttpServletResponse response) { - + protected Object createExecutionVariable(Execution execution, boolean override, boolean async, HttpServletRequest request, HttpServletResponse response) { Object result = null; if (request instanceof MultipartHttpServletRequest) { - result = setBinaryVariable((MultipartHttpServletRequest) request, execution, true); + result = setBinaryVariable((MultipartHttpServletRequest) request, execution, true, async); } else { List inputVariables = new ArrayList<>(); @@ -140,27 +139,41 @@ protected Object createExecutionVariable(Execution execution, boolean override, restApiInterceptor.createExecutionVariables(execution, variablesToSet, sharedScope); } - Map setVariables; + Map setVariables = null; if (sharedScope == RestVariableScope.LOCAL) { - runtimeService.setVariablesLocal(execution.getId(), variablesToSet); - setVariables = runtimeService.getVariablesLocal(execution.getId(), variablesToSet.keySet()); + + if (async) { + runtimeService.setVariablesLocalAsync(execution.getId(), variablesToSet); + + } else { + runtimeService.setVariablesLocal(execution.getId(), variablesToSet); + setVariables = runtimeService.getVariablesLocal(execution.getId(), variablesToSet.keySet()); + } + } else { if (execution.getParentId() != null) { - // Explicitly set on parent, setting non-local variables - // on execution itself will override local-variables if - // exists - runtimeService.setVariables(execution.getParentId(), variablesToSet); - setVariables = runtimeService.getVariables(execution.getParentId(), variablesToSet.keySet()); + // Explicitly set on parent, setting non-local variables on execution itself will override local-variables if exists + + if (async) { + runtimeService.setVariablesAsync(execution.getParentId(), variablesToSet); + + } else { + runtimeService.setVariables(execution.getParentId(), variablesToSet); + setVariables = runtimeService.getVariables(execution.getParentId(), variablesToSet.keySet()); + } + } else { // Standalone task, no global variables possible throw new FlowableIllegalArgumentException("Cannot set global variables on execution '" + execution.getId() + "', task is not part of process."); } } - for (RestVariable inputVariable : inputVariables) { - String variableName = inputVariable.getName(); - Object variableValue = setVariables.get(variableName); - resultVariables.add(restResponseFactory.createRestVariable(variableName, variableValue, varScope, execution.getId(), variableType, false)); + if (!async) { + for (RestVariable inputVariable : inputVariables) { + String variableName = inputVariable.getName(); + Object variableValue = setVariables.get(variableName); + resultVariables.add(restResponseFactory.createRestVariable(variableName, variableValue, varScope, execution.getId(), variableType, false)); + } } } diff --git a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ExecutionVariableCollectionResource.java b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ExecutionVariableCollectionResource.java index 892dc77df0a..60a943c460b 100644 --- a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ExecutionVariableCollectionResource.java +++ b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ExecutionVariableCollectionResource.java @@ -15,9 +15,6 @@ import java.util.List; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - import org.flowable.engine.runtime.Execution; import org.flowable.rest.service.api.RestResponseFactory; import org.flowable.rest.service.api.engine.variable.RestVariable; @@ -39,6 +36,8 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * @author Frederik Heremans @@ -63,7 +62,6 @@ public List getVariables(@ApiParam(name = "executionId") @PathVari return processVariables(execution, scope); } - // FIXME OASv3 to solve Multiple Endpoint issue @ApiOperation(value = "Update variables on an execution", tags = { "Executions" }, nickname = "createOrUpdateExecutionVariable", notes = "This endpoint can be used in 2 ways: By passing a JSON Body (array of RestVariable) or by passing a multipart/form-data Object.\n" + "Any number of variables can be passed into the request body array.\n" @@ -87,10 +85,34 @@ public List getVariables(@ApiParam(name = "executionId") @PathVari public Object createOrUpdateExecutionVariable(@ApiParam(name = "executionId") @PathVariable String executionId, HttpServletRequest request, HttpServletResponse response) { Execution execution = getExecutionFromRequestWithoutAccessCheck(executionId); - return createExecutionVariable(execution, true, request, response); + return createExecutionVariable(execution, true, false, request, response); + } + + @ApiOperation(value = "Update variables on an execution asynchronously", tags = { "Executions" }, nickname = "createOrUpdateExecutionVariableAsync", + notes = "This endpoint can be used in 2 ways: By passing a JSON Body (array of RestVariable) or by passing a multipart/form-data Object.\n" + + "Any number of variables can be passed into the request body array.\n" + + "NB: Swagger V2 specification does not support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") + @ApiImplicitParams({ + @ApiImplicitParam(name = "body", type = "org.flowable.rest.service.api.engine.variable.RestVariable", value = "Update a task variable", paramType = "body", example = "{\n" + + " \"name\":\"intProcVar\"\n" + + " \"type\":\"integer\"\n" + + " \"value\":123,\n" + + " }"), + @ApiImplicitParam(name = "name", value = "Required name of the variable", dataType = "string", paramType = "form", example = "Simple content item"), + @ApiImplicitParam(name = "type", value = "Type of variable that is updated. If omitted, reverts to raw JSON-value type (string, boolean, integer or double)",dataType = "string", paramType = "form", example = "integer"), + @ApiImplicitParam(name = "scope",value = "Scope of variable to be returned. When local, only task-local variable value is returned. When global, only variable value from the task’s parent execution-hierarchy are returned. When the parameter is omitted, a local variable will be returned if it exists, otherwise a global variable..", dataType = "string", paramType = "form", example = "local") + }) + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Indicates the job to update the variables has been created."), + @ApiResponse(code = 400, message = "Indicates the request body is incomplete or contains illegal values. The status description contains additional information about the error."), + }) + @PutMapping(value = "/runtime/executions/{executionId}/variables-async", produces = "application/json", consumes = {"application/json", "multipart/form-data"}) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void createOrUpdateExecutionVariableAsync(@ApiParam(name = "executionId") @PathVariable String executionId, HttpServletRequest request, HttpServletResponse response) { + Execution execution = getExecutionFromRequestWithoutAccessCheck(executionId); + createExecutionVariable(execution, true, true, request, response); } - // FIXME OASv3 to solve Multiple Endpoint issue @ApiOperation(value = "Create variables on an execution", tags = { "Executions" }, nickname = "createExecutionVariable", notes = "This endpoint can be used in 2 ways: By passing a JSON Body (array of RestVariable) or by passing a multipart/form-data Object.\n" + "Any number of variables can be passed into the request body array.\n" @@ -116,7 +138,34 @@ public Object createOrUpdateExecutionVariable(@ApiParam(name = "executionId") @P public Object createExecutionVariable(@ApiParam(name = "executionId") @PathVariable String executionId, HttpServletRequest request, HttpServletResponse response) { Execution execution = getExecutionFromRequestWithoutAccessCheck(executionId); - return createExecutionVariable(execution, false, request, response); + return createExecutionVariable(execution, false, false, request, response); + } + + @ApiOperation(value = "Create variables on an execution asynchronously", tags = { "Executions" }, nickname = "createExecutionVariableAsync", + notes = "This endpoint can be used in 2 ways: By passing a JSON Body (array of RestVariable) or by passing a multipart/form-data Object.\n" + + "Any number of variables can be passed into the request body array.\n" + + "NB: Swagger V2 specification does not support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") + @ApiImplicitParams({ + @ApiImplicitParam(name = "body", type = "org.flowable.rest.service.api.engine.variable.RestVariable", value = "Update a task variable", paramType = "body", example = "{\n" + + " \"name\":\"intProcVar\"\n" + + " \"type\":\"integer\"\n" + + " \"value\":123,\n" + + " }"), + @ApiImplicitParam(name = "name", value = "Required name of the variable", dataType = "string", paramType = "form", example = "Simple content item"), + @ApiImplicitParam(name = "type", value = "Type of variable that is updated. If omitted, reverts to raw JSON-value type (string, boolean, integer or double)",dataType = "string", paramType = "form", example = "integer"), + @ApiImplicitParam(name = "scope",value = "Scope of variable to be returned. When local, only task-local variable value is returned. When global, only variable value from the task’s parent execution-hierarchy are returned. When the parameter is omitted, a local variable will be returned if it exists, otherwise a global variable..", dataType = "string", paramType = "form", example = "local") + }) + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Indicates the job to create the variables has been created."), + @ApiResponse(code = 400, message = "Indicates the request body is incomplete or contains illegal values. The status description contains additional information about the error."), + @ApiResponse(code = 409, message = "Indicates the execution already contains a variable with the given name. Use the update-method instead.") + + }) + @PostMapping(value = "/runtime/executions/{executionId}/variables-async", produces = "application/json", consumes = {"application/json", "multipart/form-data"}) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void createExecutionVariableAsync(@ApiParam(name = "executionId") @PathVariable String executionId, HttpServletRequest request, HttpServletResponse response) { + Execution execution = getExecutionFromRequestWithoutAccessCheck(executionId); + createExecutionVariable(execution, false, true, request, response); } @ApiOperation(value = "Delete all variables for an execution", tags = { "Executions" }, code = 204) diff --git a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ExecutionVariableResource.java b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ExecutionVariableResource.java index 44f9f6a90c4..d9c4d0424ea 100644 --- a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ExecutionVariableResource.java +++ b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ExecutionVariableResource.java @@ -15,8 +15,6 @@ import java.util.Collections; -import jakarta.servlet.http.HttpServletRequest; - import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.flowable.common.engine.api.FlowableObjectNotFoundException; @@ -46,6 +44,7 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; +import jakarta.servlet.http.HttpServletRequest; /** * @author Frederik Heremans @@ -75,7 +74,6 @@ public RestVariable getVariable(@ApiParam(name = "executionId") @PathVariable("e return getVariableFromRequest(execution, variableName, scope, false); } - // FIXME OASv3 to solve Multiple Endpoint issue @ApiOperation(value = "Update a variable on an execution", tags = { "Executions" }, nickname = "updateExecutionVariable", notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable) or by passing a multipart/form-data Object.\n" + "NB: Swagger V2 specification does not support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") @@ -103,7 +101,7 @@ public RestVariable updateVariable(@ApiParam(name = "executionId") @PathVariable RestVariable result = null; if (request instanceof MultipartHttpServletRequest) { - result = setBinaryVariable((MultipartHttpServletRequest) request, execution, false); + result = setBinaryVariable((MultipartHttpServletRequest) request, execution, false, false); if (!result.getName().equals(variableName)) { throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); @@ -126,10 +124,58 @@ public RestVariable updateVariable(@ApiParam(name = "executionId") @PathVariable throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); } - result = setSimpleVariable(restVariable, execution, false); + result = setSimpleVariable(restVariable, execution, false, false); } return result; } + + @ApiOperation(value = "Update a variable on an execution asynchronously", tags = { "Executions" }, nickname = "updateExecutionVariableAsync", + notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable) or by passing a multipart/form-data Object.\n" + + "NB: Swagger V2 specification does not support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") + @ApiImplicitParams({ + @ApiImplicitParam(name = "body", type = "org.flowable.rest.service.api.engine.variable.RestVariable", value = "Update a variable on an execution", paramType = "body", example = "{\n" + + " \"name\":\"intProcVar\"\n" + + " \"type\":\"integer\"\n" + + " \"value\":123,\n" + + " \"scope\":\"global\"\n" + + " }"), + @ApiImplicitParam(name = "file", dataType = "file", paramType = "form"), + @ApiImplicitParam(name = "name", dataType = "string", paramType = "form", example = "intProcVar"), + @ApiImplicitParam(name = "type", dataType = "string", paramType = "form", example = "integer"), + @ApiImplicitParam(name = "scope", dataType = "string", paramType = "form", example = "global"), + + }) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Indicates both the process instance and variable were found and variable is updated."), + @ApiResponse(code = 404, message = "Indicates the process instance does not have a variable with the given name. Status description contains additional information about the error.") + }) + @PutMapping(value = "/runtime/executions/{executionId}/variables-async/{variableName}", consumes = {"application/json", "multipart/form-data"}) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void updateVariableAsync(@ApiParam(name = "executionId") @PathVariable("executionId") String executionId, @ApiParam(name = "variableName") @PathVariable("variableName") String variableName, HttpServletRequest request) { + Execution execution = getExecutionFromRequestWithoutAccessCheck(executionId); + + if (request instanceof MultipartHttpServletRequest) { + setBinaryVariable((MultipartHttpServletRequest) request, execution, false, true); + + } else { + RestVariable restVariable = null; + + try { + restVariable = objectMapper.readValue(request.getInputStream(), RestVariable.class); + } catch (Exception e) { + throw new FlowableIllegalArgumentException("Error converting request body to RestVariable instance", e); + } + + if (restVariable == null) { + throw new FlowableException("Invalid body was supplied"); + } + if (!restVariable.getName().equals(variableName)) { + throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); + } + + setSimpleVariable(restVariable, execution, false, true); + } + } @ApiOperation(value = "Delete a variable for an execution", tags = { "Executions" }, nickname = "deletedExecutionVariable", code = 204) @ApiResponses(value = { diff --git a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceVariableCollectionResource.java b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceVariableCollectionResource.java index a5c7db79d22..1f5cf5ab4c1 100644 --- a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceVariableCollectionResource.java +++ b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceVariableCollectionResource.java @@ -16,9 +16,6 @@ import java.util.List; import java.util.Map; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - import org.flowable.engine.runtime.Execution; import org.flowable.rest.service.api.RestResponseFactory; import org.flowable.rest.service.api.engine.variable.RestVariable; @@ -41,6 +38,8 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * @author Tijs Rademakers @@ -66,7 +65,6 @@ public List getVariables(@ApiParam(name = "processInstanceId") @Pa return processVariables(execution, scope); } - // FIXME OASv3 to solve Multiple Endpoint issue @ApiOperation(value = "Update a multiple/single (non)binary variable on a process instance", tags = { "Process Instance Variables" }, nickname = "createOrUpdateProcessVariable", notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable or an array of RestVariable) or by passing a multipart/form-data Object.\n" + "Nonexistent variables are created on the process-instance and existing ones are overridden without any error.\n" @@ -94,10 +92,37 @@ public List getVariables(@ApiParam(name = "processInstanceId") @Pa public Object createOrUpdateExecutionVariable(@ApiParam(name = "processInstanceId") @PathVariable String processInstanceId, HttpServletRequest request, HttpServletResponse response) { Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId); - return createExecutionVariable(execution, true, request, response); + return createExecutionVariable(execution, true, false, request, response); + } + + @ApiOperation(value = "Update multiple/single (non)binary variables on a process instance asynchronously", tags = { "Process Instance Variables" }, nickname = "createOrUpdateProcessVariableAsync", + notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable or an array of RestVariable) or by passing a multipart/form-data Object.\n" + + "Nonexistent variables are created on the process-instance and existing ones are overridden without any error.\n" + + "Any number of variables can be passed into the request body array.\n" + + "Note that scope is ignored, only local variables can be set in a process instance.\n" + + "NB: Swagger V2 specification does not support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") + @ApiImplicitParams({ + @ApiImplicitParam(name = "body", type = "org.flowable.rest.service.api.engine.variable.RestVariable", value = "Create a variable on a process instance", paramType = "body", example = "{\n" + + " \"name\":\"intProcVar\"\n" + + " \"type\":\"integer\"\n" + + " \"value\":123,\n" + + " }"), + @ApiImplicitParam(name = "file", dataType = "file", paramType = "form"), + @ApiImplicitParam(name = "name", dataType = "string", paramType = "form", example = "Simple content item"), + @ApiImplicitParam(name = "type", dataType = "string", paramType = "form", example = "integer"), + }) + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Indicates the job to create or update the variables was created."), + @ApiResponse(code = 400, message = "Indicates the request body is incomplete or contains illegal values. The status description contains additional information about the error."), + @ApiResponse(code = 415, message = "Indicates the serializable data contains an object for which no class is present in the JVM running the Flowable engine and therefore cannot be deserialized.") + + }) + @PutMapping(value = "/runtime/process-instances/{processInstanceId}/variables-async", consumes = {"application/json", "multipart/form-data"}) + public void createOrUpdateExecutionVariableAsync(@ApiParam(name = "processInstanceId") @PathVariable String processInstanceId, HttpServletRequest request, HttpServletResponse response) { + Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId); + createExecutionVariable(execution, true, true, request, response); } - // FIXME OASv3 to solve Multiple Endpoint issue @ApiOperation(value = "Create variables or new binary variable on a process instance", tags = { "Process Instance Variables" }, nickname = "createProcessInstanceVariable", notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable or an array of RestVariable) or by passing a multipart/form-data Object.\n" + "Nonexistent variables are created on the process-instance and existing ones are overridden without any error.\n" @@ -125,7 +150,35 @@ public Object createOrUpdateExecutionVariable(@ApiParam(name = "processInstanceI public Object createExecutionVariable(@ApiParam(name = "processInstanceId") @PathVariable String processInstanceId, HttpServletRequest request, HttpServletResponse response) { Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId); - return createExecutionVariable(execution, false, request, response); + return createExecutionVariable(execution, false, false, request, response); + } + + @ApiOperation(value = "Create variables or new binary variable on a process instance asynchronously", tags = { "Process Instance Variables" }, nickname = "createProcessInstanceVariableAsync", + notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable or an array of RestVariable) or by passing a multipart/form-data Object.\n" + + "Nonexistent variables are created on the process-instance and existing ones are overridden without any error.\n" + + "Any number of variables can be passed into the request body array.\n" + + "Note that scope is ignored, only local variables can be set in a process instance.\n" + + "NB: Swagger V2 specification does not support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") + @ApiImplicitParams({ + @ApiImplicitParam(name = "body", type = "org.flowable.rest.service.api.engine.variable.RestVariable", value = "Create a variable on a process instance", paramType = "body", example = "{\n" + + " \"name\":\"intProcVar\"\n" + + " \"type\":\"integer\"\n" + + " \"value\":123,\n" + + " }"), + @ApiImplicitParam(name = "file", dataType = "file", paramType = "form"), + @ApiImplicitParam(name = "name", dataType = "string", paramType = "form", example = "Simple content item"), + @ApiImplicitParam(name = "type", dataType = "string", paramType = "form", example = "integer"), + }) + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Indicates the job to create the variables was created."), + @ApiResponse(code = 400, message = "Indicates the request body is incomplete or contains illegal values. The status description contains additional information about the error."), + @ApiResponse(code = 409, message = "Indicates the process instance was found but already contains a variable with the given name (only thrown when POST method is used). Use the update-method instead."), + + }) + @PostMapping(value = "/runtime/process-instances/{processInstanceId}/variables-async", consumes = {"application/json", "multipart/form-data"}) + public void createExecutionVariableAsync(@ApiParam(name = "processInstanceId") @PathVariable String processInstanceId, HttpServletRequest request, HttpServletResponse response) { + Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId); + createExecutionVariable(execution, false, true, request, response); } // FIXME Documentation diff --git a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceVariableResource.java b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceVariableResource.java index ff71f9e8af6..67756587064 100644 --- a/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceVariableResource.java +++ b/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceVariableResource.java @@ -15,8 +15,6 @@ import java.util.Collections; -import jakarta.servlet.http.HttpServletRequest; - import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.flowable.common.engine.api.FlowableObjectNotFoundException; @@ -46,6 +44,7 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; +import jakarta.servlet.http.HttpServletRequest; /** * @author Frederik Heremans @@ -74,7 +73,6 @@ public RestVariable getVariable(@ApiParam(name = "processInstanceId") @PathVaria return getVariableFromRequest(execution, variableName, scope, false); } - // FIXME OASv3 to solve Multiple Endpoint issue @ApiOperation(value = "Update a single variable on a process instance", tags = { "Process Instance Variables" }, nickname = "updateProcessInstanceVariable", notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable) or by passing a multipart/form-data Object.\n" + "Nonexistent variables are created on the process-instance and existing ones are overridden without any error.\n" @@ -102,7 +100,7 @@ public RestVariable updateVariable(@ApiParam(name = "processInstanceId") @PathVa RestVariable result = null; if (request instanceof MultipartHttpServletRequest) { - result = setBinaryVariable((MultipartHttpServletRequest) request, execution, false); + result = setBinaryVariable((MultipartHttpServletRequest) request, execution, false, false); if (!result.getName().equals(variableName)) { throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); @@ -123,10 +121,58 @@ public RestVariable updateVariable(@ApiParam(name = "processInstanceId") @PathVa throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); } - result = setSimpleVariable(restVariable, execution, false); + result = setSimpleVariable(restVariable, execution, false, false); } return result; } + + @ApiOperation(value = "Update a single variable on a process instance asynchronously", tags = { "Process Instance Variables" }, nickname = "updateProcessInstanceVariableAsync", + notes = "This endpoint can be used in 2 ways: By passing a JSON Body (RestVariable) or by passing a multipart/form-data Object.\n" + + "Nonexistent variables are created on the process-instance and existing ones are overridden without any error.\n" + + "Note that scope is ignored, only local variables can be set in a process instance.\n" + + "NB: Swagger V2 specification does not support this use case that is why this endpoint might be buggy/incomplete if used with other tools.") + @ApiImplicitParams({ + @ApiImplicitParam(name = "body", type = "org.flowable.rest.service.api.engine.variable.RestVariable", value = "Create a variable on a process instance", paramType = "body", example = "{\n" + + " \"name\":\"intProcVar\"\n" + + " \"type\":\"integer\"\n" + + " \"value\":123,\n" + + " }"), + @ApiImplicitParam(name = "file", dataType = "file", paramType = "form"), + @ApiImplicitParam(name = "name", dataType = "string", paramType = "form", example = "Simple content item"), + @ApiImplicitParam(name = "type", dataType = "string", paramType = "form", example = "integer"), + }) + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Indicates the job to update the variable has been created."), + @ApiResponse(code = 404, message = "Indicates the process instance does not have a variable with the given name. Status description contains additional information about the error.") + }) + @PutMapping(value = "/runtime/process-instances/{processInstanceId}/variables-async/{variableName}", consumes = {"application/json", "multipart/form-data"}) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void updateVariableAsync(@ApiParam(name = "processInstanceId") @PathVariable("processInstanceId") String processInstanceId, @ApiParam(name = "variableName") @PathVariable("variableName") String variableName, + HttpServletRequest request) { + + Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId); + + if (request instanceof MultipartHttpServletRequest) { + setBinaryVariable((MultipartHttpServletRequest) request, execution, false, true); + + } else { + RestVariable restVariable = null; + try { + restVariable = objectMapper.readValue(request.getInputStream(), RestVariable.class); + } catch (Exception e) { + throw new FlowableIllegalArgumentException("request body could not be transformed to a RestVariable instance.", e); + } + + if (restVariable == null) { + throw new FlowableException("Invalid body was supplied"); + } + if (!restVariable.getName().equals(variableName)) { + throw new FlowableIllegalArgumentException("Variable name in the body should be equal to the name used in the requested URL."); + } + + setSimpleVariable(restVariable, execution, false, true); + } + } // FIXME Documentation @ApiOperation(value = "Delete a variable", tags = { "Process Instance Variables" }, nickname = "deleteProcessInstanceVariable", code = 204) diff --git a/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ExecutionVariableResourceTest.java b/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ExecutionVariableResourceTest.java index c001af0baef..94e69927d09 100644 --- a/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ExecutionVariableResourceTest.java +++ b/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ExecutionVariableResourceTest.java @@ -34,6 +34,7 @@ import org.flowable.engine.runtime.Execution; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.test.Deployment; +import org.flowable.job.api.Job; import org.flowable.rest.service.BaseSpringRestTestCase; import org.flowable.rest.service.HttpMultipartHelper; import org.flowable.rest.service.api.RestUrls; @@ -385,4 +386,74 @@ public void testUpdateBinaryExecutionVariable() throws Exception { assertThat(variableValue).isInstanceOf(byte[].class); assertThat(new String((byte[]) variableValue)).isEqualTo("This is binary content"); } + + @Test + @Deployment(resources = { "org/flowable/rest/service/api/runtime/ExecutionResourceTest.process-with-subprocess.bpmn20.xml" }) + public void testUpdateExecutionVariableAsync() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("processOne", Collections.singletonMap("overlappingVariable", (Object) "processValue")); + runtimeService.setVariable(processInstance.getId(), "myVar", "processValue"); + + Execution childExecution = runtimeService.createExecutionQuery().parentId(processInstance.getId()).singleResult(); + assertThat(childExecution).isNotNull(); + runtimeService.setVariableLocal(childExecution.getId(), "myVar", "childValue"); + + // Update variable local + ObjectNode requestNode = objectMapper.createObjectNode(); + requestNode.put("name", "myVar"); + requestNode.put("value", "updatedValue"); + requestNode.put("type", "string"); + + HttpPut httpPut = new HttpPut(SERVER_URL_PREFIX + RestUrls.createRelativeResourceUrl(RestUrls.URL_EXECUTION_VARIABLE_ASYNC, childExecution.getId(), "myVar")); + httpPut.setEntity(new StringEntity(requestNode.toString())); + CloseableHttpResponse response = executeRequest(httpPut, HttpStatus.SC_NO_CONTENT); + closeResponse(response); + + assertThat(runtimeService.getVariable(processInstance.getId(), "myVar")).isEqualTo("processValue"); + assertThat(runtimeService.getVariableLocal(childExecution.getId(), "myVar")).isEqualTo("childValue"); + + Job job = managementService.createJobQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + // Global value should be unaffected + assertThat(runtimeService.getVariable(processInstance.getId(), "myVar")).isEqualTo("processValue"); + assertThat(runtimeService.getVariableLocal(childExecution.getId(), "myVar")).isEqualTo("updatedValue"); + + // Update variable global + requestNode = objectMapper.createObjectNode(); + requestNode.put("name", "myVar"); + requestNode.put("value", "updatedValueGlobal"); + requestNode.put("type", "string"); + requestNode.put("scope", "global"); + + httpPut = new HttpPut(SERVER_URL_PREFIX + RestUrls.createRelativeResourceUrl(RestUrls.URL_EXECUTION_VARIABLE_ASYNC, childExecution.getId(), "myVar")); + httpPut.setEntity(new StringEntity(requestNode.toString())); + response = executeRequest(httpPut, HttpStatus.SC_NO_CONTENT); + closeResponse(response); + + assertThat(runtimeService.getVariable(processInstance.getId(), "myVar")).isEqualTo("processValue"); + assertThat(runtimeService.getVariableLocal(childExecution.getId(), "myVar")).isEqualTo("updatedValue"); + + job = managementService.createJobQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + // Local value should be unaffected + assertThat(runtimeService.getVariable(processInstance.getId(), "myVar")).isEqualTo("updatedValueGlobal"); + assertThat(runtimeService.getVariableLocal(childExecution.getId(), "myVar")).isEqualTo("updatedValue"); + + requestNode.put("name", "unexistingVariable"); + + httpPut.setEntity(new StringEntity(requestNode.toString())); + response = executeRequest(httpPut, HttpStatus.SC_BAD_REQUEST); + closeResponse(response); + + httpPut = new HttpPut( + SERVER_URL_PREFIX + RestUrls.createRelativeResourceUrl(RestUrls.URL_EXECUTION_VARIABLE_ASYNC, childExecution.getId(), "unexistingVariable")); + httpPut.setEntity(new StringEntity(requestNode.toString())); + response = executeRequest(httpPut, HttpStatus.SC_NOT_FOUND); + closeResponse(response); + } } diff --git a/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ProcessInstanceVariableResourceTest.java b/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ProcessInstanceVariableResourceTest.java index f4265ec0ca2..2a773a75b19 100644 --- a/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ProcessInstanceVariableResourceTest.java +++ b/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ProcessInstanceVariableResourceTest.java @@ -37,6 +37,7 @@ import org.apache.http.entity.StringEntity; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.test.Deployment; +import org.flowable.job.api.Job; import org.flowable.rest.service.BaseSpringRestTestCase; import org.flowable.rest.service.HttpMultipartHelper; import org.flowable.rest.service.api.RestUrls; @@ -456,4 +457,45 @@ public void testUpdateBinaryProcessVariable() throws Exception { assertThat(variableValue).isInstanceOf(byte[].class); assertThat(new String((byte[]) variableValue)).isEqualTo("This is binary content"); } + + @Test + @Deployment(resources = { "org/flowable/rest/service/api/runtime/ProcessInstanceVariableResourceTest.testProcess.bpmn20.xml" }) + public void testUpdateProcessVariableAsync() throws Exception { + ProcessInstance processInstance = runtimeService + .startProcessInstanceByKey("oneTaskProcess", Collections.singletonMap("overlappingVariable", (Object) "processValue")); + runtimeService.setVariable(processInstance.getId(), "myVar", "value"); + + // Update variable + ObjectNode requestNode = objectMapper.createObjectNode(); + requestNode.put("name", "myVar"); + requestNode.put("value", "updatedValue"); + requestNode.put("type", "string"); + + HttpPut httpPut = new HttpPut( + SERVER_URL_PREFIX + RestUrls.createRelativeResourceUrl(RestUrls.URL_PROCESS_INSTANCE_VARIABLE_ASYNC, processInstance.getId(), "myVar")); + httpPut.setEntity(new StringEntity(requestNode.toString())); + CloseableHttpResponse response = executeRequest(httpPut, HttpStatus.SC_NO_CONTENT); + closeResponse(response); + + assertThat(runtimeService.getVariable(processInstance.getId(), "myVar").equals("value")); + + Job job = managementService.createJobQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + assertThat(runtimeService.getVariable(processInstance.getId(), "myVar").equals("updatedValue")); + + // Try updating with mismatch between URL and body variableName + requestNode.put("name", "unexistingVariable"); + httpPut.setEntity(new StringEntity(requestNode.toString())); + closeResponse(executeRequest(httpPut, HttpStatus.SC_BAD_REQUEST)); + + // Try updating unexisting property + requestNode.put("name", "unexistingVariable"); + httpPut = new HttpPut( + SERVER_URL_PREFIX + RestUrls.createRelativeResourceUrl(RestUrls.URL_PROCESS_INSTANCE_VARIABLE_ASYNC, processInstance.getId(), "unexistingVariable")); + httpPut.setEntity(new StringEntity(requestNode.toString())); + closeResponse(executeRequest(httpPut, HttpStatus.SC_NOT_FOUND)); + } } diff --git a/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ProcessInstanceVariablesCollectionResourceTest.java b/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ProcessInstanceVariablesCollectionResourceTest.java index 7eda9212ffc..8c9775dd25d 100644 --- a/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ProcessInstanceVariablesCollectionResourceTest.java +++ b/modules/flowable-rest/src/test/java/org/flowable/rest/service/api/runtime/ProcessInstanceVariablesCollectionResourceTest.java @@ -34,6 +34,7 @@ import org.apache.http.entity.StringEntity; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.test.Deployment; +import org.flowable.job.api.Job; import org.flowable.rest.service.BaseSpringRestTestCase; import org.flowable.rest.service.HttpMultipartHelper; import org.flowable.rest.service.api.RestUrls; @@ -420,6 +421,148 @@ public void testCreateMultipleProcessVariablesWithOverride() throws Exception { entry("stringVariable2", "another string value") ); } + + @Test + @Deployment(resources = { "org/flowable/rest/service/api/runtime/ProcessInstanceVariablesCollectionResourceTest.testProcess.bpmn20.xml" }) + public void testCreateSingleProcessInstanceVariableAsync() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneTaskProcess"); + + ArrayNode requestNode = objectMapper.createArrayNode(); + ObjectNode variableNode = requestNode.addObject(); + variableNode.put("name", "myVariable"); + variableNode.put("value", "simple string value"); + variableNode.put("type", "string"); + + // Create a new local variable + HttpPost httpPost = new HttpPost( + SERVER_URL_PREFIX + RestUrls.createRelativeResourceUrl(RestUrls.URL_PROCESS_INSTANCE_VARIABLE_ASYNC_COLLECTION, processInstance.getId())); + httpPost.setEntity(new StringEntity(requestNode.toString())); + CloseableHttpResponse response = executeRequest(httpPost, HttpStatus.SC_CREATED); + closeResponse(response); + + assertThat(runtimeService.hasVariable(processInstance.getId(), "myVariable")).isFalse(); + + Job job = managementService.createJobQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + assertThat(runtimeService.hasVariable(processInstance.getId(), "myVariable")).isTrue(); + assertThat(runtimeService.getVariableLocal(processInstance.getId(), "myVariable")).isEqualTo("simple string value"); + } + + @Test + @Deployment(resources = { "org/flowable/rest/service/api/runtime/ProcessInstanceVariablesCollectionResourceTest.testProcess.bpmn20.xml" }) + public void testCreateSingleBinaryProcessVariableAsync() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneTaskProcess"); + + InputStream binaryContent = new ByteArrayInputStream("This is binary content".getBytes()); + + // Add name, type and scope + Map additionalFields = new HashMap<>(); + additionalFields.put("name", "binaryVariable"); + additionalFields.put("type", "binary"); + + HttpPost httpPost = new HttpPost( + SERVER_URL_PREFIX + RestUrls.createRelativeResourceUrl(RestUrls.URL_PROCESS_INSTANCE_VARIABLE_ASYNC_COLLECTION, processInstance.getId())); + httpPost.setEntity(HttpMultipartHelper.getMultiPartEntity("value", "application/octet-stream", binaryContent, additionalFields)); + CloseableHttpResponse response = executeBinaryRequest(httpPost, HttpStatus.SC_CREATED); + closeResponse(response); + + assertThat(runtimeService.hasVariable(processInstance.getId(), "binaryVariable")).isFalse(); + + Job job = managementService.createJobQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + // Check actual value of variable in engine + Object variableValue = runtimeService.getVariable(processInstance.getId(), "binaryVariable"); + assertThat(variableValue).isInstanceOf(byte[].class); + assertThat(new String((byte[]) variableValue)).isEqualTo("This is binary content"); + } + + @Test + @Deployment(resources = { "org/flowable/rest/service/api/runtime/ProcessInstanceVariablesCollectionResourceTest.testProcess.bpmn20.xml" }) + public void testCreateMultipleProcessVariablesAsync() throws Exception { + + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneTaskProcess"); + + ArrayNode requestNode = objectMapper.createArrayNode(); + + // String variable + ObjectNode stringVarNode = requestNode.addObject(); + stringVarNode.put("name", "stringVariable"); + stringVarNode.put("value", "simple string value"); + stringVarNode.put("type", "string"); + + // Integer + ObjectNode integerVarNode = requestNode.addObject(); + integerVarNode.put("name", "integerVariable"); + integerVarNode.put("value", 1234); + integerVarNode.put("type", "integer"); + + // Short + ObjectNode shortVarNode = requestNode.addObject(); + shortVarNode.put("name", "shortVariable"); + shortVarNode.put("value", 123); + shortVarNode.put("type", "short"); + + // Long + ObjectNode longVarNode = requestNode.addObject(); + longVarNode.put("name", "longVariable"); + longVarNode.put("value", 4567890L); + longVarNode.put("type", "long"); + + // Double + ObjectNode doubleVarNode = requestNode.addObject(); + doubleVarNode.put("name", "doubleVariable"); + doubleVarNode.put("value", 123.456); + doubleVarNode.put("type", "double"); + + // Boolean + ObjectNode booleanVarNode = requestNode.addObject(); + booleanVarNode.put("name", "booleanVariable"); + booleanVarNode.put("value", Boolean.TRUE); + booleanVarNode.put("type", "boolean"); + + // Date + Calendar varCal = Calendar.getInstance(); + String isoString = getISODateString(varCal.getTime()); + + ObjectNode dateVarNode = requestNode.addObject(); + dateVarNode.put("name", "dateVariable"); + dateVarNode.put("value", isoString); + dateVarNode.put("type", "date"); + + // Create local variables with a single request + HttpPost httpPost = new HttpPost( + SERVER_URL_PREFIX + RestUrls.createRelativeResourceUrl(RestUrls.URL_PROCESS_INSTANCE_VARIABLE_ASYNC_COLLECTION, processInstance.getId())); + httpPost.setEntity(new StringEntity(requestNode.toString())); + CloseableHttpResponse response = executeRequest(httpPost, HttpStatus.SC_CREATED); + closeResponse(response); + + assertThat(runtimeService.hasVariable(processInstance.getId(), "stringVariable")).isFalse(); + assertThat(runtimeService.hasVariable(processInstance.getId(), "integerVariable")).isFalse(); + + Job job = managementService.createJobQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + managementService.executeJob(job.getId()); + + // Check if engine has correct variables set + Map variables = runtimeService.getVariables(processInstance.getId()); + assertThat(variables) + .containsOnly( + entry("stringVariable", "simple string value"), + entry("integerVariable", 1234), + entry("shortVariable", (short) 123), + entry("longVariable", 4567890L), + entry("doubleVariable", 123.456), + entry("booleanVariable", true), + entry("dateVariable", dateFormat.parse(isoString)) + ); + } /** * Test deleting all process variables. DELETE runtime/process-instance/{processInstanceId}/variables