001/*
002 * Copyright 2002-2020 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;
020
021import javax.websocket.DecodeException;
022import javax.websocket.Decoder;
023import javax.websocket.EncodeException;
024import javax.websocket.Encoder;
025import javax.websocket.EndpointConfig;
026
027import org.springframework.beans.BeansException;
028import org.springframework.beans.factory.annotation.Autowired;
029import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
030import org.springframework.context.ApplicationContext;
031import org.springframework.context.ConfigurableApplicationContext;
032import org.springframework.core.GenericTypeResolver;
033import org.springframework.core.convert.ConversionException;
034import org.springframework.core.convert.ConversionService;
035import org.springframework.core.convert.TypeDescriptor;
036import org.springframework.lang.Nullable;
037import org.springframework.util.Assert;
038import org.springframework.web.context.ContextLoader;
039
040/**
041 * Base class that can be used to implement a standard {@link javax.websocket.Encoder}
042 * and/or {@link javax.websocket.Decoder}. It provides encode and decode method
043 * implementations that delegate to a Spring {@link ConversionService}.
044 *
045 * <p>By default, this class looks up a {@link ConversionService} registered in the
046 * {@link #getApplicationContext() active ApplicationContext} under
047 * the name {@code 'webSocketConversionService'}. This works fine for both client
048 * and server endpoints, in a Servlet container environment. If not running in a
049 * Servlet container, subclasses will need to override the
050 * {@link #getConversionService()} method to provide an alternative lookup strategy.
051 *
052 * <p>Subclasses can extend this class and should also implement one or
053 * both of {@link javax.websocket.Encoder} and {@link javax.websocket.Decoder}.
054 * For convenience {@link ConvertingEncoderDecoderSupport.BinaryEncoder},
055 * {@link ConvertingEncoderDecoderSupport.BinaryDecoder},
056 * {@link ConvertingEncoderDecoderSupport.TextEncoder} and
057 * {@link ConvertingEncoderDecoderSupport.TextDecoder} subclasses are provided.
058 *
059 * <p>Since JSR-356 only allows Encoder/Decoder to be registered by type, instances
060 * of this class are therefore managed by the WebSocket runtime, and do not need to
061 * be registered as Spring Beans. They can, however, by injected with Spring-managed
062 * dependencies via {@link Autowired @Autowire}.
063 *
064 * <p>Converters to convert between the {@link #getType() type} and {@code String} or
065 * {@code ByteBuffer} should be registered.
066 *
067 * @author Phillip Webb
068 * @since 4.0
069 * @param <T> the type being converted to (for Encoder) or from (for Decoder)
070 * @param <M> the WebSocket message type ({@link String} or {@link ByteBuffer})
071 * @see ConvertingEncoderDecoderSupport.BinaryEncoder
072 * @see ConvertingEncoderDecoderSupport.BinaryDecoder
073 * @see ConvertingEncoderDecoderSupport.TextEncoder
074 * @see ConvertingEncoderDecoderSupport.TextDecoder
075 */
076public abstract class ConvertingEncoderDecoderSupport<T, M> {
077
078        private static final String CONVERSION_SERVICE_BEAN_NAME = "webSocketConversionService";
079
080
081        /**
082         * Called to initialize the encoder/decoder.
083         * @see javax.websocket.Encoder#init(EndpointConfig)
084         * @see javax.websocket.Decoder#init(EndpointConfig)
085         */
086        public void init(EndpointConfig config) {
087                ApplicationContext applicationContext = getApplicationContext();
088                if (applicationContext instanceof ConfigurableApplicationContext) {
089                        ConfigurableListableBeanFactory beanFactory =
090                                        ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
091                        beanFactory.autowireBean(this);
092                }
093        }
094
095        /**
096         * Called to destroy the encoder/decoder.
097         * @see javax.websocket.Encoder#destroy()
098         * @see javax.websocket.Decoder#destroy()
099         */
100        public void destroy() {
101        }
102
103        /**
104         * Strategy method used to obtain the {@link ConversionService}. By default this
105         * method expects a bean named {@code 'webSocketConversionService'} in the
106         * {@link #getApplicationContext() active ApplicationContext}.
107         * @return the {@link ConversionService} (never null)
108         */
109        protected ConversionService getConversionService() {
110                ApplicationContext applicationContext = getApplicationContext();
111                Assert.state(applicationContext != null, "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("Unable to find ConversionService: please configure a '" +
117                                        CONVERSION_SERVICE_BEAN_NAME + "' or override the getConversionService() method", ex);
118                }
119        }
120
121        /**
122         * Returns the active {@link ApplicationContext}. Be default this method obtains
123         * the context via {@link ContextLoader#getCurrentWebApplicationContext()}, which
124         * finds the ApplicationContext loaded via {@link ContextLoader} typically in a
125         * Servlet container environment. When not running in a Servlet container and
126         * not using {@link ContextLoader}, this method should be overridden.
127         * @return the {@link ApplicationContext} or {@code null}
128         */
129        @Nullable
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                Class<?>[] resolved = GenericTypeResolver.resolveTypeArguments(getClass(), ConvertingEncoderDecoderSupport.class);
152                if (resolved == null) {
153                        throw new IllegalStateException("ConvertingEncoderDecoderSupport's generic types T and M " +
154                                        "need to be substituted in subclass: " + getClass());
155                }
156                return resolved;
157        }
158
159        /**
160         * Encode an object to a message.
161         * @see javax.websocket.Encoder.Text#encode(Object)
162         * @see javax.websocket.Encoder.Binary#encode(Object)
163         */
164        @SuppressWarnings("unchecked")
165        @Nullable
166        public M encode(T object) throws EncodeException {
167                try {
168                        return (M) getConversionService().convert(object, getType(), getMessageType());
169                }
170                catch (ConversionException ex) {
171                        throw new EncodeException(object, "Unable to encode websocket message using ConversionService", ex);
172                }
173        }
174
175        /**
176         * Determine if a given message can be decoded.
177         * @see #decode(Object)
178         * @see javax.websocket.Decoder.Text#willDecode(String)
179         * @see javax.websocket.Decoder.Binary#willDecode(ByteBuffer)
180         */
181        public boolean willDecode(M bytes) {
182                return getConversionService().canConvert(getType(), getMessageType());
183        }
184
185        /**
186         * Decode the a message into an object.
187         * @see javax.websocket.Decoder.Text#decode(String)
188         * @see javax.websocket.Decoder.Binary#decode(ByteBuffer)
189         */
190        @SuppressWarnings("unchecked")
191        @Nullable
192        public T decode(M message) throws DecodeException {
193                try {
194                        return (T) getConversionService().convert(message, getMessageType(), getType());
195                }
196                catch (ConversionException ex) {
197                        if (message instanceof String) {
198                                throw new DecodeException((String) message,
199                                                "Unable to decode websocket message using ConversionService", ex);
200                        }
201                        if (message instanceof ByteBuffer) {
202                                throw new DecodeException((ByteBuffer) message,
203                                                "Unable to decode websocket message using ConversionService", ex);
204                        }
205                        throw ex;
206                }
207        }
208
209
210        /**
211         * A binary {@link javax.websocket.Encoder.Binary javax.websocket.Encoder} that delegates
212         * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for details.
213         * @param <T> the type that this Encoder can convert to
214         */
215        public abstract static class BinaryEncoder<T> extends ConvertingEncoderDecoderSupport<T, ByteBuffer>
216                        implements Encoder.Binary<T> {
217        }
218
219
220        /**
221         * A binary {@link javax.websocket.Encoder.Binary javax.websocket.Encoder} that delegates
222         * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for details.
223         * @param <T> the type that this Decoder can convert from
224         */
225        public abstract static class BinaryDecoder<T> extends ConvertingEncoderDecoderSupport<T, ByteBuffer>
226                        implements Decoder.Binary<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
233         * details.
234         * @param <T> the type that this Encoder can convert to
235         */
236        public abstract static class TextEncoder<T> extends ConvertingEncoderDecoderSupport<T, String>
237                        implements Encoder.Text<T> {
238        }
239
240
241        /**
242         * A Text {@link javax.websocket.Encoder.Text javax.websocket.Encoder} that delegates
243         * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for details.
244         * @param <T> the type that this Decoder can convert from
245         */
246        public abstract static class TextDecoder<T> extends ConvertingEncoderDecoderSupport<T, String>
247                        implements Decoder.Text<T> {
248        }
249
250}