001/*
002 * Copyright 2002-2015 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.socket.adapter.standard;
018
019import java.nio.ByteBuffer;
020import javax.websocket.DecodeException;
021import javax.websocket.Decoder;
022import javax.websocket.EncodeException;
023import javax.websocket.Encoder;
024import javax.websocket.EndpointConfig;
025
026import org.springframework.beans.BeansException;
027import org.springframework.beans.factory.annotation.Autowired;
028import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
029import org.springframework.context.ApplicationContext;
030import org.springframework.context.ConfigurableApplicationContext;
031import org.springframework.core.GenericTypeResolver;
032import org.springframework.core.convert.ConversionException;
033import org.springframework.core.convert.ConversionService;
034import org.springframework.core.convert.TypeDescriptor;
035import org.springframework.util.Assert;
036import org.springframework.web.context.ContextLoader;
037
038/**
039 * Base class that can be used to implement a standard {@link javax.websocket.Encoder}
040 * and/or {@link javax.websocket.Decoder}. It provides encode and decode method
041 * implementations that delegate to a Spring {@link ConversionService}.
042 *
043 * <p>By default, this class looks up a {@link ConversionService} registered in the
044 * {@link #getApplicationContext() active ApplicationContext} under
045 * the name {@code 'webSocketConversionService'}. This works fine for both client
046 * and server endpoints, in a Servlet container environment. If not running in a
047 * Servlet container, subclasses will need to override the
048 * {@link #getConversionService()} method to provide an alternative lookup strategy.
049 *
050 * <p>Subclasses can extend this class and should also implement one or
051 * both of {@link javax.websocket.Encoder} and {@link javax.websocket.Decoder}.
052 * For convenience {@link ConvertingEncoderDecoderSupport.BinaryEncoder},
053 * {@link ConvertingEncoderDecoderSupport.BinaryDecoder},
054 * {@link ConvertingEncoderDecoderSupport.TextEncoder} and
055 * {@link ConvertingEncoderDecoderSupport.TextDecoder} subclasses are provided.
056 *
057 * <p>Since JSR-356 only allows Encoder/Decoder to be registered by type, instances
058 * of this class are therefore managed by the WebSocket runtime, and do not need to
059 * be registered as Spring Beans. They can, however, by injected with Spring-managed
060 * dependencies via {@link Autowired @Autowire}.
061 *
062 * <p>Converters to convert between the {@link #getType() type} and {@code String} or
063 * {@code ByteBuffer} should be registered.
064 *
065 * @author Phillip Webb
066 * @since 4.0
067 * @see ConvertingEncoderDecoderSupport.BinaryEncoder
068 * @see ConvertingEncoderDecoderSupport.BinaryDecoder
069 * @see ConvertingEncoderDecoderSupport.TextEncoder
070 * @see ConvertingEncoderDecoderSupport.TextDecoder
071 * @param <T> the type being converted to (for Encoder) or from (for Decoder)
072 * @param <M> the WebSocket message type ({@link String} or {@link ByteBuffer})
073 */
074public abstract class ConvertingEncoderDecoderSupport<T, M> {
075
076        private static final String CONVERSION_SERVICE_BEAN_NAME = "webSocketConversionService";
077
078
079        /**
080         * @see javax.websocket.Encoder#init(EndpointConfig)
081         * @see javax.websocket.Decoder#init(EndpointConfig)
082         */
083        public void init(EndpointConfig config) {
084                ApplicationContext applicationContext = getApplicationContext();
085                if (applicationContext != null && applicationContext instanceof ConfigurableApplicationContext) {
086                        ConfigurableListableBeanFactory beanFactory =
087                                        ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
088                        beanFactory.autowireBean(this);
089                }
090        }
091
092        /**
093         * @see javax.websocket.Encoder#destroy()
094         * @see javax.websocket.Decoder#destroy()
095         */
096        public void destroy() {
097        }
098
099        /**
100         * Strategy method used to obtain the {@link ConversionService}. By default this
101         * method expects a bean named {@code 'webSocketConversionService'} in the
102         * {@link #getApplicationContext() active ApplicationContext}.
103         * @return the {@link ConversionService} (never null)
104         */
105        protected ConversionService getConversionService() {
106                ApplicationContext applicationContext = getApplicationContext();
107                Assert.state(applicationContext != null, "Unable to locate the Spring ApplicationContext");
108                try {
109                        return applicationContext.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class);
110                }
111                catch (BeansException ex) {
112                        throw new IllegalStateException("Unable to find ConversionService: please configure a '" +
113                                        CONVERSION_SERVICE_BEAN_NAME + "' or override the getConversionService() method", ex);
114                }
115        }
116
117        /**
118         * Returns the active {@link ApplicationContext}. Be default this method obtains
119         * the context via {@link ContextLoader#getCurrentWebApplicationContext()}, which
120         * finds the ApplicationContext loaded via {@link ContextLoader} typically in a
121         * Servlet container environment. When not running in a Servlet container and
122         * not using {@link ContextLoader}, this method should be overridden.
123         * @return the {@link ApplicationContext} or {@code null}
124         */
125        protected ApplicationContext getApplicationContext() {
126                return ContextLoader.getCurrentWebApplicationContext();
127        }
128
129        /**
130         * Returns the type being converted. By default the type is resolved using
131         * the generic arguments of the class.
132         */
133        protected TypeDescriptor getType() {
134                return TypeDescriptor.valueOf(resolveTypeArguments()[0]);
135        }
136
137        /**
138         * Returns the websocket message type. By default the type is resolved using
139         * the generic arguments of the class.
140         */
141        protected TypeDescriptor getMessageType() {
142                return TypeDescriptor.valueOf(resolveTypeArguments()[1]);
143        }
144
145        private Class<?>[] resolveTypeArguments() {
146                Class<?>[] resolved = GenericTypeResolver.resolveTypeArguments(getClass(), ConvertingEncoderDecoderSupport.class);
147                if (resolved == null) {
148                        throw new IllegalStateException("ConvertingEncoderDecoderSupport's generic types T and M " +
149                                        "need to be substituted in subclass: " + getClass());
150                }
151                return resolved;
152        }
153
154        /**
155         * @see javax.websocket.Encoder.Text#encode(Object)
156         * @see javax.websocket.Encoder.Binary#encode(Object)
157         */
158        @SuppressWarnings("unchecked")
159        public M encode(T object) throws EncodeException {
160                try {
161                        return (M) getConversionService().convert(object, getType(), getMessageType());
162                }
163                catch (ConversionException ex) {
164                        throw new EncodeException(object, "Unable to encode websocket message using ConversionService", ex);
165                }
166        }
167
168        /**
169         * @see javax.websocket.Decoder.Text#willDecode(String)
170         * @see javax.websocket.Decoder.Binary#willDecode(ByteBuffer)
171         */
172        public boolean willDecode(M bytes) {
173                return getConversionService().canConvert(getType(), getMessageType());
174        }
175
176        /**
177         * @see javax.websocket.Decoder.Text#decode(String)
178         * @see javax.websocket.Decoder.Binary#decode(ByteBuffer)
179         */
180        @SuppressWarnings("unchecked")
181        public T decode(M message) throws DecodeException {
182                try {
183                        return (T) getConversionService().convert(message, getMessageType(), getType());
184                }
185                catch (ConversionException ex) {
186                        if (message instanceof String) {
187                                throw new DecodeException((String) message,
188                                                "Unable to decode websocket message using ConversionService", ex);
189                        }
190                        if (message instanceof ByteBuffer) {
191                                throw new DecodeException((ByteBuffer) message,
192                                                "Unable to decode websocket message using ConversionService", ex);
193                        }
194                        throw ex;
195                }
196        }
197
198
199        /**
200         * A binary {@link javax.websocket.Encoder.Binary javax.websocket.Encoder} that delegates
201         * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for details.
202         * @param <T> the type that this Encoder can convert to
203         */
204        public static abstract class BinaryEncoder<T> extends ConvertingEncoderDecoderSupport<T, ByteBuffer>
205                        implements Encoder.Binary<T> {
206        }
207
208
209        /**
210         * A binary {@link javax.websocket.Encoder.Binary javax.websocket.Encoder} that delegates
211         * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for details.
212         * @param <T> the type that this Decoder can convert from
213         */
214        public static abstract class BinaryDecoder<T> extends ConvertingEncoderDecoderSupport<T, ByteBuffer>
215                        implements Decoder.Binary<T> {
216        }
217
218
219        /**
220         * A text {@link javax.websocket.Encoder.Text javax.websocket.Encoder} that delegates
221         * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for
222         * details.
223         * @param <T> the type that this Encoder can convert to
224         */
225        public static abstract class TextEncoder<T> extends ConvertingEncoderDecoderSupport<T, String>
226                        implements Encoder.Text<T> {
227        }
228
229
230        /**
231         * A Text {@link javax.websocket.Encoder.Text javax.websocket.Encoder} that delegates
232         * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for details.
233         * @param <T> the type that this Decoder can convert from
234         */
235        public static abstract class TextDecoder<T> extends ConvertingEncoderDecoderSupport<T, String>
236                        implements Decoder.Text<T> {
237        }
238
239}