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.lang.Nullable;
031import org.springframework.util.concurrent.ListenableFuture;
032import org.springframework.util.concurrent.SettableListenableFuture;
033import org.springframework.web.client.HttpServerErrorException;
034import org.springframework.web.socket.TextMessage;
035import org.springframework.web.socket.WebSocketHandler;
036import org.springframework.web.socket.WebSocketSession;
037import org.springframework.web.socket.sockjs.frame.SockJsFrame;
038import org.springframework.web.socket.sockjs.transport.TransportType;
039
040/**
041 * Abstract base class for XHR transport implementations to extend.
042 *
043 * @author Rossen Stoyanchev
044 * @since 4.1
045 */
046public abstract class AbstractXhrTransport implements XhrTransport {
047
048        protected static final String PRELUDE;
049
050        static {
051                byte[] bytes = new byte[2048];
052                Arrays.fill(bytes, (byte) 'h');
053                PRELUDE = new String(bytes, SockJsFrame.CHARSET);
054        }
055
056
057        protected final Log logger = LogFactory.getLog(getClass());
058
059        private boolean xhrStreamingDisabled;
060
061
062        @Override
063        public List<TransportType> getTransportTypes() {
064                return (isXhrStreamingDisabled() ? Collections.singletonList(TransportType.XHR) :
065                                Arrays.asList(TransportType.XHR_STREAMING, TransportType.XHR));
066        }
067
068        /**
069         * An {@code XhrTransport} can support both the "xhr_streaming" and "xhr"
070         * SockJS server transports. From a client perspective there is no
071         * implementation difference.
072         * <p>Typically an {@code XhrTransport} is used as "XHR streaming" first and
073         * then, if that fails, as "XHR". In some cases however it may be helpful to
074         * suppress XHR streaming so that only XHR is attempted.
075         * <p>By default this property is set to {@code false} which means both
076         * "XHR streaming" and "XHR" apply.
077         */
078        public void setXhrStreamingDisabled(boolean disabled) {
079                this.xhrStreamingDisabled = disabled;
080        }
081
082        /**
083         * Whether XHR streaming is disabled or not.
084         */
085        @Override
086        public boolean isXhrStreamingDisabled() {
087                return this.xhrStreamingDisabled;
088        }
089
090
091        // Transport methods
092
093        @Override
094        public ListenableFuture<WebSocketSession> connect(TransportRequest request, WebSocketHandler handler) {
095                SettableListenableFuture<WebSocketSession> connectFuture = new SettableListenableFuture<>();
096                XhrClientSockJsSession session = new XhrClientSockJsSession(request, handler, this, connectFuture);
097                request.addTimeoutTask(session.getTimeoutTask());
098
099                URI receiveUrl = request.getTransportUrl();
100                if (logger.isDebugEnabled()) {
101                        logger.debug("Starting XHR " +
102                                        (isXhrStreamingDisabled() ? "Polling" : "Streaming") + "session url=" + receiveUrl);
103                }
104
105                HttpHeaders handshakeHeaders = new HttpHeaders();
106                handshakeHeaders.putAll(request.getHandshakeHeaders());
107
108                connectInternal(request, handler, receiveUrl, handshakeHeaders, session, connectFuture);
109                return connectFuture;
110        }
111
112        protected abstract void connectInternal(TransportRequest request, WebSocketHandler handler,
113                        URI receiveUrl, HttpHeaders handshakeHeaders, XhrClientSockJsSession session,
114                        SettableListenableFuture<WebSocketSession> connectFuture);
115
116
117        // InfoReceiver methods
118
119        @Override
120        public String executeInfoRequest(URI infoUrl, @Nullable HttpHeaders headers) {
121                if (logger.isDebugEnabled()) {
122                        logger.debug("Executing SockJS Info request, url=" + infoUrl);
123                }
124                HttpHeaders infoRequestHeaders = new HttpHeaders();
125                if (headers != null) {
126                        infoRequestHeaders.putAll(headers);
127                }
128                ResponseEntity<String> response = executeInfoRequestInternal(infoUrl, infoRequestHeaders);
129                if (response.getStatusCode() != HttpStatus.OK) {
130                        if (logger.isErrorEnabled()) {
131                                logger.error("SockJS Info request (url=" + infoUrl + ") failed: " + response);
132                        }
133                        throw new HttpServerErrorException(response.getStatusCode());
134                }
135                if (logger.isTraceEnabled()) {
136                        logger.trace("SockJS Info request (url=" + infoUrl + ") response: " + response);
137                }
138                String result = response.getBody();
139                return (result != null ? result : "");
140        }
141
142        protected abstract ResponseEntity<String> executeInfoRequestInternal(URI infoUrl, HttpHeaders headers);
143
144
145        // XhrTransport methods
146
147        @Override
148        public void executeSendRequest(URI url, HttpHeaders headers, TextMessage message) {
149                if (logger.isTraceEnabled()) {
150                        logger.trace("Starting XHR send, url=" + url);
151                }
152                ResponseEntity<String> response = executeSendRequestInternal(url, headers, message);
153                if (response.getStatusCode() != HttpStatus.NO_CONTENT) {
154                        if (logger.isErrorEnabled()) {
155                                logger.error("XHR send request (url=" + url + ") failed: " + response);
156                        }
157                        throw new HttpServerErrorException(response.getStatusCode());
158                }
159                if (logger.isTraceEnabled()) {
160                        logger.trace("XHR send request (url=" + url + ") response: " + response);
161                }
162        }
163
164        protected abstract ResponseEntity<String> executeSendRequestInternal(
165                        URI url, HttpHeaders headers, TextMessage message);
166
167}