Skip to content

Commit 8d6fd1e

Browse files
committed
ConversionService JSR-356 Encoder/Decoder adapters
Develop support class allowing JSR-356 Encoder and Decoder interfaces to delegate to Spring's ConversionService. Issue: SPR-10694
1 parent 9dba73d commit 8d6fd1e

File tree

3 files changed

+622
-0
lines changed

3 files changed

+622
-0
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.socket.adapter;
18+
19+
import java.nio.ByteBuffer;
20+
21+
import javax.websocket.DecodeException;
22+
import javax.websocket.Decoder;
23+
import javax.websocket.EncodeException;
24+
import javax.websocket.Encoder;
25+
import javax.websocket.EndpointConfig;
26+
27+
import org.springframework.beans.BeansException;
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
30+
import org.springframework.context.ApplicationContext;
31+
import org.springframework.context.ConfigurableApplicationContext;
32+
import org.springframework.core.GenericTypeResolver;
33+
import org.springframework.core.convert.ConversionException;
34+
import org.springframework.core.convert.ConversionService;
35+
import org.springframework.core.convert.TypeDescriptor;
36+
import org.springframework.util.Assert;
37+
import org.springframework.web.context.ContextLoader;
38+
39+
/**
40+
* Base class that can be used to implement a standard {@link javax.websocket.Encoder}
41+
* and/or {@link javax.websocket.Decoder}. It provides encode and decode method
42+
* implementations that delegate to a Spring {@link ConversionService}.
43+
*
44+
* <p>By default, this class looks up a {@link ConversionService} registered in the
45+
* {@link #getApplicationContext() active ApplicationContext} under
46+
* the name {@code 'webSocketConversionService'}. This works fine for both client
47+
* and server endpoints, in a Servlet container environment. If not running in a
48+
* Servlet container, subclasses will need to override the
49+
* {@link #getConversionService()} method to provide an alternative lookup strategy.
50+
*
51+
* <p>Subclasses can extend this class and should also implement one or
52+
* both of {@link javax.websocket.Encoder} and {@link javax.websocket.Decoder}.
53+
* For convenience {@link ConvertingEncoderDecoderSupport.BinaryEncoder},
54+
* {@link ConvertingEncoderDecoderSupport.BinaryDecoder},
55+
* {@link ConvertingEncoderDecoderSupport.TextEncoder} and
56+
* {@link ConvertingEncoderDecoderSupport.TextDecoder} subclasses are provided.
57+
*
58+
* <p>Since JSR-356 only allows Encoder/Decoder to be registered by type, instances
59+
* of this class are therefore managed by the WebSocket runtime, and do not need to
60+
* be registered as Spring Beans. They can, however, by injected with Spring-managed
61+
* dependencies via {@link Autowired @Autowire}.
62+
*
63+
* <p>Converters to convert between the {@link #getType() type} and {@code String} or
64+
* {@code ByteBuffer} should be registered.
65+
*
66+
* @author Phillip Webb
67+
* @since 4.0
68+
*
69+
* @param <T> The type being converted to (for Encoder) or from (for Decoder)
70+
* @param <M> The WebSocket message type ({@link String} or {@link ByteBuffer})
71+
*
72+
* @see ConvertingEncoderDecoderSupport.BinaryEncoder
73+
* @see ConvertingEncoderDecoderSupport.BinaryDecoder
74+
* @see ConvertingEncoderDecoderSupport.TextEncoder
75+
* @see ConvertingEncoderDecoderSupport.TextDecoder
76+
*/
77+
public abstract class ConvertingEncoderDecoderSupport<T, M> {
78+
79+
private static final String CONVERSION_SERVICE_BEAN_NAME = "webSocketConversionService";
80+
81+
82+
/**
83+
* @see javax.websocket.Encoder#init(EndpointConfig)
84+
* @see javax.websocket.Decoder#init(EndpointConfig)
85+
*/
86+
public void init(EndpointConfig config) {
87+
ApplicationContext applicationContext = getApplicationContext();
88+
if (applicationContext != null && applicationContext instanceof ConfigurableApplicationContext) {
89+
ConfigurableListableBeanFactory beanFactory =
90+
((ConfigurableApplicationContext) applicationContext).getBeanFactory();
91+
beanFactory.autowireBean(this);
92+
}
93+
}
94+
95+
/**
96+
* @see javax.websocket.Encoder#destroy()
97+
* @see javax.websocket.Decoder#destroy()
98+
*/
99+
public void destroy() {
100+
}
101+
102+
/**
103+
* Strategy method used to obtain the {@link ConversionService}. By default this
104+
* method expects a bean named {@code 'webSocketConversionService'} in the
105+
* {@link #getApplicationContext() active ApplicationContext}.
106+
* @return the {@link ConversionService} (never null)
107+
*/
108+
protected ConversionService getConversionService() {
109+
ApplicationContext applicationContext = getApplicationContext();
110+
Assert.state(applicationContext != null,
111+
"Unable to locate the Spring ApplicationContext");
112+
try {
113+
return applicationContext.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class);
114+
}
115+
catch (BeansException ex) {
116+
throw new IllegalStateException(
117+
"Unable to find ConversionService, please configure a '"
118+
+ CONVERSION_SERVICE_BEAN_NAME + "' or override getConversionService()", ex);
119+
}
120+
}
121+
122+
/**
123+
* Returns the active {@link ApplicationContext}. Be default this method obtains
124+
* the context via {@link ContextLoader#getCurrentWebApplicationContext()}, which
125+
* finds the ApplicationContext loaded via {@link ContextLoader} typically in a
126+
* Servlet container environment. When not running in a Servlet container and
127+
* not using {@link ContextLoader}, this method should be overridden.
128+
* @return the {@link ApplicationContext} or {@code null}
129+
*/
130+
protected ApplicationContext getApplicationContext() {
131+
return ContextLoader.getCurrentWebApplicationContext();
132+
}
133+
134+
/**
135+
* Returns the type being converted. By default the type is resolved using
136+
* the generic arguments of the class.
137+
*/
138+
protected TypeDescriptor getType() {
139+
return TypeDescriptor.valueOf(resolveTypeArguments()[0]);
140+
}
141+
142+
/**
143+
* Returns the websocket message type. By default the type is resolved using
144+
* the generic arguments of the class.
145+
*/
146+
protected TypeDescriptor getMessageType() {
147+
return TypeDescriptor.valueOf(resolveTypeArguments()[1]);
148+
}
149+
150+
private Class<?>[] resolveTypeArguments() {
151+
return GenericTypeResolver.resolveTypeArguments(getClass(),
152+
ConvertingEncoderDecoderSupport.class);
153+
}
154+
155+
/**
156+
* @see javax.websocket.Encoder.Text#encode(Object)
157+
* @see javax.websocket.Encoder.Binary#encode(Object)
158+
*/
159+
@SuppressWarnings("unchecked")
160+
public M encode(T object) throws EncodeException {
161+
try {
162+
return (M) getConversionService().convert(object, getType(), getMessageType());
163+
}
164+
catch (ConversionException ex) {
165+
throw new EncodeException(object, "Unable to encode websocket message using ConversionService", ex);
166+
}
167+
}
168+
169+
/**
170+
* @see javax.websocket.Decoder.Text#willDecode(String)
171+
* @see javax.websocket.Decoder.Binary#willDecode(ByteBuffer)
172+
*/
173+
public boolean willDecode(M bytes) {
174+
return getConversionService().canConvert(getType(), getMessageType());
175+
}
176+
177+
/**
178+
* @see javax.websocket.Decoder.Text#decode(String)
179+
* @see javax.websocket.Decoder.Binary#decode(ByteBuffer)
180+
*/
181+
@SuppressWarnings("unchecked")
182+
public T decode(M message) throws DecodeException {
183+
try {
184+
return (T) getConversionService().convert(message, getMessageType(), getType());
185+
}
186+
catch (ConversionException ex) {
187+
if (message instanceof String) {
188+
throw new DecodeException((String) message, "Unable to decode " +
189+
"websocket message using ConversionService", ex);
190+
}
191+
if (message instanceof ByteBuffer) {
192+
throw new DecodeException((ByteBuffer) message, "Unable to decode " +
193+
"websocket message using ConversionService", ex);
194+
}
195+
throw ex;
196+
}
197+
}
198+
199+
200+
/**
201+
* A Binary {@link javax.websocket.Encoder.Binary javax.websocket.Encoder} that
202+
* delegates to Spring's conversion service. See
203+
* {@link ConvertingEncoderDecoderSupport} for details.
204+
*
205+
* @param <T> The type that this Encoder can convert to.
206+
*/
207+
public static abstract class BinaryEncoder<T> extends
208+
ConvertingEncoderDecoderSupport<T, ByteBuffer> implements Encoder.Binary<T> {
209+
}
210+
211+
212+
/**
213+
* A Binary {@link javax.websocket.Encoder.Binary javax.websocket.Encoder} that delegates
214+
* to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for
215+
* details.
216+
*
217+
* @param <T> The type that this Decoder can convert from.
218+
*/
219+
public static abstract class BinaryDecoder<T> extends
220+
ConvertingEncoderDecoderSupport<T, ByteBuffer> implements Decoder.Binary<T> {
221+
}
222+
223+
224+
/**
225+
* A Text {@link javax.websocket.Encoder.Text javax.websocket.Encoder} that delegates
226+
* to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for
227+
* details.
228+
*
229+
* @param <T> The type that this Encoder can convert to.
230+
*/
231+
public static abstract class TextEncoder<T> extends
232+
ConvertingEncoderDecoderSupport<T, String> implements Encoder.Text<T> {
233+
}
234+
235+
236+
/**
237+
* A Text {@link javax.websocket.Encoder.Text javax.websocket.Encoder} that delegates
238+
* to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for
239+
* details.
240+
*
241+
* @param <T> The type that this Decoder can convert from.
242+
*/
243+
public static abstract class TextDecoder<T> extends
244+
ConvertingEncoderDecoderSupport<T, String> implements Decoder.Text<T> {
245+
}
246+
247+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.socket;
18+
19+
import java.lang.reflect.Field;
20+
import java.util.Map;
21+
22+
import org.springframework.web.context.ContextLoader;
23+
import org.springframework.web.context.WebApplicationContext;
24+
25+
/**
26+
* General test utilities for manipulating the {@link ContextLoader}.
27+
*
28+
* @author Phillip Webb
29+
*/
30+
public class ContextLoaderTestUtils {
31+
32+
private static Map<ClassLoader, WebApplicationContext> currentContextPerThread = getCurrentContextPerThreadFromContextLoader();
33+
34+
public static void setCurrentWebApplicationContext(WebApplicationContext applicationContext) {
35+
setCurrentWebApplicationContext(Thread.currentThread().getContextClassLoader(), applicationContext);
36+
}
37+
38+
public static void setCurrentWebApplicationContext(ClassLoader classLoader, WebApplicationContext applicationContext) {
39+
if(applicationContext != null) {
40+
currentContextPerThread.put(classLoader, applicationContext);
41+
} else {
42+
currentContextPerThread.remove(classLoader);
43+
}
44+
}
45+
46+
@SuppressWarnings("unchecked")
47+
private static Map<ClassLoader, WebApplicationContext> getCurrentContextPerThreadFromContextLoader() {
48+
try {
49+
Field field = ContextLoader.class.getDeclaredField("currentContextPerThread");
50+
field.setAccessible(true);
51+
return (Map<ClassLoader, WebApplicationContext>) field.get(null);
52+
}
53+
catch (Exception ex) {
54+
throw new IllegalStateException(ex);
55+
}
56+
}
57+
58+
}

0 commit comments

Comments
 (0)