Skip to content

Commit 0016106

Browse files
committed
Handle binary content as base64 encoded string.
Introduce 'saveResponseVariableAsBase64' on the HttpServiceTask
1 parent 0b61ed9 commit 0016106

12 files changed

+433
-2
lines changed

modules/flowable-http-common/src/main/java/org/flowable/http/common/api/HttpRequest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ public class HttpRequest {
3838
protected boolean saveResponse;
3939
protected boolean saveResponseTransient;
4040
protected boolean saveResponseAsJson;
41+
42+
protected Boolean saveResponseVariableAsBase64;
43+
4144
protected String prefix;
4245

4346
public String getMethod() {
@@ -174,6 +177,14 @@ public void setSaveResponseAsJson(boolean saveResponseAsJson) {
174177
this.saveResponseAsJson = saveResponseAsJson;
175178
}
176179

180+
public Boolean getSaveResponseVariableAsBase64() {
181+
return saveResponseVariableAsBase64;
182+
}
183+
184+
public void setSaveResponseVariableAsBase64(Boolean saveResponseAsBinary) {
185+
this.saveResponseVariableAsBase64 = saveResponseAsBinary;
186+
}
187+
177188
public String getPrefix() {
178189
return prefix;
179190
}

modules/flowable-http-common/src/main/java/org/flowable/http/common/impl/BaseHttpActivityDelegate.java

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
package org.flowable.http.common.impl;
1414

1515
import java.io.IOException;
16+
import java.util.Arrays;
17+
import java.util.Base64;
18+
import java.util.HashSet;
19+
import java.util.List;
1620
import java.util.Set;
1721
import java.util.concurrent.CompletableFuture;
1822

@@ -37,6 +41,17 @@
3741
*/
3842
public abstract class BaseHttpActivityDelegate {
3943

44+
protected static Set<String> DEFAULT_BINARY_CONTENT_TYPES = new HashSet<>(
45+
Arrays.asList("application/octet-stream", "application/pdf", "text/csv", "image/gif", "image/jpeg", "image/png", "image/svg+xml", "image/tiff",
46+
"audio/mpeg", "audio/wav",
47+
"video/mpeg", "video/mp4", "text/plain", "application/zip", "application/rtf", "application/msword",
48+
"application/vnd.ms-excel",
49+
"application/vnd.ms-powerpoint",
50+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
51+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
52+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
53+
"application/vnd.visio"));
54+
4055
// Validation constants
4156
public static final String HTTP_TASK_REQUEST_METHOD_REQUIRED = "requestMethod is required";
4257
public static final String HTTP_TASK_REQUEST_METHOD_INVALID = "requestMethod is invalid";
@@ -74,6 +89,8 @@ public abstract class BaseHttpActivityDelegate {
7489
protected Expression saveResponseParametersTransient;
7590
// Flag to save the response variable as an ObjectNode instead of a String
7691
protected Expression saveResponseVariableAsJson;
92+
93+
protected Expression saveResponseVariableAsBase64;
7794
// Prefix for the execution variable names (Optional)
7895
protected Expression resultVariablePrefix;
7996

@@ -103,7 +120,15 @@ protected HttpRequest createRequest(VariableContainer variableContainer, String
103120
request.setSaveRequest(ExpressionUtils.getBooleanFromField(saveRequestVariables, variableContainer));
104121
request.setSaveResponse(ExpressionUtils.getBooleanFromField(saveResponseParameters, variableContainer));
105122
request.setSaveResponseTransient(ExpressionUtils.getBooleanFromField(saveResponseParametersTransient, variableContainer));
106-
request.setSaveResponseAsJson(ExpressionUtils.getBooleanFromField(saveResponseVariableAsJson, variableContainer));
123+
boolean saveResponseAsJson = ExpressionUtils.getBooleanFromField(saveResponseVariableAsJson, variableContainer);
124+
request.setSaveResponseAsJson(saveResponseAsJson);
125+
if (saveResponseVariableAsBase64 != null) {
126+
boolean saveResponseAsBase64 = ExpressionUtils.getBooleanFromField(saveResponseVariableAsBase64, variableContainer);
127+
if (saveResponseAsBase64 && saveResponseAsJson) {
128+
throw new FlowableIllegalArgumentException("Cannot set both 'saveResponseVariableAsJson' and 'saveResponseVariableAsBase64' to true");
129+
}
130+
request.setSaveResponseVariableAsBase64(saveResponseAsBase64);
131+
}
107132
request.setPrefix(ExpressionUtils.getStringFromField(resultVariablePrefix, variableContainer));
108133

109134
String failCodes = ExpressionUtils.getStringFromField(failStatusCodes, variableContainer);
@@ -160,7 +185,19 @@ protected void saveResponseFields(VariableContainer variableContainer, HttpReque
160185
if (!response.isBodyResponseHandled()) {
161186
String responseVariableName = ExpressionUtils.getStringFromField(this.responseVariableName, variableContainer);
162187
String varName = StringUtils.isNotEmpty(responseVariableName) ? responseVariableName : request.getPrefix() + "ResponseBody";
163-
Object varValue = request.isSaveResponseAsJson() && response.getBody() != null ? objectMapper.readTree(response.getBody()) : response.getBody();
188+
Boolean encodeAsBase64 = request.getSaveResponseVariableAsBase64();
189+
if (encodeAsBase64 != null && encodeAsBase64 || (encodeAsBase64 == null && isBinaryResponse(response))) {
190+
byte[] bodyBytes = response.getBodyBytes();
191+
String base64Encoded = Base64.getEncoder().encodeToString(bodyBytes);
192+
response.setBody(base64Encoded);
193+
}
194+
195+
Object varValue;
196+
if (request.isSaveResponseAsJson()) {
197+
varValue = objectMapper.readTree(response.getBody());
198+
} else {
199+
varValue = response.getBody();
200+
}
164201
if (varValue instanceof MissingNode) {
165202
varValue = null;
166203
}
@@ -202,6 +239,18 @@ protected void saveResponseFields(VariableContainer variableContainer, HttpReque
202239
}
203240
}
204241

242+
protected boolean isBinaryResponse(HttpResponse response) {
243+
List<String> contentTypeHeader = response.getHttpHeaders().get("Content-Type");
244+
if (contentTypeHeader != null && !contentTypeHeader.isEmpty()) {
245+
String contentType = contentTypeHeader.get(0);
246+
if (contentType.indexOf(';') >= 0) {
247+
contentType = contentType.split(";")[0]; // remove charset if present
248+
}
249+
return DEFAULT_BINARY_CONTENT_TYPES.contains(contentType);
250+
}
251+
return false;
252+
}
253+
205254
protected CompletableFuture<ExecutionData> prepareAndExecuteRequest(HttpRequest request, boolean parallelInSameTransaction, AsyncTaskInvoker taskInvoker) {
206255
ExecutableHttpRequest httpRequest = httpClient.prepareRequest(request);
207256

modules/flowable-http/src/main/java/org/flowable/http/HttpRequest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,20 @@ public void setSaveResponseAsJson(boolean saveResponseAsJson) {
272272
}
273273
}
274274

275+
@Override
276+
public Boolean getSaveResponseVariableAsBase64() {
277+
return delegate != null ? delegate.getSaveResponseVariableAsBase64() : super.getSaveResponseVariableAsBase64();
278+
}
279+
280+
@Override
281+
public void setSaveResponseVariableAsBase64(Boolean saveResponseAsBinary) {
282+
if (delegate != null) {
283+
delegate.setSaveResponseVariableAsBase64(saveResponseAsBinary);
284+
} else {
285+
super.setSaveResponseVariableAsBase64(saveResponseAsBinary);
286+
}
287+
}
288+
275289
@Override
276290
public String getPrefix() {
277291
return delegate != null ? delegate.getPrefix() : super.getPrefix();

modules/flowable-http/src/test/java/org/flowable/http/bpmn/HttpServiceTaskTest.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,83 @@ public void testGetWithVariableParameters(String requestParam, String expectedRe
675675
assertProcessEnded(procId);
676676
}
677677

678+
@Test
679+
@Deployment(resources = "org/flowable/http/bpmn/HttpServiceTaskTest.testGetWithSaveResponseVariableAsBase64.bpmn20.xml")
680+
public void testGetWithSaveResponseVariableAsBase64Pdf() {
681+
String procId = runtimeService.createProcessInstanceBuilder()
682+
.processDefinitionKey("simpleGetOnly")
683+
.transientVariable("requestUrl","http://localhost:9798/binary/pdf")
684+
.transientVariable("saveResponseVariableAsBase64","true")
685+
.start()
686+
.getId();
687+
List<HistoricVariableInstance> variables = historyService.createHistoricVariableInstanceQuery().processInstanceId(procId).list();
688+
assertThat(variables)
689+
.extracting(HistoricVariableInstance::getVariableName)
690+
.containsExactly("base64Variable");
691+
assertThat(variables.get(0).getValue()).isInstanceOfSatisfying(String.class,
692+
value -> assertThat(value).startsWith("JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0Z"));
693+
assertProcessEnded(procId);
694+
}
695+
696+
/*
697+
* When setting base64 to false, the response should not be base64 encoded
698+
*/
699+
@Test
700+
@Deployment(resources = "org/flowable/http/bpmn/HttpServiceTaskTest.testGetWithSaveResponseVariableAsBase64.bpmn20.xml")
701+
public void testGetWithSaveResponseVariableAsBase64False() {
702+
String procId = runtimeService.createProcessInstanceBuilder()
703+
.processDefinitionKey("simpleGetOnly")
704+
.transientVariable("requestUrl","http://localhost:9798/binary/octet-stream-string")
705+
.transientVariable("saveResponseVariableAsBase64","false")
706+
.start()
707+
.getId();
708+
List<HistoricVariableInstance> variables = historyService.createHistoricVariableInstanceQuery().processInstanceId(procId).list();
709+
assertThat(variables)
710+
.extracting(HistoricVariableInstance::getVariableName)
711+
.containsExactly("base64Variable");
712+
assertThat(variables.get(0).getValue()).isInstanceOfSatisfying(String.class,
713+
value -> assertThat(value).isEqualTo("Content-Type is octet-stream, but still a string"));
714+
assertProcessEnded(procId);
715+
}
716+
717+
@Test
718+
@Deployment(resources = "org/flowable/http/bpmn/HttpServiceTaskTest.testGetWithSaveResponseVariableAsBase64.bpmn20.xml")
719+
public void testGetWithSaveResponseVariableAsBase64Json() {
720+
String procId = runtimeService.createProcessInstanceBuilder()
721+
.processDefinitionKey("simpleGetOnly")
722+
.transientVariable("requestUrl","http://localhost:9798/hello")
723+
.transientVariable("saveResponseVariableAsBase64","true")
724+
.start()
725+
.getId();
726+
List<HistoricVariableInstance> variables = historyService.createHistoricVariableInstanceQuery().processInstanceId(procId).list();
727+
assertThat(variables)
728+
.extracting(HistoricVariableInstance::getVariableName)
729+
.containsExactly("base64Variable");
730+
assertThat(variables.get(0).getValue()).isInstanceOfSatisfying(String.class,
731+
value -> assertThat(value).startsWith("PGh0bWw+CjxoZWFkPgo8bWV0YSBodHRwLWVxdWl2PSJDb250ZW50LVR5cGUiIGNvbnRlbnQ9I"));
732+
assertProcessEnded(procId);
733+
}
734+
735+
736+
/*
737+
* When saveResponseVariableAsBase64 is not defined, most common binary content is automatically base64 encoded.
738+
*/
739+
@Test
740+
@Deployment
741+
public void testGetBinaryAutoBase64() {
742+
String procId = runtimeService.createProcessInstanceBuilder()
743+
.processDefinitionKey("simpleGetOnly")
744+
.transientVariable("requestUrl","http://localhost:9798/binary/pdf").start().getId();
745+
List<HistoricVariableInstance> variables = historyService.createHistoricVariableInstanceQuery().processInstanceId(procId).list();
746+
assertThat(variables)
747+
.extracting(HistoricVariableInstance::getVariableName)
748+
.containsExactly("base64Variable");
749+
assertThat(variables.get(0).getValue()).isInstanceOfSatisfying(String.class,
750+
value -> assertThat(value).startsWith("JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0Z"));
751+
assertProcessEnded(procId);
752+
}
753+
754+
678755
static Stream<Arguments> parametersForGetWithVariableParameters() {
679756
return Stream.of(
680757
Arguments.arguments("Test+ Plus", "Test+ Plus"),

modules/flowable-http/src/test/java/org/flowable/http/bpmn/HttpServiceTaskTestServer.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ public class HttpServiceTaskTestServer {
9595
httpServiceTaskServletHolder.getRegistration().setMultipartConfig(multipartConfig);
9696
contextHandler.addServlet(httpServiceTaskServletHolder, "/api/*");
9797
contextHandler.addServlet(new ServletHolder(new SimpleHttpServiceTaskTestServlet()), "/test");
98+
contextHandler.addServlet(new ServletHolder(new SimpleHttpServiceTaskBinaryContentTestServlet()), "/binary/pdf");
99+
contextHandler.addServlet(new ServletHolder(new OctetStreamStringTestServlet()), "/binary/octet-stream-string");
98100
contextHandler.addServlet(new ServletHolder(new HelloServlet()), "/hello");
99101
contextHandler.addServlet(new ServletHolder(new ArrayResponseServlet()), "/array-response");
100102
contextHandler.addServlet(new ServletHolder(new DeleteResponseServlet()), "/delete");
@@ -286,6 +288,31 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
286288
resp.getWriter().println(responseNode);
287289
}
288290
}
291+
292+
private static class OctetStreamStringTestServlet extends HttpServlet {
293+
294+
private static final long serialVersionUID = 1L;
295+
296+
@Override
297+
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
298+
resp.setStatus(200);
299+
resp.setContentType("application/octet-stream");
300+
resp.getWriter().write("Content-Type is octet-stream, but still a string");
301+
}
302+
}
303+
304+
private static class SimpleHttpServiceTaskBinaryContentTestServlet extends HttpServlet {
305+
306+
private static final long serialVersionUID = 1L;
307+
308+
@Override
309+
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
310+
resp.setStatus(200);
311+
resp.setContentType("application/pdf");
312+
InputStream byteArrayInputStream = getClass().getClassLoader().getResourceAsStream("org/flowable/http/content/sample.pdf");
313+
IOUtils.copy(byteArrayInputStream, resp.getOutputStream());
314+
}
315+
}
289316

290317
private static class HelloServlet extends HttpServlet {
291318

modules/flowable-http/src/test/java/org/flowable/http/cmmn/CmmnHttpTaskTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,76 @@ public void testExpressions() throws Exception {
319319
);
320320
}
321321

322+
@Test
323+
@CmmnDeployment(resources = "org/flowable/http/cmmn/CmmnHttpTaskTest.testGetWithSaveResponseVariableAsBase64.cmmn")
324+
public void testGetWithSaveResponseVariableAsBase64Pdf() {
325+
CaseInstance caseInstance = cmmnRule.getCmmnRuntimeService().createCaseInstanceBuilder()
326+
.caseDefinitionKey("myCase")
327+
.transientVariable("saveResponseVariableAsBase64","true")
328+
.transientVariable("requestUrl","http://localhost:9798/binary/pdf")
329+
.start();
330+
331+
Map<String, Object> variables = caseInstance.getCaseVariables();
332+
assertThat(variables.get("base64Variable")).isInstanceOfSatisfying(String.class,
333+
value -> assertThat(value).startsWith("JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0Z"));
334+
}
335+
336+
/*
337+
* When setting base64 to false, the response should not be base64 encoded
338+
*/
339+
@Test
340+
@CmmnDeployment(resources = "org/flowable/http/cmmn/CmmnHttpTaskTest.testGetWithSaveResponseVariableAsBase64.cmmn")
341+
public void testGetWithSaveResponseVariableAsBase64False() {
342+
CaseInstance caseInstance = cmmnRule.getCmmnRuntimeService().createCaseInstanceBuilder()
343+
.caseDefinitionKey("myCase")
344+
.transientVariable("saveResponseVariableAsBase64","false")
345+
.transientVariable("requestUrl","http://localhost:9798/binary/octet-stream-string")
346+
.start();
347+
348+
Map<String, Object> variables = caseInstance.getCaseVariables();
349+
assertThat(variables.get("base64Variable")).isInstanceOfSatisfying(String.class,
350+
value -> assertThat(value).isEqualTo("Content-Type is octet-stream, but still a string"));
351+
}
352+
353+
@Test
354+
@CmmnDeployment(resources = "org/flowable/http/cmmn/CmmnHttpTaskTest.testGetWithSaveResponseVariableAsBase64.cmmn")
355+
public void testGetWithSaveResponseVariableAsBase64Json() {
356+
CaseInstance caseInstance = cmmnRule.getCmmnRuntimeService().createCaseInstanceBuilder()
357+
.caseDefinitionKey("myCase")
358+
.transientVariable("saveResponseVariableAsBase64","true")
359+
.transientVariable("requestUrl","http://localhost:9798/binary/pdf")
360+
.start();
361+
362+
Map<String, Object> variables = caseInstance.getCaseVariables();
363+
assertThat(variables.get("base64Variable")).isInstanceOfSatisfying(String.class,
364+
value -> assertThat(value).startsWith("JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZU"));
365+
}
366+
367+
368+
@Test
369+
@CmmnDeployment
370+
public void testGetBinaryAutoBase64() {
371+
CaseInstance caseInstance = cmmnRule.getCmmnRuntimeService().createCaseInstanceBuilder()
372+
.caseDefinitionKey("myCase")
373+
.transientVariable("saveResponseVariableAsBase64", "true")
374+
.transientVariable("requestUrl", "http://localhost:9798/binary/pdf")
375+
.start();
376+
377+
Map<String, Object> variables = caseInstance.getCaseVariables();
378+
assertThat(variables.get("base64Variable")).isInstanceOfSatisfying(String.class,
379+
value -> assertThat(value).startsWith("JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0Z"));
380+
}
381+
382+
@Test
383+
@CmmnDeployment
384+
public void testGetWithInvalidConfigJsonAndBase64() {
385+
assertThatThrownBy(() -> cmmnRule.getCmmnRuntimeService().createCaseInstanceBuilder()
386+
.caseDefinitionKey("myCase")
387+
.transientVariable("requestUrl", "http://localhost:9798/binary/pdf")
388+
.start()).isInstanceOf(FlowableException.class)
389+
.hasMessage("Cannot set both 'saveResponseVariableAsJson' and 'saveResponseVariableAsBase64' to true");
390+
}
391+
322392
protected CaseInstance createCaseInstance() {
323393
return cmmnRule.getCmmnRuntimeService().createCaseInstanceBuilder()
324394
.caseDefinitionKey("myCase")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn"
4+
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
5+
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
6+
typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath"
7+
targetNamespace="http://www.flowable.org/processdef">
8+
<process id="simpleGetOnly" name="Simple HTTP Get process">
9+
<serviceTask id="httpGet" name="HTTP Get" flowable:type="http">
10+
<extensionElements>
11+
<flowable:field name="requestMethod">
12+
<flowable:string><![CDATA[GET]]></flowable:string>
13+
</flowable:field>
14+
<flowable:field name="requestUrl">
15+
<flowable:expression><![CDATA[${requestUrl}]]></flowable:expression>
16+
</flowable:field>
17+
<flowable:field name="responseVariableName">
18+
<flowable:string><![CDATA[base64Variable]]></flowable:string>
19+
</flowable:field>
20+
</extensionElements>
21+
</serviceTask>
22+
<startEvent id="theStart" name="Start"></startEvent>
23+
<endEvent id="theEnd" name="End"></endEvent>
24+
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="httpGet"></sequenceFlow>
25+
<sequenceFlow id="flow2" sourceRef="httpGet" targetRef="theEnd"></sequenceFlow>
26+
</process>
27+
</definitions>

0 commit comments

Comments
 (0)