NIFI-7394: Add support for sending Multipart/FORM data to InvokeHTTP.

By using dynamic properties with a prefix naming scheme, allow
definition of the parts, including the name to give the Flowfile content
part, and optionally it's file name.
After review:
- change so that we can send just the form content or just form data
  without the flowfile
- change the content name and content file name from dynamic properties
  to properties
- change the dynamic name to be an invalid http header "post:form:xxxx"
- add validation and more tests

This closes #4234.

Signed-off-by: Mark Payne <markap14@hotmail.com>
This commit is contained in:
Otto Fowler 2020-04-25 11:20:53 -04:00 committed by Mark Payne
parent 1259bd5bd1
commit 659a383723
2 changed files with 467 additions and 55 deletions

View File

@ -18,12 +18,6 @@ package org.apache.nifi.processors.standard;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
import com.burgstaller.okhttp.digest.CachingAuthenticator;
import com.burgstaller.okhttp.digest.DigestAuthenticator;
import com.google.common.io.Files;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@ -57,6 +51,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
@ -66,9 +61,17 @@ import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
import com.burgstaller.okhttp.digest.CachingAuthenticator;
import com.burgstaller.okhttp.digest.DigestAuthenticator;
import com.google.common.io.Files;
import okhttp3.Cache;
import okhttp3.Credentials;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.MultipartBody.Builder;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
@ -77,6 +80,7 @@ import okhttp3.ResponseBody;
import okio.BufferedSink;
import org.apache.commons.io.input.TeeInputStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.annotation.behavior.DynamicProperties;
import org.apache.nifi.annotation.behavior.DynamicProperty;
import org.apache.nifi.annotation.behavior.InputRequirement;
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
@ -131,9 +135,17 @@ import org.joda.time.format.DateTimeFormatter;
@WritesAttribute(attribute = "invokehttp.java.exception.message", description = "The Java exception message raised when the processor fails"),
@WritesAttribute(attribute = "user-defined", description = "If the 'Put Response Body In Attribute' property is set then whatever it is set to "
+ "will become the attribute key and the value would be the body of the HTTP response.")})
@DynamicProperty(name = "Header Name", value = "Attribute Expression Language", expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
description = "Send request header with a key matching the Dynamic Property Key and a value created by evaluating "
+ "the Attribute Expression Language set in the value of the Dynamic Property.")
@DynamicProperties ({
@DynamicProperty(name = "Header Name", value = "Attribute Expression Language", expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
description =
"Send request header with a key matching the Dynamic Property Key and a value created by evaluating "
+ "the Attribute Expression Language set in the value of the Dynamic Property."),
@DynamicProperty(name = "post:form:<NAME>", value = "Attribute Expression Language", expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
description =
"When the HTTP Method is POST, dynamic properties with the property name in the form of post:form:<NAME>,"
+ " where the <NAME> will be the form data name, will be used to fill out the multipart form parts."
+ " If send message body is false, the flowfile will not be sent, but any other form data will be.")
})
public final class InvokeHTTP extends AbstractProcessor {
// flowfile attribute keys returned after reading the response
public final static String STATUS_CODE = "invokehttp.status.code";
@ -148,6 +160,8 @@ public final class InvokeHTTP extends AbstractProcessor {
public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
public static final String FORM_BASE= "post:form";
// Set of flowfile attributes which we generally always ignore during
// processing, including when converting http headers, copying attributes, etc.
// This set includes our strings defined above as well as some standard flowfile
@ -163,6 +177,8 @@ public final class InvokeHTTP extends AbstractProcessor {
public static final String HTTP = "http";
public static final String HTTPS = "https";
private static final Pattern DYNAMIC_FORM_PARAMETER_NAME = Pattern.compile("post:form:(?<formDataName>.*)$");
// properties
public static final PropertyDescriptor PROP_METHOD = new PropertyDescriptor.Builder()
.name("HTTP Method")
@ -297,6 +313,30 @@ public final class InvokeHTTP extends AbstractProcessor {
.required(false)
.build();
public static final PropertyDescriptor PROP_FORM_BODY_FORM_NAME = new PropertyDescriptor.Builder()
.name("form-body-form-name")
.displayName("Flowfile Form Data Name")
.description("When Send Message Body is true, and Flowfile Form Data Name is set, "
+ " the Flowfile will be sent as the message body in multipart/form format with this value "
+ "as the form data name.")
.required(false)
.addValidator(
StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING, true))
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.build();
public static final PropertyDescriptor PROP_SET_FORM_FILE_NAME = new PropertyDescriptor.Builder()
.name("set-form-filename")
.displayName("Set Flowfile Form Data File Name")
.description(
"When Send Message Body is true, Flowfile Form Data Name is set, "
+ "and Set Flowfile Form Data File Name is true, the Flowfile's fileName value "
+ "will be set as the filename property of the form data.")
.required(false)
.defaultValue("true")
.allowableValues("true","false")
.build();
// Per RFC 7235, 2617, and 2616.
// basic-credentials = base64-user-pass
// base64-user-pass = userid ":" password
@ -450,7 +490,9 @@ public final class InvokeHTTP extends AbstractProcessor {
PROP_PENALIZE_NO_RETRY,
PROP_USE_ETAG,
PROP_ETAG_MAX_CACHE_SIZE,
IGNORE_RESPONSE_CONTENT));
IGNORE_RESPONSE_CONTENT,
PROP_FORM_BODY_FORM_NAME,
PROP_SET_FORM_FILE_NAME));
// relationships
public static final Relationship REL_SUCCESS_REQ = new Relationship.Builder()
@ -512,6 +554,22 @@ public final class InvokeHTTP extends AbstractProcessor {
@Override
protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(String propertyDescriptorName) {
if (propertyDescriptorName.startsWith(FORM_BASE)) {
Matcher matcher = DYNAMIC_FORM_PARAMETER_NAME.matcher(propertyDescriptorName);
if (matcher.matches()) {
return new PropertyDescriptor.Builder()
.required(false)
.name(propertyDescriptorName)
.description("Form Data " + matcher.group("formDataName"))
.addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING, true))
.dynamic(true)
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.build();
}
return null;
}
return new PropertyDescriptor.Builder()
.required(false)
.name(propertyDescriptorName)
@ -594,6 +652,35 @@ public final class InvokeHTTP extends AbstractProcessor {
}
}
// Check for dynamic properties for form components.
// Even if the flowfile is not sent, we may still send form parameters.
boolean hasFormData = false;
Map<String, PropertyDescriptor> propertyDescriptors = new HashMap<>();
for (final Map.Entry<PropertyDescriptor, String> entry : validationContext.getProperties().entrySet()) {
Matcher matcher = DYNAMIC_FORM_PARAMETER_NAME.matcher(entry.getKey().getName());
if (matcher.matches()) {
hasFormData = true;
break;
}
}
// if form data exists, and send body is true, Flowfile Form Data Name must be set.
final boolean sendBody = validationContext.getProperty(PROP_SEND_BODY).asBoolean();
final boolean contentNameSet = validationContext.getProperty(PROP_FORM_BODY_FORM_NAME).isSet();
if (hasFormData) {
if (sendBody && !contentNameSet) {
results.add(new ValidationResult.Builder().subject(PROP_FORM_BODY_FORM_NAME.getDisplayName())
.valid(false).explanation(
"If dynamic form data properties are set, and send body is true, Flowfile Form Data Name must be configured.")
.build());
}
}
if (!sendBody && contentNameSet) {
results.add(new ValidationResult.Builder().subject(PROP_FORM_BODY_FORM_NAME.getDisplayName())
.valid(false).explanation("If Flowfile Form Data Name is configured, Send Message Body must be true.")
.build());
}
return results;
}
@ -1023,29 +1110,66 @@ public final class InvokeHTTP extends AbstractProcessor {
return requestBuilder.build();
}
private RequestBody getRequestBodyToSend(final ProcessSession session, final ProcessContext context, final FlowFile requestFlowFile) {
if(context.getProperty(PROP_SEND_BODY).asBoolean()) {
return new RequestBody() {
@Override
public MediaType contentType() {
String contentType = context.getProperty(PROP_CONTENT_TYPE).evaluateAttributeExpressions(requestFlowFile).getValue();
contentType = StringUtils.isBlank(contentType) ? DEFAULT_CONTENT_TYPE : contentType;
return MediaType.parse(contentType);
}
private RequestBody getRequestBodyToSend(final ProcessSession session, final ProcessContext context,
final FlowFile requestFlowFile) {
@Override
public void writeTo(BufferedSink sink) throws IOException {
session.exportTo(requestFlowFile, sink.outputStream());
}
boolean sendBody = context.getProperty(PROP_SEND_BODY).asBoolean();
@Override
public long contentLength(){
return useChunked ? -1 : requestFlowFile.getSize();
}
};
} else {
return RequestBody.create(null, new byte[0]);
String evalContentType = context.getProperty(PROP_CONTENT_TYPE)
.evaluateAttributeExpressions(requestFlowFile).getValue();
final String contentType = StringUtils.isBlank(evalContentType) ? DEFAULT_CONTENT_TYPE : evalContentType;
String contentKey = context.getProperty(PROP_FORM_BODY_FORM_NAME).evaluateAttributeExpressions(requestFlowFile).getValue();
// Check for dynamic properties for form components.
// Even if the flowfile is not sent, we may still send form parameters.
Map<String, PropertyDescriptor> propertyDescriptors = new HashMap<>();
for (final Map.Entry<PropertyDescriptor, String> entry : context.getProperties().entrySet()) {
Matcher matcher = DYNAMIC_FORM_PARAMETER_NAME.matcher(entry.getKey().getName());
if (matcher.matches()) {
propertyDescriptors.put(matcher.group("formDataName"), entry.getKey());
}
}
RequestBody requestBody = new RequestBody() {
@Nullable
@Override
public MediaType contentType() {
return MediaType.parse(contentType);
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
session.exportTo(requestFlowFile, sink.outputStream());
}
@Override
public long contentLength() {
return useChunked ? -1 : requestFlowFile.getSize();
}
};
if (propertyDescriptors.size() > 0 || StringUtils.isNotEmpty(contentKey)) {
// we have form data
MultipartBody.Builder builder = new Builder().setType(MultipartBody.FORM);
boolean useFileName = context.getProperty(PROP_SET_FORM_FILE_NAME).asBoolean();
String contentFileName = null;
if (useFileName) {
contentFileName = requestFlowFile.getAttribute(CoreAttributes.FILENAME.key());
}
// loop through the dynamic form parameters
for (final Map.Entry<String, PropertyDescriptor> entry : propertyDescriptors.entrySet()) {
final String propValue = context.getProperty(entry.getValue().getName())
.evaluateAttributeExpressions(requestFlowFile).getValue();
builder.addFormDataPart(entry.getKey(), propValue);
}
if (sendBody) {
builder.addFormDataPart(contentKey, contentFileName, requestBody);
}
return builder.build();
} else if (sendBody) {
return requestBody;
}
return RequestBody.create(null, new byte[0]);
}
private Request.Builder setHeaderProperties(final ProcessContext context, Request.Builder requestBuilder, final FlowFile requestFlowFile) {
@ -1063,6 +1187,12 @@ public final class InvokeHTTP extends AbstractProcessor {
logger.warn(excludedHeaders.get(headerKey), new Object[]{headerKey});
continue;
}
// don't include dynamic form data properties
if ( DYNAMIC_FORM_PARAMETER_NAME.matcher(headerKey).matches()) {
continue;
}
requestBuilder = requestBuilder.addHeader(headerKey, headerValue);
}

View File

@ -17,28 +17,12 @@
package org.apache.nifi.processors.standard.util;
import org.apache.nifi.flowfile.attributes.CoreAttributes;
import org.apache.nifi.processors.standard.InvokeHTTP;
import org.apache.nifi.provenance.ProvenanceEventRecord;
import org.apache.nifi.provenance.ProvenanceEventType;
import org.apache.nifi.util.MockFlowFile;
import org.apache.nifi.util.TestRunner;
import org.apache.nifi.web.util.TestServer;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.junit.Assert;
import org.junit.Test;
import static org.apache.commons.codec.binary.Base64.encodeBase64;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
@ -49,11 +33,35 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import static org.apache.commons.codec.binary.Base64.encodeBase64;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.expression.ExpressionLanguageScope;
import org.apache.nifi.flowfile.attributes.CoreAttributes;
import org.apache.nifi.processors.standard.InvokeHTTP;
import org.apache.nifi.provenance.ProvenanceEventRecord;
import org.apache.nifi.provenance.ProvenanceEventType;
import org.apache.nifi.util.MockFlowFile;
import org.apache.nifi.util.StringUtils;
import org.apache.nifi.util.TestRunner;
import org.apache.nifi.web.util.TestServer;
import org.eclipse.jetty.http.MultiPartFormInputStream;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.MultiPartInputStreamParser;
import org.junit.Assert;
import org.junit.Test;
public abstract class TestInvokeHttpCommon {
@ -1011,6 +1019,195 @@ public abstract class TestInvokeHttpCommon {
runner.assertTransferCount(InvokeHTTP.REL_RESPONSE, 1);
}
@Test
public void testPostWithFormDataWithFileName() throws Exception {
final String suppliedMimeType = "text/plain";
MultipartFormHandler handler = new MultipartFormHandler();
handler.addExpectedPart("name1", "form data 1");
handler.addExpectedPart("name2", "form data 2");
handler.addExpectedPart("content", "Hello");
handler.addFileName("content", "file_name");
addHandler(handler);
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
// dynamic form properties
PropertyDescriptor dynamicProp1 = new PropertyDescriptor.Builder()
.dynamic(true)
.name(InvokeHTTP.FORM_BASE + ":name1")
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.build();
runner.setProperty(dynamicProp1, "form data 1");
PropertyDescriptor dynamicProp2 = new PropertyDescriptor.Builder()
.dynamic(true)
.name(InvokeHTTP.FORM_BASE + ":name2")
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.build();
runner.setProperty(dynamicProp2, "form data 2");
runner.setProperty(InvokeHTTP.PROP_FORM_BODY_FORM_NAME ,"content");
runner.setProperty(InvokeHTTP.PROP_SET_FORM_FILE_NAME, "true");
final Map<String, String> attrs = new HashMap<>();
attrs.put(CoreAttributes.MIME_TYPE.key(), "text/csv");
attrs.put(CoreAttributes.FILENAME.key(), "file_name");
runner.enqueue("Hello".getBytes(), attrs);
runner.run(1);
runner.assertTransferCount(InvokeHTTP.REL_SUCCESS_REQ, 1);
runner.assertTransferCount(InvokeHTTP.REL_RESPONSE, 1);
}
@Test
public void testPostFormContentOnly() throws Exception {
final String suppliedMimeType = "text/plain";
MultipartFormHandler handler = new MultipartFormHandler();
handler.addExpectedPart("content", "Hello");
handler.addFileName("content", "file_name");
addHandler(handler);
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
runner.setProperty(InvokeHTTP.PROP_FORM_BODY_FORM_NAME ,"content");
runner.setProperty(InvokeHTTP.PROP_SET_FORM_FILE_NAME, "true");
final Map<String, String> attrs = new HashMap<>();
attrs.put(CoreAttributes.MIME_TYPE.key(), "text/csv");
attrs.put(CoreAttributes.FILENAME.key(), "file_name");
runner.enqueue("Hello".getBytes(), attrs);
runner.run(1);
runner.assertTransferCount(InvokeHTTP.REL_SUCCESS_REQ, 1);
runner.assertTransferCount(InvokeHTTP.REL_RESPONSE, 1);
}
@Test
public void testPostWithFormDataNoFileName() throws Exception {
final String suppliedMimeType = "text/plain";
MultipartFormHandler handler = new MultipartFormHandler();
handler.addExpectedPart("name1", "form data 1");
handler.addExpectedPart("name2", "form data 2");
handler.addExpectedPart("content", "Hello");
addHandler(handler);
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
// dynamic form properties
PropertyDescriptor dynamicProp1 = new PropertyDescriptor.Builder()
.dynamic(true)
.name(InvokeHTTP.FORM_BASE + ":name1")
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.build();
runner.setProperty(dynamicProp1, "form data 1");
PropertyDescriptor dynamicProp2 = new PropertyDescriptor.Builder()
.dynamic(true)
.name(InvokeHTTP.FORM_BASE + ":name2")
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.build();
runner.setProperty(dynamicProp2, "form data 2");
runner.setProperty(InvokeHTTP.PROP_FORM_BODY_FORM_NAME ,"content");
final Map<String, String> attrs = new HashMap<>();
attrs.put(CoreAttributes.MIME_TYPE.key(), "text/csv");
runner.enqueue("Hello".getBytes(), attrs);
runner.run(1);
runner.assertTransferCount(InvokeHTTP.REL_SUCCESS_REQ, 1);
runner.assertTransferCount(InvokeHTTP.REL_RESPONSE, 1);
}
@Test
public void testPostWithFormDataNoFile() throws Exception {
final String suppliedMimeType = "text/plain";
MultipartFormHandler handler = new MultipartFormHandler();
handler.addExpectedPart("name1", "form data 1");
handler.addExpectedPart("name2", "form data 2");
addHandler(handler);
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
runner.setProperty(InvokeHTTP.PROP_SEND_BODY, "false");
// dynamic form properties
PropertyDescriptor dynamicProp1 = new PropertyDescriptor.Builder()
.dynamic(true)
.name(InvokeHTTP.FORM_BASE + ":name1")
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.build();
runner.setProperty(dynamicProp1, "form data 1");
PropertyDescriptor dynamicProp2 = new PropertyDescriptor.Builder()
.dynamic(true)
.name(InvokeHTTP.FORM_BASE + ":name2")
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.build();
runner.setProperty(dynamicProp2, "form data 2");
final Map<String, String> attrs = new HashMap<>();
attrs.put(CoreAttributes.MIME_TYPE.key(), "text/csv");
runner.enqueue("Hello".getBytes(), attrs);
runner.run(1);
runner.assertTransferCount(InvokeHTTP.REL_SUCCESS_REQ, 1);
runner.assertTransferCount(InvokeHTTP.REL_RESPONSE, 1);
}
@Test
public void testPostNoSendBodyWithContentFails() throws Exception {
final String suppliedMimeType = "text/plain";
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
runner.setProperty(InvokeHTTP.PROP_SEND_BODY, "false");
runner.setProperty(InvokeHTTP.PROP_FORM_BODY_FORM_NAME ,"content");
runner.assertNotValid();
}
@Test
public void testPostNoFormContentWithFileNameFails() throws Exception {
final String suppliedMimeType = "text/plain";
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
// dynamic form properties
PropertyDescriptor dynamicProp1 = new PropertyDescriptor.Builder()
.dynamic(true)
.name(InvokeHTTP.FORM_BASE + ":name1")
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.build();
runner.setProperty(dynamicProp1, "form data 1");
PropertyDescriptor dynamicProp2 = new PropertyDescriptor.Builder()
.dynamic(true)
.name(InvokeHTTP.FORM_BASE + ":name2")
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.build();
runner.setProperty(dynamicProp2, "form data 2");
runner.setProperty(InvokeHTTP.PROP_SET_FORM_FILE_NAME, "true");
final Map<String, String> attrs = new HashMap<>();
attrs.put(CoreAttributes.MIME_TYPE.key(), "text/csv");
attrs.put(CoreAttributes.FILENAME.key(), "file_name");
runner.enqueue("Hello".getBytes(), attrs);
runner.assertNotValid();
}
@Test
public void testPutWithMimeType() throws Exception {
final String suppliedMimeType = "text/plain";
@ -1555,6 +1752,91 @@ public abstract class TestInvokeHttpCommon {
}
public static class MultipartFormHandler extends AbstractHandler {
private static final String MULTIPART_FORMDATA_TYPE = "multipart/form-data";
private String headerToTrack;
private String trackedHeaderValue;
private final HashMap<String,String> expectedParts = new HashMap<>();
private String fileNamePartName = null;
private String fileName = null;
public MultipartFormHandler() {
}
public void addExpectedPart(String name, String value) {
expectedParts.put(name,value);
}
public void addFileName(String partName, String fileName) {
fileNamePartName = partName;
this.fileName = fileName;
}
private void setHeaderToTrack(String headerToTrack) {
this.headerToTrack = headerToTrack;
}
public String getTrackedHeaderValue() {
return trackedHeaderValue;
}
private boolean isMultipartRequest(HttpServletRequest request) {
return request.getContentType() != null
&& request.getContentType().startsWith(MULTIPART_FORMDATA_TYPE);
}
private static final MultipartConfigElement MULTI_PART_CONFIG = new MultipartConfigElement(
System.getProperty("java.io.tmpdir"));
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
baseRequest.setHandled(true);
assertTrue(request.getHeader("Content-Type").startsWith("multipart/form-data"));
this.trackedHeaderValue = baseRequest.getHttpFields().get(headerToTrack);
boolean multipartRequest = isMultipartRequest(request);
if (multipartRequest) {
baseRequest.setAttribute(Request.MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG);
if (expectedParts.size() > 0) {
assertEquals(expectedParts.size(), request.getParts().size());
}
for (Part part : request.getParts()) {
String name;
String val;
String partFileName;
if (part instanceof MultiPartInputStreamParser.MultiPart) {
MultiPartInputStreamParser.MultiPart multiPart = ((MultiPartInputStreamParser.MultiPart) part);
val = new String(multiPart.getBytes());
name = part.getName();
partFileName = part.getSubmittedFileName();
} else if (part instanceof MultiPartFormInputStream.MultiPart) {
MultiPartFormInputStream.MultiPart multiPart = ((MultiPartFormInputStream.MultiPart) part);
val = new String(multiPart.getBytes());
name = part.getName();
partFileName = part.getSubmittedFileName();
} else {
name = "NO";
val = "NO";
partFileName = "NO";
}
if (expectedParts.size() > 0) {
assertNotNull(expectedParts.get(name));
assertEquals(expectedParts.get(name), val);
}
if (!StringUtils.isBlank(fileNamePartName)) {
if (name.equals(fileNamePartName)) {
assertEquals(fileName, partFileName);
}
}
}
}
}
}
public static class GetOrHeadHandler extends AbstractHandler {
@Override