diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfiguration.java new file mode 100644 index 000000000000..5449c05af825 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfiguration.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.websocket; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.converter.DefaultContentTypeResolver; +import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.converter.MessageConverter; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for WebSocket-based messaging. + * + * @author Andy Wilkinson + * @since 1.3.0 + */ +@ConditionalOnWebApplication +@ConditionalOnClass(WebSocketMessageBrokerConfigurer.class) +@AutoConfigureAfter(JacksonAutoConfiguration.class) +public class WebSocketMessagingAutoConfiguration { + + @Configuration + @ConditionalOnBean({ DelegatingWebSocketMessageBrokerConfiguration.class, + ObjectMapper.class }) + @ConditionalOnClass(ObjectMapper.class) + static class WebSocketMessageConverterConfiguration extends + AbstractWebSocketMessageBrokerConfigurer { + + @Autowired + private ObjectMapper objectMapper; + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // The user must register their own endpoints + } + + @Override + public boolean configureMessageConverters(List messageConverters) { + MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); + converter.setObjectMapper(this.objectMapper); + DefaultContentTypeResolver resolver = new DefaultContentTypeResolver(); + resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON); + converter.setContentTypeResolver(resolver); + messageConverters.add(converter); + return true; + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 79bb6b264671..9daae3512490 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -82,7 +82,8 @@ org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguratio org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,\ org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration,\ org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,\ -org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration +org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration,\ +org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration # Template availability providers org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfigurationTests.java new file mode 100644 index 000000000000..54dbf471faf6 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfigurationTests.java @@ -0,0 +1,230 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.websocket; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; +import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; +import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.web.ServerPortInfoApplicationContextInitializer; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.converter.SimpleMessageConverter; +import org.springframework.messaging.simp.annotation.SubscribeMapping; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompFrameHandler; +import org.springframework.messaging.simp.stomp.StompHeaders; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.simp.stomp.StompSessionHandler; +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; +import org.springframework.stereotype.Controller; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.messaging.WebSocketStompClient; +import org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport; +import org.springframework.web.socket.sockjs.client.SockJsClient; +import org.springframework.web.socket.sockjs.client.Transport; +import org.springframework.web.socket.sockjs.client.WebSocketTransport; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * Tests for {@link WebSocketMessagingAutoConfiguration}. + * + * @author Andy Wilkinson + */ +public class WebSocketMessagingAutoConfigurationTests { + + private AnnotationConfigEmbeddedWebApplicationContext context = new AnnotationConfigEmbeddedWebApplicationContext(); + + private SockJsClient sockJsClient; + + @Before + public void setup() { + List transports = Arrays.asList(new WebSocketTransport( + new StandardWebSocketClient()), new RestTemplateXhrTransport( + new RestTemplate())); + this.sockJsClient = new SockJsClient(transports); + } + + @After + public void tearDown() { + this.context.close(); + this.sockJsClient.stop(); + } + + @Test + public void basicMessagingWithJson() throws Throwable { + EnvironmentTestUtils.addEnvironment(this.context, "server.port:0", + "spring.jackson.serialization.indent-output:true"); + this.context.register(WebSocketMessagingConfiguration.class); + new ServerPortInfoApplicationContextInitializer().initialize(this.context); + this.context.refresh(); + WebSocketStompClient stompClient = new WebSocketStompClient(this.sockJsClient); + final AtomicReference failure = new AtomicReference(); + final AtomicReference result = new AtomicReference(); + final CountDownLatch latch = new CountDownLatch(1); + StompSessionHandler handler = new StompSessionHandlerAdapter() { + + @Override + public void afterConnected(StompSession session, StompHeaders connectedHeaders) { + session.subscribe("/app/data", new StompFrameHandler() { + + @Override + public void handleFrame(StompHeaders headers, Object payload) { + result.set(payload); + latch.countDown(); + } + + @Override + public Type getPayloadType(StompHeaders headers) { + return Object.class; + } + + }); + } + + @Override + public void handleFrame(StompHeaders headers, Object payload) { + latch.countDown(); + } + + @Override + public void handleException(StompSession session, StompCommand command, + StompHeaders headers, byte[] payload, Throwable exception) { + failure.set(exception); + latch.countDown(); + } + + @Override + public void handleTransportError(StompSession session, Throwable exception) { + failure.set(exception); + latch.countDown(); + } + + }; + + stompClient.setMessageConverter(new SimpleMessageConverter()); + stompClient.connect("ws://localhost:{port}/messaging", handler, this.context + .getEnvironment().getProperty("local.server.port")); + + if (!latch.await(30, TimeUnit.SECONDS)) { + if (failure.get() != null) { + throw failure.get(); + } + else { + fail("Response was not received within 30 seconds"); + } + } + assertThat(new String((byte[]) result.get()), + is(equalTo(String.format("{%n \"foo\" : 5,%n \"bar\" : \"baz\"%n}")))); + } + + @Configuration + @EnableWebSocket + @EnableConfigurationProperties + @EnableWebSocketMessageBroker + @ImportAutoConfiguration({ JacksonAutoConfiguration.class, + EmbeddedServletContainerAutoConfiguration.class, + ServerPropertiesAutoConfiguration.class, + WebSocketMessagingAutoConfiguration.class, + DispatcherServletAutoConfiguration.class }) + static class WebSocketMessagingConfiguration extends + AbstractWebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/messaging").withSockJS(); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/app"); + } + + @Bean + public MessagingController messagingController() { + return new MessagingController(); + } + + @Bean + public TomcatEmbeddedServletContainerFactory tomcat() { + return new TomcatEmbeddedServletContainerFactory(0); + } + + @Bean + public TomcatWebSocketContainerCustomizer tomcatCuztomiser() { + return new TomcatWebSocketContainerCustomizer(); + } + + } + + @Controller + static class MessagingController { + + @SubscribeMapping("/data") + Data getData() { + return new Data(5, "baz"); + } + + } + + static class Data { + + private int foo; + + private String bar; + + Data(int foo, String bar) { + this.foo = foo; + this.bar = bar; + } + + public int getFoo() { + return this.foo; + } + + public String getBar() { + return this.bar; + } + + } + +}