001/** 002 * 003 * Copyright 2021-2023 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smack.websocket.java11; 018 019import java.net.URI; 020import java.net.http.HttpClient; 021import java.net.http.WebSocket; 022import java.nio.ByteBuffer; 023import java.util.concurrent.CompletableFuture; 024import java.util.concurrent.CompletionStage; 025import java.util.concurrent.ExecutionException; 026import java.util.logging.Level; 027 028import javax.net.ssl.SSLSession; 029 030import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; 031import org.jivesoftware.smack.util.LazyStringBuilder; 032import org.jivesoftware.smack.websocket.impl.AbstractWebSocket; 033import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; 034 035public final class Java11WebSocket extends AbstractWebSocket { 036 037 private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().build(); 038 039 private WebSocket webSocket; 040 041 enum PingPong { 042 ping, 043 pong, 044 }; 045 046 Java11WebSocket(WebSocketRemoteConnectionEndpoint endpoint, 047 ModularXmppClientToServerConnectionInternal connectionInternal) { 048 super(endpoint, connectionInternal); 049 050 final WebSocket.Listener listener = new WebSocket.Listener() { 051 @Override 052 public void onOpen(WebSocket webSocket) { 053 LOGGER.finer(webSocket + " opened"); 054 webSocket.request(1); 055 } 056 057 LazyStringBuilder received = new LazyStringBuilder(); 058 059 @Override 060 public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) { 061 received.append(data); 062 webSocket.request(1); 063 064 if (last) { 065 String wholeMessage = received.toString(); 066 received = new LazyStringBuilder(); 067 onIncomingWebSocketElement(wholeMessage); 068 } 069 070 return null; 071 } 072 073 @Override 074 public void onError(WebSocket webSocket, Throwable error) { 075 onWebSocketFailure(error); 076 } 077 078 @Override 079 public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) { 080 LOGGER.finer(webSocket + " closed with status code " + statusCode + ". Provided reason: " + reason); 081 // TODO: What should we do here? What if some server implementation closes the WebSocket out of the 082 // blue? Ideally, we should react on this. Some situation in the okhttp implementation. 083 return null; 084 } 085 086 @Override 087 public CompletionStage<?> onPing(WebSocket webSocket, ByteBuffer message) { 088 logPingPong(PingPong.ping, webSocket, message); 089 090 webSocket.request(1); 091 return null; 092 } 093 094 @Override 095 public CompletionStage<?> onPong(WebSocket webSocket, ByteBuffer message) { 096 logPingPong(PingPong.pong, webSocket, message); 097 098 webSocket.request(1); 099 return null; 100 } 101 102 private void logPingPong(PingPong pingPong, WebSocket webSocket, ByteBuffer message) { 103 final Level pingPongLogLevel = Level.FINER; 104 if (!LOGGER.isLoggable(pingPongLogLevel)) { 105 return; 106 } 107 108 LOGGER.log(pingPongLogLevel, "Received " + pingPong + " over " + webSocket + ". Message: " + message); 109 } 110 }; 111 112 final URI uri = endpoint.getUri(); 113 CompletionStage<WebSocket> webSocketFuture = HTTP_CLIENT.newWebSocketBuilder() 114 .subprotocols(SEC_WEBSOCKET_PROTOCOL_HEADER_FILED_VALUE_XMPP) 115 .buildAsync(uri, listener); 116 117 webSocketFuture.whenComplete((webSocket, throwable) -> { 118 if (throwable == null) { 119 this.webSocket = webSocket; 120 future.setResult(this); 121 } else { 122 onWebSocketFailure(throwable); 123 } 124 }); 125 } 126 127 @Override 128 protected void send(String element) { 129 CompletableFuture<WebSocket> completableFuture = webSocket.sendText(element, true); 130 try { 131 completableFuture.get(); 132 } catch (ExecutionException e) { 133 onWebSocketFailure(e); 134 } catch (InterruptedException e) { 135 // This thread should never be interrupted, as it is a Smack internal thread. 136 throw new AssertionError(e); 137 } 138 } 139 140 @Override 141 public void disconnect(int code, String message) { 142 try { 143 if (!webSocket.isOutputClosed()) { 144 CompletableFuture<WebSocket> completableFuture = webSocket.sendClose(code, message); 145 completableFuture.get(); 146 } 147 } catch (ExecutionException e) { 148 LOGGER.log(Level.WARNING, "Failed to send final close when disconnecting " + this, e); 149 } catch (InterruptedException e) { 150 // This thread should never be interrupted, as it is a Smack internal thread. 151 throw new AssertionError(e); 152 } finally { 153 webSocket.abort(); 154 } 155 } 156 157 @Override 158 public SSLSession getSSLSession() { 159 return null; 160 } 161 162 private void onWebSocketFailure(ExecutionException executionException) { 163 Throwable cause = executionException.getCause(); 164 onWebSocketFailure(cause); 165 } 166}