From b2037c8316fb7d2f49ce4efc213347464137ff23 Mon Sep 17 00:00:00 2001 From: Dmitry Katsubo Date: Wed, 29 Aug 2012 18:22:03 +0200 Subject: [PATCH] Add ObjectToStringHttpMessageConverter This new converter uses StringHttpMessageConverter internally combined with a ConversionService for converting String content to and from the target object type. Issue: SPR-9738 --- .../ObjectToStringHttpMessageConverter.java | 118 ++++++++++++ .../converter/StringHttpMessageConverter.java | 4 +- ...jectToStringHttpMessageConverterTests.java | 182 ++++++++++++++++++ 3 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java create mode 100644 spring-web/src/test/java/org/springframework/http/converter/ObjectToStringHttpMessageConverterTests.java diff --git a/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java new file mode 100644 index 00000000000..a5e644b9bdd --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2012 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; + +import java.io.IOException; +import java.nio.charset.Charset; + +import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; + +/** + * An {@code HttpMessageConverter} that uses {@link StringHttpMessageConverter} + * for reading and writing content and a {@link ConversionService} for converting + * the String content to and from the target object type. + *

+ * By default, this converter supports the media type {@code text/plain} only. + * This can be overridden by setting the + * {@link #setSupportedMediaTypes supportedMediaTypes} property. + * Example of usage: + * + *

+ * <bean class="org.springframework.http.converter.ObjectToStringHttpMessageConverter">
+ *   <constructor-arg>
+ *     <bean class="org.springframework.context.support.ConversionServiceFactoryBean"/>
+ *   </constructor-arg>
+ * </bean>
+ * 
+ * + * @author Dmitry Katsubo + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConverter { + + public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); + + private ConversionService conversionService; + + private StringHttpMessageConverter stringHttpMessageConverter; + + + /** + * A constructor accepting a {@code ConversionService} to use to convert the + * (String) message body to/from the target class type. + * @param conversionService the conversion service + */ + public ObjectToStringHttpMessageConverter(ConversionService conversionService) { + this(conversionService, DEFAULT_CHARSET); + } + + public ObjectToStringHttpMessageConverter(ConversionService conversionService, Charset defaultCharset) { + super(new MediaType("text", "plain", defaultCharset)); + + Assert.notNull(conversionService, "conversionService is required"); + this.conversionService = conversionService; + this.stringHttpMessageConverter = new StringHttpMessageConverter(defaultCharset); + } + + /** + * Indicates whether the {@code Accept-Charset} should be written to any outgoing request. + *

Default is {@code true}. + */ + public void setWriteAcceptCharset(boolean writeAcceptCharset) { + this.stringHttpMessageConverter.setWriteAcceptCharset(writeAcceptCharset); + } + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return this.conversionService.canConvert(String.class, clazz) && canRead(mediaType); + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return this.conversionService.canConvert(clazz, String.class) && canWrite(mediaType); + } + + @Override + protected boolean supports(Class clazz) { + // should not be called, since we override canRead/Write + throw new UnsupportedOperationException(); + } + + @Override + protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { + String value = this.stringHttpMessageConverter.readInternal(String.class, inputMessage); + return this.conversionService.convert(value, clazz); + } + + @Override + protected void writeInternal(Object obj, HttpOutputMessage outputMessage) throws IOException { + String s = this.conversionService.convert(obj, String.class); + this.stringHttpMessageConverter.writeInternal(s, outputMessage); + } + + @Override + protected Long getContentLength(Object obj, MediaType contentType) { + String value = this.conversionService.convert(obj, String.class); + return this.stringHttpMessageConverter.getContentLength(value, contentType); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java index 3e59116b039..c037c1fcc93 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java @@ -76,14 +76,13 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter clazz) { return String.class.equals(clazz); } @Override - protected String readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { + protected String readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType()); return FileCopyUtils.copyToString(new InputStreamReader(inputMessage.getBody(), charset)); } @@ -126,5 +125,4 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverterDmitry Katsubo + * @author Rossen Stoyanchev + */ +public class ObjectToStringHttpMessageConverterTests { + + private ObjectToStringHttpMessageConverter converter; + + private MockHttpServletResponse servletResponse; + + private ServletServerHttpResponse response; + + + @Before + public void setUp() { + ConversionService conversionService = new DefaultConversionService(); + this.converter = new ObjectToStringHttpMessageConverter(conversionService); + + this.servletResponse = new MockHttpServletResponse(); + this.response = new ServletServerHttpResponse(this.servletResponse); + } + + @Test + public void canRead() { + assertFalse(this.converter.canRead(Math.class, null)); + assertFalse(this.converter.canRead(Resource.class, null)); + + assertTrue(this.converter.canRead(Locale.class, null)); + assertTrue(this.converter.canRead(BigInteger.class, null)); + + assertFalse(this.converter.canRead(BigInteger.class, MediaType.TEXT_HTML)); + assertFalse(this.converter.canRead(BigInteger.class, MediaType.TEXT_XML)); + assertFalse(this.converter.canRead(BigInteger.class, MediaType.APPLICATION_XML)); + } + + @Test + public void canWrite() { + assertFalse(this.converter.canWrite(Math.class, null)); + assertFalse(this.converter.canWrite(Resource.class, null)); + + assertTrue(this.converter.canWrite(Locale.class, null)); + assertTrue(this.converter.canWrite(Double.class, null)); + + assertFalse(this.converter.canWrite(BigInteger.class, MediaType.TEXT_HTML)); + assertFalse(this.converter.canWrite(BigInteger.class, MediaType.TEXT_XML)); + assertFalse(this.converter.canWrite(BigInteger.class, MediaType.APPLICATION_XML)); + + assertTrue(this.converter.canWrite(BigInteger.class, MediaType.valueOf("text/*"))); + } + + @Test + public void defaultCharset() throws IOException { + this.converter.write(Integer.valueOf(5), null, response); + + assertEquals("ISO-8859-1", servletResponse.getCharacterEncoding()); + } + + @Test + public void defaultCharsetModified() throws IOException { + Charset charset = Charset.forName("UTF-16"); + ConversionService cs = new DefaultConversionService(); + ObjectToStringHttpMessageConverter converter = new ObjectToStringHttpMessageConverter(cs, charset); + converter.write((byte) 31, null, this.response); + + assertEquals("UTF-16", this.servletResponse.getCharacterEncoding()); + } + + @Test + public void writeAcceptCharset() throws IOException { + this.converter.write(new Date(), null, this.response); + + assertNotNull(this.servletResponse.getHeader("Accept-Charset")); + } + + @Test + public void writeAcceptCharsetTurnedOff() throws IOException { + this.converter.setWriteAcceptCharset(false); + this.converter.write(new Date(), null, this.response); + + assertNull(this.servletResponse.getHeader("Accept-Charset")); + } + + @Test + public void read() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest(); + + request.setContentType(MediaType.TEXT_PLAIN_VALUE); + + Short shortValue = Short.valueOf((short) 781); + + request.setContent(shortValue.toString().getBytes( + StringHttpMessageConverter.DEFAULT_CHARSET)); + + assertEquals(shortValue, this.converter.read(Short.class, new ServletServerHttpRequest(request))); + + Float floatValue = Float.valueOf(123); + + request.setCharacterEncoding("UTF-16"); + request.setContent(floatValue.toString().getBytes("UTF-16")); + + assertEquals(floatValue, this.converter.read(Float.class, new ServletServerHttpRequest(request))); + + Long longValue = Long.valueOf(55819182821331L); + + request.setCharacterEncoding("UTF-8"); + request.setContent(longValue.toString().getBytes("UTF-8")); + + assertEquals(longValue, this.converter.read(Long.class, new ServletServerHttpRequest(request))); + } + + @Test + public void write() throws IOException { + this.converter.write((byte) -8, null, this.response); + + assertEquals("ISO-8859-1", this.servletResponse.getCharacterEncoding()); + assertTrue(this.servletResponse.getContentType().startsWith(MediaType.TEXT_PLAIN_VALUE)); + assertEquals(2, this.servletResponse.getContentLength()); + assertArrayEquals(new byte[] { '-', '8' }, this.servletResponse.getContentAsByteArray()); + } + + @Test + public void writeUtf16() throws IOException { + MediaType contentType = new MediaType("text", "plain", Charset.forName("UTF-16")); + this.converter.write(Integer.valueOf(958), contentType, this.response); + + assertEquals("UTF-16", this.servletResponse.getCharacterEncoding()); + assertTrue(this.servletResponse.getContentType().startsWith(MediaType.TEXT_PLAIN_VALUE)); + assertEquals(8, this.servletResponse.getContentLength()); + // First two bytes: byte order mark + assertArrayEquals(new byte[] { -2, -1, 0, '9', 0, '5', 0, '8' }, this.servletResponse.getContentAsByteArray()); + } + + @Test(expected = IllegalArgumentException.class) + public void testConversionServiceRequired() { + new ObjectToStringHttpMessageConverter(null); + } + +}