001/*
002 * Copyright 2002-2018 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.sockjs.client;
018
019import java.net.URI;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.List;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026
027import org.springframework.http.HttpHeaders;
028import org.springframework.http.HttpStatus;
029import org.springframework.http.ResponseEntity;
030import org.springframework.util.concurrent.ListenableFuture;
031import org.springframework.util.concurrent.SettableListenableFuture;
032import org.springframework.web.client.HttpServerErrorException;
033import org.springframework.web.socket.TextMessage;
034import org.springframework.web.socket.WebSocketHandler;
035import org.springframework.web.socket.WebSocketSession;
036import org.springframework.web.socket.sockjs.frame.SockJsFrame;
037import org.springframework.web.socket.sockjs.transport.TransportType;
038
039/**
040 * Abstract base class for XHR transport implementations to extend.
041 *
042 * @author Rossen Stoyanchev
043 * @since 4.1
044 */
045public abstract class AbstractXhrTransport implements XhrTransport {
046
047        protected static final String PRELUDE;
048
049        static {
050                byte[] bytes = new byte[2048];
051                for (int i = 0; i < bytes.length; i++) {
052                        bytes[i] = 'h';
053                }
054                PRELUDE = new String(bytes, SockJsFrame.CHARSET);
055        }
056
057
058        protected final Log logger = LogFactory.getLog(getClass());
059
060        private boolean xhrStreamingDisabled;
061
062        private HttpHeaders requestHeaders = new HttpHeaders();
063
064
065        @Override
066        public List<TransportType> getTransportTypes() {
067                return (isXhrStreamingDisabled() ? Collections.singletonList(TransportType.XHR) :
068                                Arrays.asList(TransportType.XHR_STREAMING, TransportType.XHR));
069        }
070
071        /**
072         * An {@code XhrTransport} can support both the "xhr_streaming" and "xhr"
073         * SockJS server transports. From a client perspective there is no
074         * implementation difference.
075         * <p>Typically an {@code XhrTransport} is used as "XHR streaming" first and
076         * then, if that fails, as "XHR". In some cases however it may be helpful to
077         * suppress XHR streaming so that only XHR is attempted.
078         * <p>By default this property is set to {@code false} which means both
079         * "XHR streaming" and "XHR" apply.
080         */
081        public void setXhrStreamingDisabled(boolean disabled) {
082                this.xhrStreamingDisabled = disabled;
083        }
084
085        /**
086         * Whether XHR streaming is disabled or not.
087         */
088        public boolean isXhrStreamingDisabled() {
089                return this.xhrStreamingDisabled;
090        }
091
092        /**
093         * Configure headers to be added to every executed HTTP request.
094         * @param requestHeaders the headers to add to requests
095         * @deprecated as of 4.2 in favor of {@link SockJsClient#setHttpHeaderNames}.
096         */
097        @Deprecated
098        public void setRequestHeaders(HttpHeaders requestHeaders) {
099                this.requestHeaders.clear();
100                if (requestHeaders != null) {
101                        this.requestHeaders.putAll(requestHeaders);
102                }
103        }
104
105        @Deprecated
106        public HttpHeaders getRequestHeaders() {
107                return this.requestHeaders;
108        }
109
110
111        // Transport methods
112
113        @Override
114        @SuppressWarnings("deprecation")
115        public ListenableFuture<WebSocketSession> connect(TransportRequest request, WebSocketHandler handler) {
116                SettableListenableFuture<WebSocketSession> connectFuture = new SettableListenableFuture<WebSocketSession>();
117                XhrClientSockJsSession session = new XhrClientSockJsSession(request, handler, this, connectFuture);
118                request.addTimeoutTask(session.getTimeoutTask());
119
120                URI receiveUrl = request.getTransportUrl();
121                if (logger.isDebugEnabled()) {
122                        logger.debug("Starting XHR " +
123                                        (isXhrStreamingDisabled() ? "Polling" : "Streaming") + "session url=" + receiveUrl);
124                }
125
126                HttpHeaders handshakeHeaders = new HttpHeaders();
127                handshakeHeaders.putAll(getRequestHeaders());
128                handshakeHeaders.putAll(request.getHandshakeHeaders());
129
130                connectInternal(request, handler, receiveUrl, handshakeHeaders, session, connectFuture);
131                return connectFuture;
132        }
133
134        protected abstract void connectInternal(TransportRequest request, WebSocketHandler handler,
135                        URI receiveUrl, HttpHeaders handshakeHeaders, XhrClientSockJsSession session,
136                        SettableListenableFuture<WebSocketSession> connectFuture);
137
138
139        // InfoReceiver methods
140
141        @Override
142        @SuppressWarnings("deprecation")
143        public String executeInfoRequest(URI infoUrl, HttpHeaders headers) {
144                if (logger.isDebugEnabled()) {
145                        logger.debug("Executing SockJS Info request, url=" + infoUrl);
146                }
147                HttpHeaders infoRequestHeaders = new HttpHeaders();
148                infoRequestHeaders.putAll(getRequestHeaders());
149                if (headers != null) {
150                        infoRequestHeaders.putAll(headers);
151                }
152                ResponseEntity<String> response = executeInfoRequestInternal(infoUrl, infoRequestHeaders);
153                if (response.getStatusCode() != HttpStatus.OK) {
154                        if (logger.isErrorEnabled()) {
155                                logger.error("SockJS Info request (url=" + infoUrl + ") failed: " + response);
156                        }
157                        throw new HttpServerErrorException(response.getStatusCode());
158                }
159                if (logger.isTraceEnabled()) {
160                        logger.trace("SockJS Info request (url=" + infoUrl + ") response: " + response);
161                }
162                return response.getBody();
163        }
164
165        protected abstract ResponseEntity<String> executeInfoRequestInternal(URI infoUrl, HttpHeaders headers);
166
167
168        // XhrTransport methods
169
170        @Override
171        public void executeSendRequest(URI url, HttpHeaders headers, TextMessage message) {
172                if (logger.isTraceEnabled()) {
173                        logger.trace("Starting XHR send, url=" + url);
174                }
175                ResponseEntity<String> response = executeSendRequestInternal(url, headers, message);
176                if (response.getStatusCode() != HttpStatus.NO_CONTENT) {
177                        if (logger.isErrorEnabled()) {
178                                logger.error("XHR send request (url=" + url + ") failed: " + response);
179                        }
180                        throw new HttpServerErrorException(response.getStatusCode());
181                }
182                if (logger.isTraceEnabled()) {
183                        logger.trace("XHR send request (url=" + url + ") response: " + response);
184                }
185        }
186
187        protected abstract ResponseEntity<String> executeSendRequestInternal(
188                        URI url, HttpHeaders headers, TextMessage message);
189
190
191        @Override
192        public String toString() {
193                return getClass().getSimpleName();
194        }
195
196}