diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java index 2ccaed5922..15a039525b 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java @@ -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:", 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:," + + " where the 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:(?.*)$"); + // 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 propertyDescriptors = new HashMap<>(); + for (final Map.Entry 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 propertyDescriptors = new HashMap<>(); + for (final Map.Entry 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 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); } diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/TestInvokeHttpCommon.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/TestInvokeHttpCommon.java index 7842a16f57..c11e4f3e69 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/TestInvokeHttpCommon.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/TestInvokeHttpCommon.java @@ -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 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 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 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 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 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 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