diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/AbstractPart.java b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/AbstractPart.java new file mode 100644 index 00000000000..b81dc47e8ca --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/AbstractPart.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter.multipart; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import org.springframework.http.MediaType; +import org.springframework.util.Assert; + +/** + *

Inspired by {@link org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}. + * + * @author Arjen Poutsma + * @since 3.0.2 + */ +abstract class AbstractPart implements Part { + + private static final byte[] CONTENT_DISPOSITION = + new byte[]{'C', 'o', 'n', 't', 'e', 'n', 't', '-', 'D', 'i', 's', 'p', 'o', 's', 'i', 't', 'i', 'o', 'n', + ':', ' ', 'f', 'o', 'r', 'm', '-', 'd', 'a', 't', 'a', ';', ' ', 'n', 'a', 'm', 'e', '='}; + + private static final byte[] CONTENT_TYPE = + new byte[]{'C', 'o', 'n', 't', 'e', 'n', 't', '-', 'T', 'y', 'p', 'e', ':', ' '}; + + private final MediaType contentType; + + protected AbstractPart(MediaType contentType) { + Assert.notNull(contentType, "'contentType' must not be null"); + this.contentType = contentType; + } + + public final void write(byte[] boundary, String name, OutputStream os) throws IOException { + writeBoundary(boundary, os); + writeContentDisposition(name, os); + writeContentType(os); + writeEndOfHeader(os); + writeData(os); + writeEnd(os); + } + + protected void writeBoundary(byte[] boundary, OutputStream os) throws IOException { + os.write('-'); + os.write('-'); + os.write(boundary); + writeNewLine(os); + } + + protected void writeContentDisposition(String name, OutputStream os) throws IOException { + os.write(CONTENT_DISPOSITION); + os.write('"'); + os.write(getAsciiBytes(name)); + os.write('"'); + } + + protected void writeContentType(OutputStream os) throws IOException { + writeNewLine(os); + os.write(CONTENT_TYPE); + os.write(getAsciiBytes(contentType.toString())); + } + + protected byte[] getAsciiBytes(String name) { + try { + return name.getBytes("US-ASCII"); + } + catch (UnsupportedEncodingException ex) { + // should not happen, US-ASCII is always supported + throw new IllegalStateException(ex); + } + } + + protected void writeEndOfHeader(OutputStream os) throws IOException { + writeNewLine(os); + writeNewLine(os); + } + + protected void writeEnd(OutputStream os) throws IOException { + writeNewLine(os); + } + + private void writeNewLine(OutputStream os) throws IOException { + os.write('\r'); + os.write('\n'); + } + + protected abstract void writeData(OutputStream os) throws IOException; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/ByteArrayPart.java b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/ByteArrayPart.java new file mode 100644 index 00000000000..d1d81f5f4d7 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/ByteArrayPart.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter.multipart; + +import java.io.IOException; +import java.io.OutputStream; + +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; + +/** + * @author Arjen Poutsma + * @since 3.0.2 + */ +class ByteArrayPart extends AbstractPart { + + private final byte[] value; + + public ByteArrayPart(byte[] value, MediaType contentType) { + super(contentType); + Assert.isTrue(value != null && value.length != 0, "'value' must not be null"); + this.value = value; + } + + @Override + protected void writeData(OutputStream os) throws IOException { + FileCopyUtils.copy(value, os); + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java new file mode 100644 index 00000000000..edcd7737d5c --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter.multipart; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; + +/** + *

Inspired by {@link org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}. + * + * @author Arjen Poutsma + * @since 3.0.2 + */ +public class MultipartHttpMessageConverter extends AbstractHttpMessageConverter { + + private static final byte[] BOUNDARY_CHARS = + new byte[]{'-', '_', + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; + + private final Random rnd = new Random(); + + public MultipartHttpMessageConverter() { + super(new MediaType("multipart", "form-data")); + } + + @Override + protected boolean supports(Class clazz) { + return MultipartMap.class.isAssignableFrom(clazz); + } + + @Override + protected void writeInternal(MultipartMap map, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + byte[] boundary = generateBoundary(); + HttpHeaders headers = outputMessage.getHeaders(); + MediaType contentType = headers.getContentType(); + if (contentType != null) { + String boundaryString = new String(boundary, "US-ASCII"); + Map params = Collections.singletonMap("boundary", boundaryString); + contentType = new MediaType(contentType.getType(), contentType.getSubtype(), params); + headers.setContentType(contentType); + } + OutputStream os = outputMessage.getBody(); + for (Map.Entry> entry : map.entrySet()) { + String name = entry.getKey(); + for (Part part : entry.getValue()) { + part.write(boundary, name, os); + } + } + os.write('-'); + os.write('-'); + os.write(boundary); + os.write('-'); + os.write('-'); + os.write('\r'); + os.write('\n'); + } + + + + /** + * Generate a multipart boundary. + * + *

Default implementation returns a random boundary. + */ + protected byte[] generateBoundary() { + byte[] boundary = new byte[rnd.nextInt(11) + 30]; + for (int i = 0; i < boundary.length; i++) { + boundary[i] = BOUNDARY_CHARS[rnd.nextInt(BOUNDARY_CHARS.length)]; + } + return boundary; + } + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + // reading not supported yet + return false; + } + + @Override + protected MultipartMap readInternal(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + throw new UnsupportedOperationException(); + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/MultipartMap.java b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/MultipartMap.java new file mode 100644 index 00000000000..40231cd6a8e --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/MultipartMap.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter.multipart; + +import java.io.File; +import java.nio.charset.Charset; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; + +/** + * @author Arjen Poutsma + * @since 3.0.2 + */ +public class MultipartMap extends LinkedMultiValueMap { + + public void addTextPart(String name, String value) { + Assert.hasText(name, "'name' must not be empty"); + add(name, new StringPart(value)); + } + + public void addTextPart(String name, String value, Charset charset) { + Assert.hasText(name, "'name' must not be empty"); + add(name, new StringPart(value, charset)); + } + + public void addBinaryPart(String name, Resource resource) { + Assert.hasText(name, "'name' must not be empty"); + add(name, new ResourcePart(resource)); + } + + public void addBinaryPart(Resource resource) { + Assert.notNull(resource, "'resource' must not be null"); + addBinaryPart(resource.getFilename(), resource); + } + + public void addBinaryPart(String name, File file) { + addBinaryPart(name, new FileSystemResource(file)); + } + + public void addBinaryPart(File file) { + addBinaryPart(new FileSystemResource(file)); + } + + public void addPart(String name, byte[] value, MediaType contentType) { + Assert.hasText(name, "'name' must not be empty"); + add(name, new ByteArrayPart(value, contentType)); + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/Part.java b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/Part.java new file mode 100644 index 00000000000..f60788d9911 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/Part.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter.multipart; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * @author Arjen Poutsma + * @since 3.0.2 + */ +public interface Part { + + void write(byte[] boundary, String name, OutputStream os) throws IOException; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/ResourcePart.java b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/ResourcePart.java new file mode 100644 index 00000000000..a6ae5ed61b8 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/ResourcePart.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter.multipart; + +import java.io.IOException; +import java.io.OutputStream; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +/** @author Arjen Poutsma */ +class ResourcePart extends AbstractPart { + + private static final byte[] FILE_NAME = new byte[]{';', ' ', 'f', 'i', 'l', 'e', 'n', 'a', 'm', 'e', '='}; + + private final Resource resource; + + public ResourcePart(Resource resource) { + super(new MediaType("application", "octet-stream")); + Assert.notNull(resource, "'resource' must not be null"); + Assert.isTrue(resource.exists(), "'" + resource + "' does not exist"); + this.resource = resource; + } + + @Override + protected void writeContentDisposition(String name, OutputStream os) throws IOException { + super.writeContentDisposition(name, os); + String filename = resource.getFilename(); + if (StringUtils.hasLength(filename)) { + os.write(FILE_NAME); + os.write('"'); + os.write(getAsciiBytes(filename)); + os.write('"'); + } + } + + @Override + protected void writeData(OutputStream os) throws IOException { + FileCopyUtils.copy(resource.getInputStream(), os); + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/StringPart.java b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/StringPart.java new file mode 100644 index 00000000000..3e5f223c37d --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/StringPart.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter.multipart; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; + +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; + +/** @author Arjen Poutsma */ +class StringPart extends AbstractPart { + + private static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); + + private final String value; + + private final Charset charset; + + public StringPart(String value) { + this(value, DEFAULT_CHARSET); + } + + public StringPart(String value, Charset charset) { + super(new MediaType("text", "plain", charset)); + Assert.hasText(value, "'value' must not be null"); + Assert.notNull(charset, "'charset' must not be null"); + this.value = value; + this.charset = charset; + } + + @Override + protected void writeData(OutputStream os) throws IOException { + FileCopyUtils.copy(value, new OutputStreamWriter(os, charset)); + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/package-info.java b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/package-info.java new file mode 100644 index 00000000000..fd9ca91e344 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * + * Provides a HttpMessageConverter implementations for handling multipart data. + * + */ + +package org.springframework.http.converter.multipart; diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java b/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java index 12673043d03..5b1fa0f0b92 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -37,6 +37,7 @@ import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.http.converter.multipart.MultipartHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.util.Assert; @@ -122,6 +123,7 @@ public class RestTemplate extends HttpAccessor implements RestOperations { public RestTemplate() { this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); + this.messageConverters.add(new MultipartHttpMessageConverter()); this.messageConverters.add(new FormHttpMessageConverter()); this.messageConverters.add(new SourceHttpMessageConverter()); if (jaxb2Present) { diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTest.java b/org.springframework.web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTest.java new file mode 100644 index 00000000000..76613d21fdf --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter.multipart; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.FileUpload; +import org.apache.commons.fileupload.RequestContext; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.http.MockHttpOutputMessage; + +/** @author Arjen Poutsma */ +public class MultipartHttpMessageConverterTest { + + private MultipartHttpMessageConverter converter; + + @Before + public void setUp() { + converter = new MultipartHttpMessageConverter(); + } + + @Test + public void canRead() { + assertFalse(converter.canRead(MultipartMap.class, new MediaType("multipart","form-data"))); + } + + @Test + public void canWrite() { + assertTrue(converter.canWrite(MultipartMap.class, new MediaType("multipart","form-data"))); + assertTrue(converter.canWrite(MultipartMap.class, MediaType.ALL)); + } + + @Test + public void write() throws Exception { + MultipartMap body = new MultipartMap(); + body.addTextPart("name 1", "value 1"); + body.addTextPart("name 2", "value 2+1"); + body.addTextPart("name 2", "value 2+2"); + Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); + body.addBinaryPart("logo", logo); + byte[] xml = "".getBytes("UTF-8"); + body.addPart("xml", xml, new MediaType("application", "xml")); + + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(body, null, outputMessage); + final MediaType contentType = outputMessage.getHeaders().getContentType(); + final byte[] result = outputMessage.getBodyAsBytes(); + assertNotNull(contentType); + assertNotNull(contentType.getParameter("boundary")); + + // see if Commons FileUpload can read what we wrote + FileItemFactory fileItemFactory = new DiskFileItemFactory(); + FileUpload fileUpload = new FileUpload(fileItemFactory); + List items = fileUpload.parseRequest(new RequestContext() { + public String getCharacterEncoding() { + return null; + } + + public String getContentType() { + return contentType.toString(); + } + + public int getContentLength() { + return result.length; + } + + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(result); + } + }); + assertEquals(5, items.size()); + FileItem item = (FileItem) items.get(0); + assertTrue(item.isFormField()); + assertEquals("name 1", item.getFieldName()); + assertEquals("value 1", item.getString()); + + item = (FileItem) items.get(1); + assertTrue(item.isFormField()); + assertEquals("name 2", item.getFieldName()); + assertEquals("value 2+1", item.getString()); + + item = (FileItem) items.get(2); + assertTrue(item.isFormField()); + assertEquals("name 2", item.getFieldName()); + assertEquals("value 2+2", item.getString()); + + item = (FileItem) items.get(3); + assertFalse(item.isFormField()); + assertEquals("logo", item.getFieldName()); + assertEquals("logo.jpg", item.getName()); + assertEquals("application/octet-stream", item.getContentType()); + assertEquals(logo.getFile().length(), item.getSize()); + + item = (FileItem) items.get(4); + assertEquals("xml", item.getFieldName()); + assertEquals("application/xml", item.getContentType()); + assertEquals(xml.length, item.getSize()); + } + + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java b/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java index 0f6f8fb9b70..a0c47767756 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2010 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,11 @@ package org.springframework.web.client; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.EnumSet; +import java.util.List; import java.util.Set; import javax.servlet.GenericServlet; import javax.servlet.ServletException; @@ -29,6 +31,11 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.junit.AfterClass; import static org.junit.Assert.*; import org.junit.Before; @@ -38,8 +45,11 @@ import org.mortbay.jetty.Server; import org.mortbay.jetty.servlet.Context; import org.mortbay.jetty.servlet.ServletHolder; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.http.HttpMethod; import org.springframework.http.client.CommonsClientHttpRequestFactory; +import org.springframework.http.converter.multipart.MultipartMap; import org.springframework.util.FileCopyUtils; /** @author Arjen Poutsma */ @@ -67,6 +77,7 @@ public class RestTemplateIntegrationTests { jettyContext.addServlet(new ServletHolder(new ErrorServlet(404)), "/errors/notfound"); jettyContext.addServlet(new ServletHolder(new ErrorServlet(500)), "/errors/server"); jettyContext.addServlet(new ServletHolder(new UriServlet()), "/uri/*"); + jettyContext.addServlet(new ServletHolder(new MultipartServlet()), "/multipart"); jettyServer.start(); } @@ -132,6 +143,18 @@ public class RestTemplateIntegrationTests { assertEquals("Invalid request URI", "/uri/query=foo@bar", result); } + @Test + public void multipart() throws UnsupportedEncodingException { + MultipartMap body = new MultipartMap(); + body.addTextPart("name 1", "value 1"); + body.addTextPart("name 2", "value 2+1"); + body.addTextPart("name 2", "value 2+2"); + Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); + body.addBinaryPart("logo", logo); + + template.postForLocation(URI + "/multipart", body); + } + /** Servlet that returns and error message for a given status code. */ private static class ErrorServlet extends GenericServlet { @@ -209,4 +232,42 @@ public class RestTemplateIntegrationTests { } } + private static class MultipartServlet extends HttpServlet { + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + assertTrue(ServletFileUpload.isMultipartContent(req)); + FileItemFactory factory = new DiskFileItemFactory(); + ServletFileUpload upload = new ServletFileUpload(factory); + try { + List items = upload.parseRequest(req); + assertEquals(4, items.size()); + FileItem item = (FileItem) items.get(0); + assertTrue(item.isFormField()); + assertEquals("name 1", item.getFieldName()); + assertEquals("value 1", item.getString()); + + item = (FileItem) items.get(1); + assertTrue(item.isFormField()); + assertEquals("name 2", item.getFieldName()); + assertEquals("value 2+1", item.getString()); + + item = (FileItem) items.get(2); + assertTrue(item.isFormField()); + assertEquals("name 2", item.getFieldName()); + assertEquals("value 2+2", item.getString()); + + item = (FileItem) items.get(3); + assertFalse(item.isFormField()); + assertEquals("logo", item.getFieldName()); + assertEquals("logo.jpg", item.getName()); + assertEquals("application/octet-stream", item.getContentType()); + } + catch (FileUploadException ex) { + throw new ServletException(ex); + } + + } + } + }