diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index f042d82d512..9a79c5c5b2f 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -18,17 +18,27 @@ package org.springframework.http.converter; import java.io.IOException; import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Random; +import org.springframework.core.io.Resource; +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.xml.SourceHttpMessageConverter; +import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -36,36 +46,114 @@ import org.springframework.util.StringUtils; import org.springframework.web.util.WebUtils; /** - * Implementation of {@link HttpMessageConverter} that can read and write form data. + * Implementation of {@link HttpMessageConverter} that can handle form data, including multipart form data + * (i.e. file uploads). * - *

By default, this converter reads and writes the media type ({@code application/x-www-form-urlencoded}). This can - * be overridden by setting the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property. Form data - * is read from and written into a {@link MultiValueMap MultiValueMap<String, String>}. + *

This converter can write the {@code application/x-www-form-urlencoded} and {@code multipart/form-data} media + * types, and read the {@code application/x-www-form-urlencoded}) media type (but not {@code multipart/form-data}). + * + *

In other words, this converter can read and write 'normal' HTML forms (as + * {@link MultiValueMap MultiValueMap<String, String>}), and it can write multipart form (as + * {@link MultiValueMap MultiValueMap<String, Object>}. When writing multipart, this converter uses other + * {@link HttpMessageConverter HttpMessageConverters} to write the respective MIME parts. By default, basic converters + * are registered (supporting {@code Strings} and {@code Resources}, for instance); these can be overridden by setting + * the {@link #setPartConverters(java.util.List) partConverters} property. + * + *

For example, the following snippet shows how to submit an HTML form: + *

+ * RestTemplate template = new RestTemplate(); // FormHttpMessageConverter is configured by default
+ * MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
+ * form.add("field 1", "value 1");
+ * form.add("field 2", "value 2");
+ * form.add("field 2", "value 3");
+ * template.postForLocation("http://example.com/myForm", form);
+ * 
+ *

The following snippet shows how to do a file upload: + *

+ * MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
+ * parts.add("field 1", "value 1");
+ * parts.add("file", new ClassPathResource("myFile.jpg"));
+ * template.postForLocation("http://example.com/myFileUpload", parts);
+ * 
+ * + *

Some methods in this class were inspired by {@link org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}. * * @author Arjen Poutsma * @see MultiValueMap * @since 3.0 */ -public class FormHttpMessageConverter extends AbstractHttpMessageConverter> { +public class FormHttpMessageConverter implements HttpMessageConverter> { - public static final Charset DEFAULT_CHARSET = Charset.forName(WebUtils.DEFAULT_CHARACTER_ENCODING); + 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(); + + private Charset charset = Charset.forName(WebUtils.DEFAULT_CHARACTER_ENCODING); + + private List> partConverters = new ArrayList>(); - /** Creates a new instance of the {@code FormHttpMessageConverter}. */ public FormHttpMessageConverter() { - super(new MediaType("application", "x-www-form-urlencoded")); + this.partConverters.add(new ByteArrayHttpMessageConverter()); + StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); + stringHttpMessageConverter.setWriteAcceptCharset(false); + this.partConverters.add(stringHttpMessageConverter); + this.partConverters.add(new ResourceHttpMessageConverter()); + this.partConverters.add(new SourceHttpMessageConverter()); } - - @Override - public boolean supports(Class clazz) { - return MultiValueMap.class.isAssignableFrom(clazz); + /** + * Set the message body converters to use. These converters are used to convert objects to MIME parts. + */ + public void setPartConverters(List> partConverters) { + Assert.notEmpty(partConverters, "'messageConverters' must not be empty"); + this.partConverters = partConverters; } - @Override - public MultiValueMap readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { + /** + * Sets the character set used for writing form data. + */ + public void setCharset(Charset charset) { + this.charset = charset; + } + + public boolean canRead(Class clazz, MediaType mediaType) { + if (!MultiValueMap.class.isAssignableFrom(clazz)) { + return false; + } + if (mediaType != null) { + return MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType); + } + else { + return true; + } + } + + public boolean canWrite(Class clazz, MediaType mediaType) { + if (!MultiValueMap.class.isAssignableFrom(clazz)) { + return false; + } + if (mediaType != null) { + return mediaType.includes(MediaType.APPLICATION_FORM_URLENCODED) || + mediaType.includes(MediaType.MULTIPART_FORM_DATA); + } + else { + return true; + } + } + + public List getSupportedMediaTypes() { + return Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED, MediaType.MULTIPART_FORM_DATA); + } + + public MultiValueMap read(Class> clazz, + HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { MediaType contentType = inputMessage.getHeaders().getContentType(); - Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : DEFAULT_CHARSET; + Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : this.charset; String body = FileCopyUtils.copyToString(new InputStreamReader(inputMessage.getBody(), charset)); String[] pairs = StringUtils.tokenizeToStringArray(body, "&"); @@ -86,17 +174,39 @@ public class FormHttpMessageConverter extends AbstractHttpMessageConverter form, HttpOutputMessage outputMessage) - throws IOException { - MediaType contentType = getDefaultContentType(form); - Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : DEFAULT_CHARSET; + @SuppressWarnings("unchecked") + public void write(MultiValueMap map, MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + if (!isMultipart(map, contentType)) { + writeForm((MultiValueMap) map, outputMessage); + } + else { + writeMultipart((MultiValueMap) map, outputMessage); + } + } + + private boolean isMultipart(MultiValueMap map, MediaType contentType) { + if (contentType != null) { + return MediaType.MULTIPART_FORM_DATA.equals(contentType); + } + for (String name : map.keySet()) { + for (Object value : map.get(name)) { + if (value != null && !(value instanceof String)) { + return true; + } + } + } + return false; + } + + private void writeForm(MultiValueMap form, HttpOutputMessage outputMessage) throws IOException { + + outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + StringBuilder builder = new StringBuilder(); - for (Iterator>> entryIterator = form.entrySet().iterator(); - entryIterator.hasNext();) { - Map.Entry> entry = entryIterator.next(); - String name = entry.getKey(); - for (Iterator valueIterator = entry.getValue().iterator(); valueIterator.hasNext();) { + for (Iterator nameIterator = form.keySet().iterator(); nameIterator.hasNext();) { + String name = nameIterator.next(); + for (Iterator valueIterator = form.get(name).iterator(); valueIterator.hasNext();) { String value = valueIterator.next(); builder.append(URLEncoder.encode(name, charset.name())); if (value != null) { @@ -107,11 +217,158 @@ public class FormHttpMessageConverter extends AbstractHttpMessageConverter parts, HttpOutputMessage outputMessage) + throws IOException { + byte[] boundary = generateMultipartBoundary(); + + Map parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII")); + MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters); + outputMessage.getHeaders().setContentType(contentType); + + writeParts(outputMessage.getBody(), parts, boundary); + writeEnd(boundary, outputMessage.getBody()); + } + + private void writeParts(OutputStream os, MultiValueMap parts, byte[] boundary) throws IOException { + for (Map.Entry> entry : parts.entrySet()) { + String name = entry.getKey(); + for (Object part : entry.getValue()) { + writeBoundary(boundary, os); + writePart(name, part, os); + writeNewLine(os); + } + } + } + + private void writeBoundary(byte[] boundary, OutputStream os) throws IOException { + os.write('-'); + os.write('-'); + os.write(boundary); + writeNewLine(os); + } + + @SuppressWarnings("unchecked") + private void writePart(String name, Object part, OutputStream os) throws IOException { + Class partType = part.getClass(); + for (HttpMessageConverter messageConverter : partConverters) { + if (messageConverter.canWrite(partType, null)) { + HttpOutputMessage multipartOutputMessage = new MultipartHttpOutputMessage(os); + multipartOutputMessage.getHeaders().setContentDispositionFormData(name, getFilename(part)); + messageConverter.write(part, null, multipartOutputMessage); + return; + } + } + throw new HttpMessageNotWritableException( + "Could not write request: no suitable HttpMessageConverter found for request type [" + + partType.getName() + "]"); + } + + private void writeEnd(byte[] boundary, OutputStream os) throws IOException { + os.write('-'); + os.write('-'); + os.write(boundary); + os.write('-'); + os.write('-'); + writeNewLine(os); + } + + private void writeNewLine(OutputStream os) throws IOException { + os.write('\r'); + os.write('\n'); + } + + /** + * Generate a multipart boundary. + * + *

Default implementation returns a random boundary. Can be overridden in subclasses. + */ + protected byte[] generateMultipartBoundary() { + 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; + } + + /** + * Returns the filename of the given multipart part. This value will be used for the {@code Content-Disposition} header. + * + *

Default implementation returns {@link Resource#getFilename()} if the part is a {@code Resource}, and + * {@code null} in other cases. Can be overridden in subclasses. + * + * @param part the part to determine the file name for + * @return the filename, or {@code null} if not known + */ + protected String getFilename(Object part) { + if (part instanceof Resource) { + Resource resource = (Resource) part; + return resource.getFilename(); + } + else { + return null; + } + } + + /** + * Implementation of {@link org.springframework.http.HttpOutputMessage} used for writing multipart data. + */ + + private class MultipartHttpOutputMessage implements HttpOutputMessage { + + private final HttpHeaders headers = new HttpHeaders(); + + private final OutputStream os; + + private boolean headersWritten = false; + + public MultipartHttpOutputMessage(OutputStream os) { + this.os = os; + } + + public HttpHeaders getHeaders() { + return headersWritten ? HttpHeaders.readOnlyHttpHeaders(headers) : this.headers; + } + + public OutputStream getBody() throws IOException { + writeHeaders(); + return this.os; + } + + private void writeHeaders() throws IOException { + if (!this.headersWritten) { + for (Map.Entry> entry : this.headers.entrySet()) { + byte[] headerName = getAsciiBytes(entry.getKey()); + for (String headerValueString : entry.getValue()) { + byte[] headerValue = getAsciiBytes(headerValueString); + os.write(headerName); + os.write(':'); + os.write(' '); + os.write(headerValue); + writeNewLine(os); + } + } + writeNewLine(os); + this.headersWritten = true; + } + } + + 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); + } + } + + + } } 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 deleted file mode 100644 index 8caabe740ca..00000000000 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * 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 java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Random; - -import org.springframework.core.io.Resource; -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.ByteArrayHttpMessageConverter; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.http.converter.ResourceHttpMessageConverter; -import org.springframework.http.converter.StringHttpMessageConverter; -import org.springframework.http.converter.xml.SourceHttpMessageConverter; -import org.springframework.util.Assert; - -/** - * Implementation of {@link org.springframework.http.converter.HttpMessageConverter} that can write multipart form data - * (i.e. file uploads). - * - *

This converter writes the media type ({@code multipart/form-data}). Multipart form data is provided as - * a {@link MultipartMap}. - * - *

Inspired by {@link org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}. - * - * @author Arjen Poutsma - * @see MultipartMap - * @since 3.0.2 - */ -public class MultipartHttpMessageConverter implements HttpMessageConverter { - - 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(); - - private List> partConverters = new ArrayList>(); - - public MultipartHttpMessageConverter() { - this.partConverters.add(new ByteArrayHttpMessageConverter()); - StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); - stringHttpMessageConverter.setWriteAcceptCharset(false); - this.partConverters.add(stringHttpMessageConverter); - this.partConverters.add(new ResourceHttpMessageConverter()); - this.partConverters.add(new SourceHttpMessageConverter()); - } - - /** - * Set the message body converters to use. These converters are used to convert to MIME parts. - */ - public void setPartConverters(List> partConverters) { - Assert.notEmpty(partConverters, "'messageConverters' must not be empty"); - this.partConverters = partConverters; - } - - /** - * Returns {@code false}, as reading multipart data is not supported. - */ - public boolean canRead(Class clazz, MediaType mediaType) { - return false; - } - - public boolean canWrite(Class clazz, MediaType mediaType) { - if (!MultipartMap.class.isAssignableFrom(clazz)) { - return false; - } - if (mediaType != null) { - return mediaType.includes(MediaType.MULTIPART_FORM_DATA); - } else { - return true; - } - } - - public List getSupportedMediaTypes() { - return Collections.singletonList(MediaType.MULTIPART_FORM_DATA); - } - - public MultipartMap read(Class clazz, HttpInputMessage inputMessage) - throws IOException, HttpMessageNotReadableException { - throw new UnsupportedOperationException(); - } - - public void write(MultipartMap map, MediaType contentType, HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { - - byte[] boundary = generateBoundary(); - - HttpHeaders headers = outputMessage.getHeaders(); - OutputStream os = outputMessage.getBody(); - - setContentType(headers, boundary); - writeParts(os, map, boundary); - writeEnd(boundary, os); - } - - private void setContentType(HttpHeaders headers, byte[] boundary) throws UnsupportedEncodingException { - Map parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII")); - MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters); - headers.setContentType(contentType); - } - - private void writeParts(OutputStream os, MultipartMap map, byte[] boundary) - throws IOException { - for (Map.Entry> entry : map.entrySet()) { - String name = entry.getKey(); - for (Object part : entry.getValue()) { - writeBoundary(boundary, os); - writePart(name, part, os); - writeNewLine(os); - } - } - } - - private void writeBoundary(byte[] boundary, OutputStream os) throws IOException { - os.write('-'); - os.write('-'); - os.write(boundary); - writeNewLine(os); - } - - @SuppressWarnings("unchecked") - private void writePart(String name, Object part, OutputStream os) throws IOException { - Class partType = part.getClass(); - for (HttpMessageConverter messageConverter : partConverters) { - if (messageConverter.canWrite(partType, null)) { - HttpOutputMessage multipartOutputMessage = new MultipartHttpOutputMessage(os); - multipartOutputMessage.getHeaders().setContentDispositionFormData(name, getFileName(part)); - messageConverter.write(part, null, multipartOutputMessage); - return; - } - } - throw new HttpMessageNotWritableException( - "Could not write request: no suitable HttpMessageConverter found for request type [" + - partType.getName() + "]"); - } - - protected String getFileName(Object part) { - if (part instanceof Resource) { - Resource resource = (Resource) part; - return resource.getFilename(); - } - else { - return null; - } - } - - private void writeEnd(byte[] boundary, OutputStream os) throws IOException { - os.write('-'); - os.write('-'); - os.write(boundary); - os.write('-'); - os.write('-'); - writeNewLine(os); - } - - private void writeNewLine(OutputStream os) throws IOException { - 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; - } - -} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpOutputMessage.java b/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpOutputMessage.java deleted file mode 100644 index 31bdc8b799a..00000000000 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpOutputMessage.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 java.util.List; -import java.util.Map; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpOutputMessage; - -/** - * Implementation of {@link HttpOutputMessage} used for writing multipart data. - * - * @author Arjen Poutsma - * @since 3.0.2 - */ -class MultipartHttpOutputMessage implements HttpOutputMessage { - - private final HttpHeaders headers = new HttpHeaders(); - - private final OutputStream os; - - private boolean headersWritten = false; - - public MultipartHttpOutputMessage(OutputStream os) { - this.os = os; - } - - public HttpHeaders getHeaders() { - return headersWritten ? HttpHeaders.readOnlyHttpHeaders(headers) : this.headers; - } - - public OutputStream getBody() throws IOException { - writeHeaders(); - return this.os; - } - - private void writeHeaders() throws IOException { - if (!this.headersWritten) { - for (Map.Entry> entry : this.headers.entrySet()) { - byte[] headerName = getAsciiBytes(entry.getKey()); - for (String headerValueString : entry.getValue()) { - byte[] headerValue = getAsciiBytes(headerValueString); - os.write(headerName); - os.write(':'); - os.write(' '); - os.write(headerValue); - writeNewLine(os); - } - } - writeNewLine(os); - this.headersWritten = true; - } - } - - private void writeNewLine(OutputStream os) throws IOException { - os.write('\r'); - os.write('\n'); - } - - 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); - } - } - - -} 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 deleted file mode 100644 index ed2c889848c..00000000000 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/MultipartMap.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 org.springframework.util.LinkedMultiValueMap; - -/** - * Represents HTTP multipart form data, mapping names to parts. - * - *

In addition to the normal methods defined by {@link org.springframework.util.MultiValueMap}, this class offers - * the following convenience methods: - *

- * - * @author Arjen Poutsma - * @since 3.0.2 - */ -public class MultipartMap extends LinkedMultiValueMap { - -} 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 deleted file mode 100644 index fd9ca91e344..00000000000 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/multipart/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 bb46d53c69d..72342e1aebd 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 @@ -40,7 +40,6 @@ import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; 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; @@ -130,7 +129,6 @@ public class RestTemplate extends HttpAccessor implements RestOperations { this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); this.messageConverters.add(new ResourceHttpMessageConverter()); - 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/FormHttpMessageConverterTests.java b/org.springframework.web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java index af7f7a0e03e..ab8999b380a 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.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. @@ -16,14 +16,26 @@ package org.springframework.http.converter; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; import java.nio.charset.Charset; import java.util.List; +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; +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.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; @@ -41,29 +53,30 @@ public class FormHttpMessageConverterTests { } @Test - @SuppressWarnings("unchecked") public void canRead() { assertTrue(converter.canRead(MultiValueMap.class, new MediaType("application", "x-www-form-urlencoded"))); + assertFalse(converter.canRead(MultiValueMap.class, new MediaType("multipart","form-data"))); } @Test - @SuppressWarnings("unchecked") public void canWrite() { assertTrue(converter.canWrite(MultiValueMap.class, new MediaType("application", "x-www-form-urlencoded"))); + assertTrue(converter.canWrite(MultiValueMap.class, new MediaType("multipart","form-data"))); assertTrue(converter.canWrite(MultiValueMap.class, MediaType.ALL)); } - @SuppressWarnings("unchecked") @Test - public void read() throws Exception { + @SuppressWarnings("unchecked") + public void readForm() throws Exception { String body = "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3"; Charset iso88591 = Charset.forName("ISO-8859-1"); MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(iso88591)); inputMessage.getHeaders().setContentType(new MediaType("application", "x-www-form-urlencoded", iso88591)); - MultiValueMap result = converter.read(null, inputMessage); + MultiValueMap result = (MultiValueMap) converter.read(null, inputMessage); + assertEquals("Invalid result", 3, result.size()); assertEquals("Invalid result", "value 1", result.getFirst("name 1")); - List values = (List) result.get("name 2"); + List values = result.get("name 2"); assertEquals("Invalid result", 2, values.size()); assertEquals("Invalid result", "value 2+1", values.get(0)); assertEquals("Invalid result", "value 2+2", values.get(1)); @@ -71,19 +84,95 @@ public class FormHttpMessageConverterTests { } @Test - public void write() throws IOException { + public void writeForm() throws IOException { MultiValueMap body = new LinkedMultiValueMap(); body.set("name 1", "value 1"); body.add("name 2", "value 2+1"); body.add("name 2", "value 2+2"); body.add("name 3", null); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - converter.write(body, null, outputMessage); + converter.write(body, MediaType.APPLICATION_FORM_URLENCODED, outputMessage); Charset iso88591 = Charset.forName("ISO-8859-1"); assertEquals("Invalid result", "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3", outputMessage.getBodyAsString(iso88591)); assertEquals("Invalid content-type", new MediaType("application", "x-www-form-urlencoded"), outputMessage.getHeaders().getContentType()); } + + @Test + public void writeMultipart() throws Exception { + MultiValueMap parts = new LinkedMultiValueMap(); + parts.add("name 1", "value 1"); + parts.add("name 2", "value 2+1"); + parts.add("name 2", "value 2+2"); + Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); + parts.add("logo", logo); + Source xml = new StreamSource(new StringReader("")); + parts.add("xml", xml); + + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(parts, MediaType.MULTIPART_FORM_DATA, outputMessage); + + final MediaType contentType = outputMessage.getHeaders().getContentType(); + 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 MockHttpOutputMessageRequestContext(outputMessage)); + 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("image/jpeg", item.getContentType()); + assertEquals(logo.getFile().length(), item.getSize()); + + item = (FileItem) items.get(4); + assertEquals("xml", item.getFieldName()); + assertEquals("application/xml", item.getContentType()); + } + + private static class MockHttpOutputMessageRequestContext implements RequestContext { + private final MockHttpOutputMessage outputMessage; + + private MockHttpOutputMessageRequestContext(MockHttpOutputMessage outputMessage) { + this.outputMessage = outputMessage; + } + + public String getCharacterEncoding() { + MediaType contentType = outputMessage.getHeaders().getContentType(); + return contentType != null && contentType.getCharSet() != null ? contentType.getCharSet().name() : null; + } + + public String getContentType() { + MediaType contentType = outputMessage.getHeaders().getContentType(); + return contentType != null ? contentType.toString() : null; + } + + public int getContentLength() { + return outputMessage.getBodyAsBytes().length; + } + + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(outputMessage.getBodyAsBytes()); + } + } + } 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 deleted file mode 100644 index 7185ebe2063..00000000000 --- a/org.springframework.web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTest.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.io.StringReader; -import java.util.List; -import javax.xml.transform.Source; -import javax.xml.transform.stream.StreamSource; - -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.add("name 1", "value 1"); - body.add("name 2", "value 2+1"); - body.add("name 2", "value 2+2"); - Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); - body.add("logo", logo); - Source xml = new StreamSource(new StringReader("")); - body.add("xml", xml); - - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - converter.write(body, null, outputMessage); - final MediaType contentType = outputMessage.getHeaders().getContentType(); - final byte[] result = outputMessage.getBodyAsBytes(); - System.out.println(new String(result)); - 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("image/jpeg", item.getContentType()); - assertEquals(logo.getFile().length(), item.getSize()); - - item = (FileItem) items.get(4); - assertEquals("xml", item.getFieldName()); - assertEquals("application/xml", item.getContentType()); - } - - -} 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 3061a062b8a..e04604381a1 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 @@ -49,8 +49,9 @@ 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; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** @author Arjen Poutsma */ public class RestTemplateIntegrationTests { @@ -145,14 +146,14 @@ public class RestTemplateIntegrationTests { @Test public void multipart() throws UnsupportedEncodingException { - MultipartMap body = new MultipartMap(); - body.add("name 1", "value 1"); - body.add("name 2", "value 2+1"); - body.add("name 2", "value 2+2"); + MultiValueMap parts = new LinkedMultiValueMap(); + parts.add("name 1", "value 1"); + parts.add("name 2", "value 2+1"); + parts.add("name 2", "value 2+2"); Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); - body.add("logo", logo); + parts.add("logo", logo); - template.postForLocation(URI + "/multipart", body); + template.postForLocation(URI + "/multipart", parts); } /** Servlet that returns and error message for a given status code. */