diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java index e009793f77..895f8f2a51 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java @@ -96,7 +96,7 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, public EndpointHandlerAdapter endpointHandlerAdapter( final HttpMessageConverters messageConverters) { EndpointHandlerAdapter adapter = new EndpointHandlerAdapter(); - adapter.setMessageConverters(messageConverters.getMessageConverters()); + adapter.setMessageConverters(messageConverters.getConverters()); return adapter; } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConverters.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConverters.java index 275e8f0486..8aad3563a5 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConverters.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConverters.java @@ -17,95 +17,141 @@ package org.springframework.boot.autoconfigure.web; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; +import java.util.Iterator; import java.util.List; +import org.springframework.beans.factory.InitializingBean; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.xml.AbstractXmlHttpMessageConverter; +import org.springframework.util.Assert; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; /** - * Convenient utility for adding and merging additional {@link HttpMessageConverter} in an - * application context. It also modifies the default converters a bit (putting XML - * converters at the back of the list if they are present). + * Bean used to manage the {@link HttpMessageConverter}s used in a Spring Boot + * application. Provides a convenient way to add and merge additional + * {@link HttpMessageConverter}s to a web application. + *

+ * An instance of this bean can be registered with specific + * {@link #HttpMessageConverters(HttpMessageConverter...) additional converters} if + * needed, otherwise default converters will be used. + *

+ * NOTE: The default converters used are the same as standard Spring MVC (see + * {@link WebMvcConfigurationSupport#getMessageConverters} with some slight re-ordering to + * put XML converters at the back of the list. * * @author Dave Syer + * @author Phillip Webb + * @see #HttpMessageConverters(HttpMessageConverter...) + * @see #HttpMessageConverters(Collection) + * @see #getConverters() + * @see #setConverters(List) */ -public class HttpMessageConverters { - - private List> defaults; - - private List> overrides; - - private Object lock = new Object(); +public class HttpMessageConverters implements Iterable>, + InitializingBean { private List> converters; - public HttpMessageConverters() { - this(Collections.> emptyList()); + private boolean initialized; + + /** + * Create a new {@link HttpMessageConverters} instance with the specified additional + * converters. + * @param additionalConverters additional converters to be added. New converters will + * be added to the front of the list, overrides will replace existing items without + * changing the order. The {@link #getConverters()} methods can be used for further + * converter manipulation. + */ + public HttpMessageConverters(HttpMessageConverter... additionalConverters) { + this(Arrays.asList(additionalConverters)); } - public HttpMessageConverters(Collection> overrides) { - this.overrides = new ArrayList>(overrides); - } - - public List> getMessageConverters() { - if (this.converters == null) { - synchronized (this.lock) { - if (this.converters == null) { - getDefaultMessageConverters(); // ensure they are available - Collection> fallbacks = new LinkedHashSet>(); - for (HttpMessageConverter fallback : this.defaults) { - boolean overridden = false; - for (HttpMessageConverter converter : this.overrides) { - if (fallback.getClass() - .isAssignableFrom(converter.getClass())) { - if (!fallbacks.contains(converter)) { - fallbacks.add(converter); - overridden = true; - } - } - } - if (!overridden) { - fallbacks.add(fallback); - } - } - Collection> converters = new LinkedHashSet>( - this.overrides); - converters.addAll(fallbacks); - this.converters = new ArrayList>(converters); - } + /** + * Create a new {@link HttpMessageConverters} instance with the specified additional + * converters. + * @param additionalConverters additional converters to be added. New converters will + * be added to the front of the list, overrides will replace existing items without + * changing the order. The {@link #getConverters()} methods can be used for further + * converter manipulation. + */ + public HttpMessageConverters(Collection> additionalConverters) { + this.converters = new ArrayList>(); + List> defaultConverters = getDefaultConverters(); + for (HttpMessageConverter converter : additionalConverters) { + int defaultConverterIndex = indexOfItemClass(defaultConverters, converter); + if (defaultConverterIndex == -1) { + this.converters.add(converter); + } + else { + defaultConverters.set(defaultConverterIndex, converter); } } + this.converters.addAll(defaultConverters); + } + + private List> getDefaultConverters() { + List> converters = new ArrayList>(); + converters.addAll(new WebMvcConfigurationSupport() { + public List> defaultMessageConverters() { + return super.getMessageConverters(); + } + }.defaultMessageConverters()); + reorderXmlConvertersToEnd(converters); + return converters; + } + + private void reorderXmlConvertersToEnd(List> converters) { + List> xml = new ArrayList>(); + for (Iterator> iterator = converters.iterator(); iterator + .hasNext();) { + HttpMessageConverter converter = iterator.next(); + if (converter instanceof AbstractXmlHttpMessageConverter) { + xml.add(converter); + iterator.remove(); + } + } + converters.addAll(xml); + } + + private int indexOfItemClass(List list, E item) { + Class itemClass = item.getClass(); + for (int i = 0; i < list.size(); i++) { + if (list.get(i).getClass().isAssignableFrom(itemClass)) { + return i; + } + } + return -1; + } + + @Override + public Iterator> iterator() { + return getConverters().iterator(); + } + + /** + * Return a mutable list of the converters in the order that they will be registered. + * Values in the list cannot be modified once the bean has been initialized. + * @return the converters + */ + public List> getConverters() { return this.converters; } - public List> getDefaultMessageConverters() { - if (this.defaults == null) { - synchronized (this.lock) { - if (this.defaults == null) { - this.defaults = new ArrayList>(); - this.defaults.addAll(new WebMvcConfigurationSupport() { - public List> defaultMessageConverters() { - return super.getMessageConverters(); - } - }.defaultMessageConverters()); - List> xmls = new ArrayList>(); - for (HttpMessageConverter converter : this.defaults) { - // Shift XML converters to the back of the list so they only get - // used if nothing else works... - if (converter instanceof AbstractXmlHttpMessageConverter) { - xmls.add(converter); - } - } - this.defaults.removeAll(xmls); - this.defaults.addAll(xmls); - } - } - } - return Collections.unmodifiableList(this.defaults); + /** + * Set the converters to use, replacing any existing values. This method can only be + * called before the bean has been initialized. + * @param converters the converters to set + */ + public void setConverters(List> converters) { + Assert.state(!this.initialized, "Unable to set converters once initialized"); + this.converters = converters; } -} \ No newline at end of file + @Override + public void afterPropertiesSet() throws Exception { + this.initialized = true; + this.converters = Collections.unmodifiableList(this.converters); + } +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java index 4f0b052d9d..5599fcb224 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java @@ -133,7 +133,7 @@ public class WebMvcAutoConfiguration { @Override public void configureMessageConverters(List> converters) { - converters.addAll(this.messageConverters.getMessageConverters()); + converters.addAll(this.messageConverters.getConverters()); } @Bean diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java index 4ee137dbcd..46af4b2f95 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java @@ -42,6 +42,8 @@ import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.verify; /** + * Tests for {@link HttpMessageConvertersAutoConfiguration}. + * * @author Dave Syer */ public class HttpMessageConvertersAutoConfigurationTests { @@ -67,7 +69,7 @@ public class HttpMessageConvertersAutoConfigurationTests { converter.getObjectMapper()); HttpMessageConverters converters = this.context .getBean(HttpMessageConverters.class); - assertTrue(converters.getMessageConverters().contains(converter)); + assertTrue(converters.getConverters().contains(converter)); } @Test diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersTests.java index 2c760c630f..86472e49cf 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersTests.java @@ -16,43 +16,96 @@ package org.springframework.boot.autoconfigure.web; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import org.junit.Rule; import org.junit.Test; -import org.mockito.Mockito; +import org.junit.rules.ExpectedException; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.ResourceHttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.http.converter.xml.SourceHttpMessageConverter; +import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; /** * @author Dave Syer + * @author Phillip Webb */ public class HttpMessageConvertersTests { + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Test - public void defaultsCreated() { - HttpMessageConverters messageConverters = new HttpMessageConverters(); - assertFalse(messageConverters.getDefaultMessageConverters().isEmpty()); + public void containsDefaults() throws Exception { + HttpMessageConverters converters = new HttpMessageConverters(); + List> converterClasses = new ArrayList>(); + for (HttpMessageConverter converter : converters) { + converterClasses.add(converter.getClass()); + } + assertThat(converterClasses, equalTo(Arrays.> asList( + ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class, + ResourceHttpMessageConverter.class, SourceHttpMessageConverter.class, + AllEncompassingFormHttpMessageConverter.class, + MappingJackson2HttpMessageConverter.class, + Jaxb2RootElementHttpMessageConverter.class))); + } + + @Test + public void canModifyBeforeInitialize() throws Exception { + HttpMessageConverters converters = new HttpMessageConverters(); + HttpMessageConverter converter = mock(HttpMessageConverter.class); + converters.getConverters().add(converter); + assertThat(converters.getConverters().contains(converter), equalTo(true)); + } + + @Test + public void cannotModifyAfterInitialize() throws Exception { + HttpMessageConverters converters = new HttpMessageConverters(); + converters.afterPropertiesSet(); + this.thrown.expect(UnsupportedOperationException.class); + converters.getConverters().add(mock(HttpMessageConverter.class)); + } + + @Test + public void canSetBeforeInitialize() throws Exception { + HttpMessageConverters converters = new HttpMessageConverters(); + converters.setConverters(new ArrayList>()); + assertThat(converters.getConverters().size(), equalTo(0)); + } + + @Test + public void cannotSetAfterInitailzie() throws Exception { + HttpMessageConverters converters = new HttpMessageConverters(); + converters.afterPropertiesSet(); + this.thrown.expect(IllegalStateException.class); + converters.setConverters(new ArrayList>()); } @Test public void overrideExistingConverter() { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); - HttpMessageConverters messageConverters = new HttpMessageConverters( - Arrays.> asList(converter)); - assertTrue(messageConverters.getMessageConverters().contains(converter)); + HttpMessageConverters converters = new HttpMessageConverters(converter); + assertTrue(converters.getConverters().contains(converter)); } @Test - public void addNewOne() { - HttpMessageConverter converter = Mockito.mock(HttpMessageConverter.class); - HttpMessageConverters messageConverters = new HttpMessageConverters( - Arrays.> asList(converter)); - assertTrue(messageConverters.getMessageConverters().contains(converter)); - assertEquals(converter, messageConverters.getMessageConverters().get(0)); + public void addNewConverter() { + HttpMessageConverter converter = mock(HttpMessageConverter.class); + HttpMessageConverters converters = new HttpMessageConverters(converter); + assertTrue(converters.getConverters().contains(converter)); + assertEquals(converter, converters.getConverters().get(0)); } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java index f3b33a073a..1a57aeb621 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java @@ -83,8 +83,7 @@ public class WebMvcAutoConfigurationTests { assertEquals(3, this.context.getBeanNamesForType(HandlerAdapter.class).length); assertFalse(this.context.getBean(RequestMappingHandlerAdapter.class) .getMessageConverters().isEmpty()); - assertEquals(this.context.getBean(HttpMessageConverters.class) - .getMessageConverters(), + assertEquals(this.context.getBean(HttpMessageConverters.class).getConverters(), this.context.getBean(RequestMappingHandlerAdapter.class) .getMessageConverters()); }