diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupport.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupport.java
new file mode 100644
index 00000000000..5158eeac7ec
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupport.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2002-2013 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.web.socket.adapter;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.GenericTypeResolver;
+import org.springframework.core.convert.ConversionException;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.util.Assert;
+import org.springframework.web.context.ContextLoader;
+
+/**
+ * Base class that can be used to implement a standard {@link javax.websocket.Encoder}
+ * and/or {@link javax.websocket.Decoder}. It provides encode and decode method
+ * implementations that delegate to a Spring {@link ConversionService}.
+ *
+ *
By default, this class looks up a {@link ConversionService} registered in the
+ * {@link #getApplicationContext() active ApplicationContext} under
+ * the name {@code 'webSocketConversionService'}. This works fine for both client
+ * and server endpoints, in a Servlet container environment. If not running in a
+ * Servlet container, subclasses will need to override the
+ * {@link #getConversionService()} method to provide an alternative lookup strategy.
+ *
+ *
Subclasses can extend this class and should also implement one or
+ * both of {@link javax.websocket.Encoder} and {@link javax.websocket.Decoder}.
+ * For convenience {@link ConvertingEncoderDecoderSupport.BinaryEncoder},
+ * {@link ConvertingEncoderDecoderSupport.BinaryDecoder},
+ * {@link ConvertingEncoderDecoderSupport.TextEncoder} and
+ * {@link ConvertingEncoderDecoderSupport.TextDecoder} subclasses are provided.
+ *
+ *
Since JSR-356 only allows Encoder/Decoder to be registered by type, instances
+ * of this class are therefore managed by the WebSocket runtime, and do not need to
+ * be registered as Spring Beans. They can, however, by injected with Spring-managed
+ * dependencies via {@link Autowired @Autowire}.
+ *
+ *
Converters to convert between the {@link #getType() type} and {@code String} or
+ * {@code ByteBuffer} should be registered.
+ *
+ * @author Phillip Webb
+ * @since 4.0
+ *
+ * @param The type being converted to (for Encoder) or from (for Decoder)
+ * @param The WebSocket message type ({@link String} or {@link ByteBuffer})
+ *
+ * @see ConvertingEncoderDecoderSupport.BinaryEncoder
+ * @see ConvertingEncoderDecoderSupport.BinaryDecoder
+ * @see ConvertingEncoderDecoderSupport.TextEncoder
+ * @see ConvertingEncoderDecoderSupport.TextDecoder
+ */
+public abstract class ConvertingEncoderDecoderSupport {
+
+ private static final String CONVERSION_SERVICE_BEAN_NAME = "webSocketConversionService";
+
+
+ /**
+ * @see javax.websocket.Encoder#init(EndpointConfig)
+ * @see javax.websocket.Decoder#init(EndpointConfig)
+ */
+ public void init(EndpointConfig config) {
+ ApplicationContext applicationContext = getApplicationContext();
+ if (applicationContext != null && applicationContext instanceof ConfigurableApplicationContext) {
+ ConfigurableListableBeanFactory beanFactory =
+ ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
+ beanFactory.autowireBean(this);
+ }
+ }
+
+ /**
+ * @see javax.websocket.Encoder#destroy()
+ * @see javax.websocket.Decoder#destroy()
+ */
+ public void destroy() {
+ }
+
+ /**
+ * Strategy method used to obtain the {@link ConversionService}. By default this
+ * method expects a bean named {@code 'webSocketConversionService'} in the
+ * {@link #getApplicationContext() active ApplicationContext}.
+ * @return the {@link ConversionService} (never null)
+ */
+ protected ConversionService getConversionService() {
+ ApplicationContext applicationContext = getApplicationContext();
+ Assert.state(applicationContext != null,
+ "Unable to locate the Spring ApplicationContext");
+ try {
+ return applicationContext.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class);
+ }
+ catch (BeansException ex) {
+ throw new IllegalStateException(
+ "Unable to find ConversionService, please configure a '"
+ + CONVERSION_SERVICE_BEAN_NAME + "' or override getConversionService()", ex);
+ }
+ }
+
+ /**
+ * Returns the active {@link ApplicationContext}. Be default this method obtains
+ * the context via {@link ContextLoader#getCurrentWebApplicationContext()}, which
+ * finds the ApplicationContext loaded via {@link ContextLoader} typically in a
+ * Servlet container environment. When not running in a Servlet container and
+ * not using {@link ContextLoader}, this method should be overridden.
+ * @return the {@link ApplicationContext} or {@code null}
+ */
+ protected ApplicationContext getApplicationContext() {
+ return ContextLoader.getCurrentWebApplicationContext();
+ }
+
+ /**
+ * Returns the type being converted. By default the type is resolved using
+ * the generic arguments of the class.
+ */
+ protected TypeDescriptor getType() {
+ return TypeDescriptor.valueOf(resolveTypeArguments()[0]);
+ }
+
+ /**
+ * Returns the websocket message type. By default the type is resolved using
+ * the generic arguments of the class.
+ */
+ protected TypeDescriptor getMessageType() {
+ return TypeDescriptor.valueOf(resolveTypeArguments()[1]);
+ }
+
+ private Class>[] resolveTypeArguments() {
+ return GenericTypeResolver.resolveTypeArguments(getClass(),
+ ConvertingEncoderDecoderSupport.class);
+ }
+
+ /**
+ * @see javax.websocket.Encoder.Text#encode(Object)
+ * @see javax.websocket.Encoder.Binary#encode(Object)
+ */
+ @SuppressWarnings("unchecked")
+ public M encode(T object) throws EncodeException {
+ try {
+ return (M) getConversionService().convert(object, getType(), getMessageType());
+ }
+ catch (ConversionException ex) {
+ throw new EncodeException(object, "Unable to encode websocket message using ConversionService", ex);
+ }
+ }
+
+ /**
+ * @see javax.websocket.Decoder.Text#willDecode(String)
+ * @see javax.websocket.Decoder.Binary#willDecode(ByteBuffer)
+ */
+ public boolean willDecode(M bytes) {
+ return getConversionService().canConvert(getType(), getMessageType());
+ }
+
+ /**
+ * @see javax.websocket.Decoder.Text#decode(String)
+ * @see javax.websocket.Decoder.Binary#decode(ByteBuffer)
+ */
+ @SuppressWarnings("unchecked")
+ public T decode(M message) throws DecodeException {
+ try {
+ return (T) getConversionService().convert(message, getMessageType(), getType());
+ }
+ catch (ConversionException ex) {
+ if (message instanceof String) {
+ throw new DecodeException((String) message, "Unable to decode " +
+ "websocket message using ConversionService", ex);
+ }
+ if (message instanceof ByteBuffer) {
+ throw new DecodeException((ByteBuffer) message, "Unable to decode " +
+ "websocket message using ConversionService", ex);
+ }
+ throw ex;
+ }
+ }
+
+
+ /**
+ * A Binary {@link javax.websocket.Encoder.Binary javax.websocket.Encoder} that
+ * delegates to Spring's conversion service. See
+ * {@link ConvertingEncoderDecoderSupport} for details.
+ *
+ * @param The type that this Encoder can convert to.
+ */
+ public static abstract class BinaryEncoder extends
+ ConvertingEncoderDecoderSupport implements Encoder.Binary {
+ }
+
+
+ /**
+ * A Binary {@link javax.websocket.Encoder.Binary javax.websocket.Encoder} that delegates
+ * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for
+ * details.
+ *
+ * @param The type that this Decoder can convert from.
+ */
+ public static abstract class BinaryDecoder extends
+ ConvertingEncoderDecoderSupport implements Decoder.Binary {
+ }
+
+
+ /**
+ * A Text {@link javax.websocket.Encoder.Text javax.websocket.Encoder} that delegates
+ * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for
+ * details.
+ *
+ * @param The type that this Encoder can convert to.
+ */
+ public static abstract class TextEncoder extends
+ ConvertingEncoderDecoderSupport implements Encoder.Text {
+ }
+
+
+ /**
+ * A Text {@link javax.websocket.Encoder.Text javax.websocket.Encoder} that delegates
+ * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for
+ * details.
+ *
+ * @param The type that this Decoder can convert from.
+ */
+ public static abstract class TextDecoder extends
+ ConvertingEncoderDecoderSupport implements Decoder.Text {
+ }
+
+}
diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/ContextLoaderTestUtils.java b/spring-websocket/src/test/java/org/springframework/web/socket/ContextLoaderTestUtils.java
new file mode 100644
index 00000000000..d0024dbbb14
--- /dev/null
+++ b/spring-websocket/src/test/java/org/springframework/web/socket/ContextLoaderTestUtils.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2013 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.web.socket;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+
+import org.springframework.web.context.ContextLoader;
+import org.springframework.web.context.WebApplicationContext;
+
+/**
+ * General test utilities for manipulating the {@link ContextLoader}.
+ *
+ * @author Phillip Webb
+ */
+public class ContextLoaderTestUtils {
+
+ private static Map currentContextPerThread = getCurrentContextPerThreadFromContextLoader();
+
+ public static void setCurrentWebApplicationContext(WebApplicationContext applicationContext) {
+ setCurrentWebApplicationContext(Thread.currentThread().getContextClassLoader(), applicationContext);
+ }
+
+ public static void setCurrentWebApplicationContext(ClassLoader classLoader, WebApplicationContext applicationContext) {
+ if(applicationContext != null) {
+ currentContextPerThread.put(classLoader, applicationContext);
+ } else {
+ currentContextPerThread.remove(classLoader);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map getCurrentContextPerThreadFromContextLoader() {
+ try {
+ Field field = ContextLoader.class.getDeclaredField("currentContextPerThread");
+ field.setAccessible(true);
+ return (Map) field.get(null);
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+}
diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupportTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupportTests.java
new file mode 100644
index 00000000000..1ed49a81e79
--- /dev/null
+++ b/spring-websocket/src/test/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupportTests.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2002-2013 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.web.socket.adapter;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.core.convert.ConverterNotFoundException;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.convert.support.ByteBufferConverter;
+import org.springframework.core.convert.support.GenericConversionService;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
+import org.springframework.web.socket.ContextLoaderTestUtils;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+/**
+ * Test for {@link ConvertingEncoderDecoderSupport}.
+ *
+ * @author Phillip Webb
+ */
+public class ConvertingEncoderDecoderSupportTests {
+
+ private static final String CONVERTED_TEXT = "_test";
+
+ private static final ByteBuffer CONVERTED_BYTES = ByteBuffer.wrap("~test".getBytes());
+
+
+ @Rule
+ public ExpectedException thown = ExpectedException.none();
+
+ private WebApplicationContext applicationContext;
+
+ private MyType myType = new MyType("test");
+
+
+ @Before
+ public void setup() {
+ setup(Config.class);
+ }
+
+ @After
+ public void teardown() {
+ ContextLoaderTestUtils.setCurrentWebApplicationContext(null);
+ }
+
+ private void setup(Class> configurationClass) {
+ AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
+ applicationContext.register(configurationClass);
+ applicationContext.refresh();
+ this.applicationContext = applicationContext;
+ ContextLoaderTestUtils.setCurrentWebApplicationContext(this.applicationContext);
+ }
+
+ @Test
+ public void encodeToText() throws Exception {
+ assertThat(new MyTextEncoder().encode(myType), equalTo(CONVERTED_TEXT));
+ }
+
+ @Test
+ public void encodeToTextCannotConvert() throws Exception {
+ setup(NoConvertersConfig.class);
+ thown.expect(EncodeException.class);
+ thown.expectCause(isA(ConverterNotFoundException.class));
+ new MyTextEncoder().encode(myType);
+ }
+
+ @Test
+ public void encodeToBinary() throws Exception {
+ assertThat(new MyBinaryEncoder().encode(myType).array(),
+ equalTo(CONVERTED_BYTES.array()));
+ }
+
+ @Test
+ public void encodeToBinaryCannotConvert() throws Exception {
+ setup(NoConvertersConfig.class);
+ thown.expect(EncodeException.class);
+ thown.expectCause(isA(ConverterNotFoundException.class));
+ new MyBinaryEncoder().encode(myType);
+ }
+
+ @Test
+ public void decodeFromText() throws Exception {
+ Decoder.Text decoder = new MyTextDecoder();
+ assertThat(decoder.willDecode(CONVERTED_TEXT), is(true));
+ assertThat(decoder.decode(CONVERTED_TEXT), equalTo(myType));
+ }
+
+ @Test
+ public void decodeFromTextCannotConvert() throws Exception {
+ setup(NoConvertersConfig.class);
+ Decoder.Text decoder = new MyTextDecoder();
+ assertThat(decoder.willDecode(CONVERTED_TEXT), is(false));
+ thown.expect(DecodeException.class);
+ thown.expectCause(isA(ConverterNotFoundException.class));
+ decoder.decode(CONVERTED_TEXT);
+ }
+
+ @Test
+ public void decodeFromBinary() throws Exception {
+ Decoder.Binary decoder = new MyBinaryDecoder();
+ assertThat(decoder.willDecode(CONVERTED_BYTES), is(true));
+ assertThat(decoder.decode(CONVERTED_BYTES), equalTo(myType));
+ }
+
+ @Test
+ public void decodeFromBinaryCannotConvert() throws Exception {
+ setup(NoConvertersConfig.class);
+ Decoder.Binary decoder = new MyBinaryDecoder();
+ assertThat(decoder.willDecode(CONVERTED_BYTES), is(false));
+ thown.expect(DecodeException.class);
+ thown.expectCause(isA(ConverterNotFoundException.class));
+ decoder.decode(CONVERTED_BYTES);
+ }
+
+ @Test
+ public void encodeAndDecodeText() throws Exception {
+ MyTextEncoderDecoder encoderDecoder = new MyTextEncoderDecoder();
+ String encoded = encoderDecoder.encode(myType);
+ assertThat(encoderDecoder.decode(encoded), equalTo(myType));
+ }
+
+ @Test
+ public void encodeAndDecodeBytes() throws Exception {
+ MyBinaryEncoderDecoder encoderDecoder = new MyBinaryEncoderDecoder();
+ ByteBuffer encoded = encoderDecoder.encode(myType);
+ assertThat(encoderDecoder.decode(encoded), equalTo(myType));
+ }
+
+ @Test
+ public void autowiresIntoEncoder() throws Exception {
+ WithAutowire withAutowire = new WithAutowire();
+ withAutowire.init(null);
+ assertThat(withAutowire.config, equalTo(applicationContext.getBean(Config.class)));
+ }
+
+ @Test
+ public void cannotFindApplicationContext() throws Exception {
+ ContextLoaderTestUtils.setCurrentWebApplicationContext(null);
+ WithAutowire encoder = new WithAutowire();
+ encoder.init(null);
+ thown.expect(IllegalStateException.class);
+ thown.expectMessage("Unable to locate the Spring ApplicationContext");
+ encoder.encode(myType);
+ }
+
+ @Test
+ public void cannotFindConversionService() throws Exception {
+ setup(NoConfig.class);
+ MyBinaryEncoder encoder = new MyBinaryEncoder();
+ encoder.init(null);
+ thown.expect(IllegalStateException.class);
+ thown.expectMessage("Unable to find ConversionService");
+ encoder.encode(myType);
+ }
+
+ @Configuration
+ public static class Config {
+
+ @Bean
+ public ConversionService webSocketConversionService() {
+ GenericConversionService conversionService = new GenericConversionService();
+ conversionService.addConverter(new ByteBufferConverter(conversionService));
+ conversionService.addConverter(new MyTypeToStringConverter());
+ conversionService.addConverter(new MyTypeToBytesConverter());
+ conversionService.addConverter(new StringToMyTypeConverter());
+ conversionService.addConverter(new BytesToMyTypeConverter());
+ return conversionService;
+ }
+
+ }
+
+ @Configuration
+ public static class NoConvertersConfig {
+
+ @Bean
+ public ConversionService webSocketConversionService() {
+ return new GenericConversionService();
+ }
+
+ }
+
+
+ @Configuration
+ public static class NoConfig {
+ }
+
+
+ public static class MyType {
+
+ private String value;
+
+ public MyType(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return this.value;
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if(obj instanceof MyType) {
+ return ((MyType)obj).value.equals(value);
+ }
+ return false;
+ }
+ }
+
+
+ private static class MyTypeToStringConverter implements Converter {
+ @Override
+ public String convert(MyType source) {
+ return "_" + source.toString();
+ }
+ }
+
+
+ private static class MyTypeToBytesConverter implements Converter {
+ @Override
+ public byte[] convert(MyType source) {
+ return ("~" + source.toString()).getBytes();
+ }
+ }
+
+
+ private static class StringToMyTypeConverter implements Converter {
+ @Override
+ public MyType convert(String source) {
+ return new MyType(source.substring(1));
+ }
+ }
+
+
+ private static class BytesToMyTypeConverter implements Converter {
+ @Override
+ public MyType convert(byte[] source) {
+ return new MyType(new String(source).substring(1));
+ }
+ }
+
+
+ public static class MyTextEncoder extends
+ ConvertingEncoderDecoderSupport.TextEncoder {
+ }
+
+
+ public static class MyBinaryEncoder extends
+ ConvertingEncoderDecoderSupport.BinaryEncoder {
+ }
+
+
+ public static class MyTextDecoder extends
+ ConvertingEncoderDecoderSupport.TextDecoder {
+ }
+
+
+ public static class MyBinaryDecoder extends
+ ConvertingEncoderDecoderSupport.BinaryDecoder {
+ }
+
+
+ public static class MyTextEncoderDecoder extends
+ ConvertingEncoderDecoderSupport implements Encoder.Text,
+ Decoder.Text {
+ }
+
+
+ public static class MyBinaryEncoderDecoder extends
+ ConvertingEncoderDecoderSupport implements Encoder.Binary,
+ Decoder.Binary {
+ }
+
+
+ public static class WithAutowire extends ConvertingEncoderDecoderSupport.TextDecoder {
+
+ @Autowired
+ private Config config;
+
+ }
+
+}