mirror of https://github.com/apache/nifi.git
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:
parent
1259bd5bd1
commit
659a383723
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue