diff --git a/README.adoc b/README.adoc index 60e7b5fd..bced0bee 100644 --- a/README.adoc +++ b/README.adoc @@ -38,8 +38,8 @@ The following features are available in both Pipeline and traditional project ty * You can specify a string that must be present in the response (if the string is not present, the build fails) * You can set a connection timeout limit (build fails if timeout is exceeded) -* You can set an "Accept" header -* You can set a "Content-Type" header +* You can set an "Accept" header directly +* You can set a "Content-Type" header directly * You can set any custom header === Basic plugin features @@ -102,6 +102,13 @@ You can also set custom headers: def response = httpRequest customHeaders: [[name: 'foo', value: 'bar']] ---- +You can send ``multipart/form-data`` forms: + +[source,groovy] +---- +def response = httpRequest httpMode: 'POST', formData: [[contentType: 'application/json', name: 'model', body: '{"foo": "bar"}'], [contentType: 'text/plain', name: 'file', fileName: 'readme.txt', uploadFile: 'data/lipsum.txt']] +---- + For details on the Pipeline features, use the Pipeline snippet generator in the Pipeline job configuration. diff --git a/src/main/java/jenkins/plugins/http_request/HttpRequest.java b/src/main/java/jenkins/plugins/http_request/HttpRequest.java index a6a3165a..b3484ee6 100644 --- a/src/main/java/jenkins/plugins/http_request/HttpRequest.java +++ b/src/main/java/jenkins/plugins/http_request/HttpRequest.java @@ -45,6 +45,7 @@ import jenkins.plugins.http_request.auth.BasicDigestAuthentication; import jenkins.plugins.http_request.auth.FormAuthentication; import jenkins.plugins.http_request.util.HttpClientUtil; +import jenkins.plugins.http_request.util.HttpRequestFormDataPart; import jenkins.plugins.http_request.util.HttpRequestNameValuePair; /** @@ -74,6 +75,7 @@ public class HttpRequest extends Builder { private Boolean useSystemProperties = DescriptorImpl.useSystemProperties; private boolean useNtlm = DescriptorImpl.useNtlm; private List customHeaders = DescriptorImpl.customHeaders; + private List formData = DescriptorImpl.formData; @DataBoundConstructor public HttpRequest(@NonNull String url) { @@ -239,6 +241,15 @@ public void setCustomHeaders(List customHeaders) { this.customHeaders = customHeaders; } + public List getFormData() { + return formData; + } + + @DataBoundSetter + public void setFormData(List formData) { + this.formData = Collections.unmodifiableList(formData); + } + public String getUploadFile() { return uploadFile; } @@ -277,6 +288,9 @@ protected Object readResolve() { if (customHeaders == null) { customHeaders = DescriptorImpl.customHeaders; } + if (formData == null) { + formData = DescriptorImpl.formData; + } if (validResponseCodes == null || validResponseCodes.trim().isEmpty()) { validResponseCodes = DescriptorImpl.validResponseCodes; } @@ -371,27 +385,55 @@ FilePath resolveOutputFile(EnvVars envVars, AbstractBuild build) { return workspace.child(filePath); } - FilePath resolveUploadFile(EnvVars envVars, AbstractBuild build) { - if (uploadFile == null || uploadFile.trim().isEmpty()) { + FilePath resolveUploadFile(EnvVars envVars, AbstractBuild build) { + return resolveUploadFileInternal(uploadFile, envVars, build); + } + + private static FilePath resolveUploadFileInternal(String path, EnvVars envVars, AbstractBuild build) { + if (path == null || path.trim().isEmpty()) { return null; } - String filePath = envVars.expand(uploadFile); + String filePath = envVars.expand(path); try { FilePath workspace = build.getWorkspace(); if (workspace == null) { - throw new IllegalStateException("Could not find workspace to check existence of upload file: " + uploadFile + - ". You should use it inside a 'node' block"); + throw new IllegalStateException( + "Could not find workspace to check existence of upload file: " + path + + ". You should use it inside a 'node' block"); } FilePath uploadFilePath = workspace.child(filePath); - if (!uploadFilePath.exists()) { - throw new IllegalStateException("Could not find upload file: " + uploadFile); - } + if (!uploadFilePath.exists()) { + throw new IllegalStateException("Could not find upload file: " + path); + } return uploadFilePath; } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } + List resolveFormDataParts(EnvVars envVars, AbstractBuild build) { + if (formData == null || formData.isEmpty()) { + return Collections.emptyList(); + } + + List resolved = new ArrayList<>(formData.size()); + + for (HttpRequestFormDataPart part : formData) { + String name = envVars.expand(part.getName()); + String fileName = envVars.expand(part.getFileName()); + FilePath resolvedUploadFile = + resolveUploadFileInternal(part.getUploadFile(), envVars, build); + String body = envVars.expand(part.getBody()); + + HttpRequestFormDataPart newPart = new HttpRequestFormDataPart(part.getUploadFile(), + name, fileName, part.getContentType(), body); + newPart.setResolvedUploadFile(resolvedUploadFile); + resolved.add(newPart); + } + + return resolved; + } + @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException @@ -440,7 +482,8 @@ public static final class DescriptorImpl extends BuildStepDescriptor { public static final boolean wrapAsMultipart = true; public static final Boolean useSystemProperties = false; public static final boolean useNtlm = false; - public static final List customHeaders = Collections.emptyList(); + public static final List customHeaders = Collections.emptyList(); + public static final List formData = Collections.emptyList(); public DescriptorImpl() { load(); diff --git a/src/main/java/jenkins/plugins/http_request/HttpRequestExecution.java b/src/main/java/jenkins/plugins/http_request/HttpRequestExecution.java index 7606161f..918b5d9f 100644 --- a/src/main/java/jenkins/plugins/http_request/HttpRequestExecution.java +++ b/src/main/java/jenkins/plugins/http_request/HttpRequestExecution.java @@ -70,6 +70,7 @@ import jenkins.plugins.http_request.auth.CredentialBasicAuthentication; import jenkins.plugins.http_request.auth.CredentialNtlmAuthentication; import jenkins.plugins.http_request.util.HttpClientUtil; +import jenkins.plugins.http_request.util.HttpRequestFormDataPart; import jenkins.plugins.http_request.util.HttpRequestNameValuePair; import jenkins.plugins.http_request.util.RequestAction; @@ -87,6 +88,7 @@ public class HttpRequestExecution extends MasterToSlaveCallable headers; + private final List formData; private final FilePath uploadFile; private final String multipartName; @@ -117,12 +119,15 @@ static HttpRequestExecution from(HttpRequest http, FilePath uploadFile = http.resolveUploadFile(envVars, build); Item project = build.getProject(); + List formData = http.resolveFormDataParts(envVars, build); + return new HttpRequestExecution( url, http.getHttpMode(), http.getIgnoreSslErrors(), http.getHttpProxy(), http.getProxyAuthentication(), body, headers, http.getTimeout(), uploadFile, http.getMultipartName(), http.getWrapAsMultipart(), http.getAuthentication(), http.isUseNtlm(), http.getUseSystemProperties(), + formData, http.getValidResponseCodes(), http.getValidResponseContent(), http.getConsoleLogResponseBody(), outputFile, @@ -140,6 +145,9 @@ static HttpRequestExecution from(HttpRequestStep step, TaskListener taskListener List headers = step.resolveHeaders(); FilePath outputFile = execution.resolveOutputFile(); FilePath uploadFile = execution.resolveUploadFile(); + List formData = execution.resolveFormDataParts(); + + // TODO: resolveFormDataParts missing Item project = execution.getProject(); return new HttpRequestExecution( step.getUrl(), step.getHttpMode(), step.isIgnoreSslErrors(), @@ -147,6 +155,7 @@ static HttpRequestExecution from(HttpRequestStep step, TaskListener taskListener step.getRequestBody(), headers, step.getTimeout(), uploadFile, step.getMultipartName(), step.isWrapAsMultipart(), step.getAuthentication(), step.isUseNtlm(), step.getUseSystemProperties(), + formData, step.getValidResponseCodes(), step.getValidResponseContent(), step.getConsoleLogResponseBody(), outputFile, @@ -160,6 +169,7 @@ private HttpRequestExecution( List headers, Integer timeout, FilePath uploadFile, String multipartName, boolean wrapAsMultipart, String authentication, boolean useNtlm, boolean useSystemProperties, + List formData, String validResponseCodes, String validResponseContent, Boolean consoleLogResponseBody, FilePath outputFile, @@ -197,6 +207,7 @@ private HttpRequestExecution( this.body = body; this.headers = headers; + this.formData = formData; this.timeout = timeout != null ? timeout : -1; this.useNtlm = useNtlm; if (authentication != null && !authentication.isEmpty()) { @@ -293,9 +304,35 @@ private ResponseContentSupplier authAndRequest() } HttpClientUtil clientUtil = new HttpClientUtil(); + // Create the simple body, this is the most frequent operation. It will be overridden + // later, if a more complex payload descriptor is set. HttpRequestBase httpRequestBase = clientUtil.createRequestBase(new RequestAction(new URL(url), httpMode, body, null, headers)); - if (uploadFile != null && (httpMode == HttpMode.POST || httpMode == HttpMode.PUT)) { + if (formData != null && !formData.isEmpty() && httpMode == HttpMode.POST) { + // multipart/form-data builder mode + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + + for (HttpRequestFormDataPart part : formData) { + if (part.getFileName() == null || part.getFileName().isEmpty()) { + ContentType textContentType = part.getContentType() == null || part.getContentType().isEmpty() + ? ContentType.TEXT_PLAIN + : ContentType.create(part.getContentType()); + builder.addTextBody(part.getName(), part.getBody(), + textContentType); + } else { + ContentType fileContentType = part.getContentType() == null || part.getContentType().isEmpty() + ? ContentType.APPLICATION_OCTET_STREAM + : ContentType.create(part.getContentType()); + builder.addBinaryBody(part.getName(), + new File(part.getResolvedUploadFile().getRemote()), + fileContentType, part.getFileName()); + } + } + + HttpEntity mimeBody = builder.build(); + ((HttpEntityEnclosingRequestBase) httpRequestBase).setEntity(mimeBody); + } else if (uploadFile != null && (httpMode == HttpMode.POST || httpMode == HttpMode.PUT)) { + // No form-data, but a singular uploadFile is set. ContentType contentType = ContentType.APPLICATION_OCTET_STREAM; for (HttpRequestNameValuePair header : headers) { if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(header.getName())) { @@ -317,6 +354,7 @@ private ResponseContentSupplier authAndRequest() entity = new FileEntity(new File(uploadFile.getRemote()), contentType); } + // File upload overrides requestBody. ((HttpEntityEnclosingRequestBase) httpRequestBase).setEntity(entity); httpRequestBase.setHeader(entity.getContentType()); httpRequestBase.setHeader(entity.getContentEncoding()); diff --git a/src/main/java/jenkins/plugins/http_request/HttpRequestStep.java b/src/main/java/jenkins/plugins/http_request/HttpRequestStep.java index 7786bc6b..eddec451 100644 --- a/src/main/java/jenkins/plugins/http_request/HttpRequestStep.java +++ b/src/main/java/jenkins/plugins/http_request/HttpRequestStep.java @@ -31,6 +31,7 @@ import hudson.util.FormValidation; import hudson.util.ListBoxModel; +import jenkins.plugins.http_request.util.HttpRequestFormDataPart; import jenkins.plugins.http_request.util.HttpRequestNameValuePair; /** @@ -58,6 +59,7 @@ public final class HttpRequestStep extends Step { private Boolean useSystemProperties = DescriptorImpl.useSystemProperties; private boolean useNtlm = DescriptorImpl.useNtlm; private List customHeaders = DescriptorImpl.customHeaders; + private List formData = DescriptorImpl.formData; private String outputFile = DescriptorImpl.outputFile; private ResponseHandle responseHandle = DescriptorImpl.responseHandle; @@ -206,6 +208,15 @@ public List getCustomHeaders() { return customHeaders; } + public List getFormData() { + return formData; + } + + @DataBoundSetter + public void setFormData(List formData) { + this.formData = Collections.unmodifiableList(formData); + } + public String getOutputFile() { return outputFile; } @@ -311,6 +322,7 @@ public static final class DescriptorImpl extends StepDescriptor { public static final Boolean useSystemProperties = HttpRequest.DescriptorImpl.useSystemProperties; public static final boolean useNtlm = HttpRequest.DescriptorImpl.useNtlm; public static final List customHeaders = Collections.emptyList(); + public static final List formData = Collections.emptyList(); public static final String outputFile = ""; public static final ResponseHandle responseHandle = ResponseHandle.STRING; @@ -416,20 +428,27 @@ FilePath resolveOutputFile() { } FilePath resolveUploadFile() { - String uploadFile = step.getUploadFile(); - if (uploadFile == null || uploadFile.trim().isEmpty()) { + return resolveUploadFileInternal(step.getUploadFile()); + } + + public Item getProject() throws IOException, InterruptedException { + return Objects.requireNonNull(getContext().get(Run.class)).getParent(); + } + + private FilePath resolveUploadFileInternal(String path) { + if (path == null || path.trim().isEmpty()) { return null; } try { FilePath workspace = getContext().get(FilePath.class); if (workspace == null) { - throw new IllegalStateException("Could not find workspace to check existence of upload file: " + uploadFile + + throw new IllegalStateException("Could not find workspace to check existence of upload file: " + path + ". You should use it inside a 'node' block"); } - FilePath uploadFilePath = workspace.child(uploadFile); + FilePath uploadFilePath = workspace.child(path); if (!uploadFilePath.exists()) { - throw new IllegalStateException("Could not find upload file: " + uploadFile); + throw new IllegalStateException("Could not find upload file: " + path); } return uploadFilePath; } catch (IOException | InterruptedException e) { @@ -437,8 +456,22 @@ FilePath resolveUploadFile() { } } - public Item getProject() throws IOException, InterruptedException { - return Objects.requireNonNull(getContext().get(Run.class)).getParent(); + List resolveFormDataParts() { + List formData = step.getFormData(); + if (formData == null || formData.isEmpty()) { + return Collections.emptyList(); + } + + List resolved = new ArrayList<>(formData.size()); + + for (HttpRequestFormDataPart part : formData) { + HttpRequestFormDataPart newPart = new HttpRequestFormDataPart(part.getUploadFile(), + part.getName(), part.getFileName(), part.getContentType(), part.getBody()); + newPart.setResolvedUploadFile(resolveUploadFileInternal(part.getUploadFile())); + resolved.add(newPart); + } + + return resolved; } } } diff --git a/src/main/java/jenkins/plugins/http_request/MimeType.java b/src/main/java/jenkins/plugins/http_request/MimeType.java index 32cd7741..5a6b1e42 100644 --- a/src/main/java/jenkins/plugins/http_request/MimeType.java +++ b/src/main/java/jenkins/plugins/http_request/MimeType.java @@ -13,6 +13,7 @@ public enum MimeType { TEXT_HTML(ContentType.TEXT_HTML), TEXT_PLAIN(ContentType.TEXT_PLAIN), APPLICATION_FORM(ContentType.APPLICATION_FORM_URLENCODED), + APPLICATION_FORM_DATA(ContentType.MULTIPART_FORM_DATA), APPLICATION_JSON(ContentType.create("application/json")), APPLICATION_JSON_UTF8(ContentType.APPLICATION_JSON), APPLICATION_TAR(ContentType.create("application/x-tar")), diff --git a/src/main/java/jenkins/plugins/http_request/util/HttpRequestFormDataPart.java b/src/main/java/jenkins/plugins/http_request/util/HttpRequestFormDataPart.java new file mode 100644 index 00000000..12beb316 --- /dev/null +++ b/src/main/java/jenkins/plugins/http_request/util/HttpRequestFormDataPart.java @@ -0,0 +1,78 @@ +package jenkins.plugins.http_request.util; + +import hudson.Extension; +import hudson.FilePath; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import hudson.util.FormValidation; + +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + +import java.io.Serializable; + +public class HttpRequestFormDataPart extends AbstractDescribableImpl + implements Serializable { + + private static final long serialVersionUID = 1L; + private final String contentType; + private final String body; + private final String name; + private final String fileName; + private final String uploadFile; + private FilePath resolvedUploadFile; + + @DataBoundConstructor + public HttpRequestFormDataPart( + final String uploadFile, + final String name, + final String fileName, + final String contentType, + final String body) { + this.contentType = contentType; + this.body = body; + this.name = name; + this.fileName = fileName; + this.uploadFile = uploadFile; + } + + public String getBody() { + return body; + } + + public String getContentType() { + return contentType; + } + + public String getFileName() { + return fileName; + } + + public String getName() { + return name; + } + + public String getUploadFile() { + return uploadFile; + } + + public FilePath getResolvedUploadFile() { + return resolvedUploadFile; + } + + public void setResolvedUploadFile(FilePath resolvedUploadFile) { + this.resolvedUploadFile = resolvedUploadFile; + } + + @Extension + public static class FormDataDescriptor extends Descriptor { + @Override + public String getDisplayName() { + return "Multipart Form Data Entry"; + } + + public FormValidation doCheckName(@QueryParameter String value) { + return FormValidation.validateRequired(value); + } + } +} diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequest/config.jelly b/src/main/resources/jenkins/plugins/http_request/HttpRequest/config.jelly index fc66dbd8..934411e6 100644 --- a/src/main/resources/jenkins/plugins/http_request/HttpRequest/config.jelly +++ b/src/main/resources/jenkins/plugins/http_request/HttpRequest/config.jelly @@ -42,9 +42,9 @@ - - - + + + @@ -52,6 +52,11 @@ + + + + + diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestStep/config.jelly b/src/main/resources/jenkins/plugins/http_request/HttpRequestStep/config.jelly index abe91be6..f6a13a42 100644 --- a/src/main/resources/jenkins/plugins/http_request/HttpRequestStep/config.jelly +++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestStep/config.jelly @@ -45,11 +45,14 @@ - - + + + + + diff --git a/src/main/resources/jenkins/plugins/http_request/util/HttpRequestFormDataPart/config.jelly b/src/main/resources/jenkins/plugins/http_request/util/HttpRequestFormDataPart/config.jelly new file mode 100644 index 00000000..dd0bfb65 --- /dev/null +++ b/src/main/resources/jenkins/plugins/http_request/util/HttpRequestFormDataPart/config.jelly @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + +
+ +
+
+
diff --git a/src/main/webapp/help-formData.html b/src/main/webapp/help-formData.html new file mode 100644 index 00000000..919d022b --- /dev/null +++ b/src/main/webapp/help-formData.html @@ -0,0 +1,4 @@ +
+ Upload multiple entries as a multipart/form-data POST request.

+ Each entry must have a Content Type, a Part Name, and either a Body or a FileName and Path. +
diff --git a/src/main/webapp/help-partBody.html b/src/main/webapp/help-partBody.html new file mode 100644 index 00000000..76c11f6d --- /dev/null +++ b/src/main/webapp/help-partBody.html @@ -0,0 +1,3 @@ +
+ Textual content of this form-data Part. Don't forget to set the Content Type appropriately. +
diff --git a/src/main/webapp/help-partContentType.html b/src/main/webapp/help-partContentType.html new file mode 100644 index 00000000..44ffc2d2 --- /dev/null +++ b/src/main/webapp/help-partContentType.html @@ -0,0 +1,4 @@ +
+ Freetext Content Type of this form-data Part.

+ Defaults to text/plain for text bodies, application/octet-stream or files. +
diff --git a/src/main/webapp/help-partFileName.html b/src/main/webapp/help-partFileName.html new file mode 100644 index 00000000..07ef085c --- /dev/null +++ b/src/main/webapp/help-partFileName.html @@ -0,0 +1,3 @@ +
+ Suggested filename of this Part. +
diff --git a/src/main/webapp/help-partName.html b/src/main/webapp/help-partName.html new file mode 100644 index 00000000..fafb4dd0 --- /dev/null +++ b/src/main/webapp/help-partName.html @@ -0,0 +1,3 @@ +
+ Name of this form-data Part. +
diff --git a/src/main/webapp/help-partUploadFile.html b/src/main/webapp/help-partUploadFile.html new file mode 100644 index 00000000..3d298b4c --- /dev/null +++ b/src/main/webapp/help-partUploadFile.html @@ -0,0 +1,4 @@ +
+ Path to the upload file, relative to build workspace or absolute path.

+ The according content type should be set above. +
diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestStepTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestStepTest.java index b65f43cb..40c99e1a 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestStepTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestStepTest.java @@ -7,6 +7,7 @@ import static jenkins.plugins.http_request.Registers.registerFileUpload; import static jenkins.plugins.http_request.Registers.registerFormAuth; import static jenkins.plugins.http_request.Registers.registerFormAuthBad; +import static jenkins.plugins.http_request.Registers.registerFormData; import static jenkins.plugins.http_request.Registers.registerInvalidStatusCode; import static jenkins.plugins.http_request.Registers.registerReqAction; import static jenkins.plugins.http_request.Registers.registerRequestChecker; @@ -16,12 +17,14 @@ import java.io.File; import java.io.IOException; import java.net.URL; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.io.FilenameUtils; import org.apache.http.entity.ContentType; import org.eclipse.jetty.server.Request; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; @@ -691,6 +694,39 @@ public void testFileUpload() throws Exception { j.assertLogContains("Success: Status code 201 is in the accepted range: 201", run); } + @Test + public void testFormData() throws Exception { + final File testFolder = folder.newFolder(); + File projectRoot = Paths.get("").toAbsolutePath().toFile(); + String responseText = "File upload successful!"; + String json = "{\"foo\": \"bar\"}"; + File file1 = new File(projectRoot, "src/test/resources/testdata/readme.txt"); + File file2 = new File(projectRoot, "src/test/resources/testdata/small.zip"); + registerFormData(testFolder, json, file1, file2, responseText); + + // Let's upload these files and a JSON + String script = "node {\n" + + "def response = httpRequest httpMode: 'POST', validResponseCodes: '201', " + + "consoleLogResponseBody: true, acceptType: '" + MimeType.TEXT_PLAIN.toString() + + "', url: '" + baseURL() + "/formData', " + + "formData: [[contentType: 'application/json', body: '" + json + + "', name: 'model'], [contentType: 'text/plain', name: 'file1', fileName: 'readme.txt', uploadFile: '" + + FilenameUtils.separatorsToUnix(file1.getPath()) // Developing on windows is a joyride + + "'], [contentType: 'application/zip', name: 'file2', fileName: 'small.zip', uploadFile: '" + + FilenameUtils.separatorsToUnix(file2.getPath()) + "']]\n}"; + + // Prepare HttpRequest + WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "formData"); + proj.setDefinition(new CpsFlowDefinition(script, true)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + + // Check expectations + j.assertBuildStatusSuccess(run); + j.assertLogContains(responseText, run); + } + @Test public void nonExistentProxyAuthFailsTheBuild() throws Exception { // Prepare the server diff --git a/src/test/java/jenkins/plugins/http_request/Registers.java b/src/test/java/jenkins/plugins/http_request/Registers.java index f05d6d1f..5fe54ed1 100644 --- a/src/test/java/jenkins/plugins/http_request/Registers.java +++ b/src/test/java/jenkins/plugins/http_request/Registers.java @@ -8,6 +8,7 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Enumeration; import java.util.Map; @@ -19,6 +20,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; +import org.apache.commons.io.IOUtils; import org.apache.http.HttpHeaders; import org.apache.http.entity.ContentType; import org.eclipse.jetty.http.MultiPartFormInputStream; @@ -311,6 +313,67 @@ void doHandle(String target, Request baseRequest, HttpServletRequest request, Ht }); } + static void registerFormData(final File testFolder, String content, final File file1, + File file2, final String responseText) { + registerHandler("/formData", HttpMode.POST, new SimpleHandler() { + + private static final String MULTIPART_FORMDATA_TYPE = "multipart/form-data"; + + private void enableMultipartSupport(HttpServletRequest request, + MultipartConfigElement multipartConfig) { + request.setAttribute(Request.MULTIPART_CONFIG_ELEMENT, multipartConfig); + } + + private boolean isMultipartRequest(ServletRequest request) { + return request.getContentType() != null + && request.getContentType().startsWith(MULTIPART_FORMDATA_TYPE); + } + + @Override + void doHandle(String target, Request baseRequest, HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + assertEquals("POST", request.getMethod()); + assertTrue(isMultipartRequest(request)); + + MultipartConfigElement multipartConfig = + new MultipartConfigElement(testFolder.getAbsolutePath()); + enableMultipartSupport(request, multipartConfig); + + try { + Part file1Part = request.getPart("file1"); + assertNotNull(file1Part); + assertEquals(file1.length(), file1Part.getSize()); + assertEquals(file1.getName(), file1Part.getSubmittedFileName()); + assertEquals(MimeType.TEXT_PLAIN.getValue(), file1Part.getContentType()); + + Part file2Part = request.getPart("file2"); + assertNotNull(file2Part); + assertEquals(file2.length(), file2Part.getSize()); + assertEquals(file2.getName(), file2Part.getSubmittedFileName()); + assertEquals(MimeType.APPLICATION_ZIP.getValue(), file2Part.getContentType()); + + Part modelPart = request.getPart("model"); + assertNotNull(modelPart); + assertEquals(content, + IOUtils.toString(modelPart.getInputStream(), StandardCharsets.UTF_8)); + assertEquals(MimeType.APPLICATION_JSON.getValue(), modelPart.getContentType()); + + // So far so good + body(response, HttpServletResponse.SC_CREATED, ContentType.TEXT_PLAIN, + responseText); + } finally { + String MULTIPART = + "org.eclipse.jetty.servlet.MultiPartFile.multiPartInputStream"; + MultiPartFormInputStream multipartInputStream = + (MultiPartFormInputStream) request.getAttribute(MULTIPART); + if (multipartInputStream != null) { + multipartInputStream.deleteParts(); + } + } + } + }); + } + static void registerUnwrappedPutFileUpload(final File uploadFile, final String responseText) { registerHandler("/uploadFile/" + uploadFile.getName(), HttpMode.PUT, new SimpleHandler() { diff --git a/src/test/resources/testdata/readme.txt b/src/test/resources/testdata/readme.txt new file mode 100644 index 00000000..15754701 --- /dev/null +++ b/src/test/resources/testdata/readme.txt @@ -0,0 +1,6 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. diff --git a/src/test/resources/testdata/small.zip b/src/test/resources/testdata/small.zip new file mode 100644 index 00000000..b1fd1c39 Binary files /dev/null and b/src/test/resources/testdata/small.zip differ