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}