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}