001package Torello.Browser; 002 003import javax.json.*; 004import java.io.*; 005 006import NeoVisionaries.WebSockets.*; 007 008import Torello.Java.*; 009import Torello.Java.Additional.AppendableSafe; 010 011import static Torello.Java.C.*; 012 013import java.util.concurrent.ConcurrentHashMap; 014import java.util.concurrent.atomic.AtomicInteger; 015 016import java.util.List; 017import java.util.Objects; 018import java.lang.reflect.Constructor; 019import java.util.function.Consumer; 020 021/** 022 * This class implements a connection to a Web-Browser using the Remote Debug Protocol over 023 * Web-Sockets. 024 * 025 * <H3 STYLE='background: black; color: white; padding: 0.5em;'>Browser Remote Debug Protocol 026 * Connection Class</H3> 027 * 028 * <BR />Java is capable of communicating with either a Headless instance of Google Chrome - <I>or 029 * any browser that implements the Remote Debuggin Protocol</I>. It is not mandatory to run the 030 * browser in headless mode, but it is more common. 031 */ 032@SuppressWarnings({"rawtypes", "unchecked"}) 033public class WebSocketSender 034{ 035 private static final String CTITLE = 036 BCYAN_BKGND + BBLACK + StringParse.rightSpacePad(" [Class WebSocketSender]", 30) + RESET; 037 038 039 // ******************************************************************************************** 040 // ******************************************************************************************** 041 // Main Fields 042 // ******************************************************************************************** 043 // ******************************************************************************************** 044 045 046 /** The Browser Web-Socket Connection */ 047 public final WebSocket webSocket; 048 049 // Stores the lists of promises 050 private ConcurrentHashMap<Integer, Promise> promises = new ConcurrentHashMap<>(); 051 052 final ConnRecord connRec; 053 054 055 // ******************************************************************************************** 056 // ******************************************************************************************** 057 // Constructor 058 // ******************************************************************************************** 059 // ******************************************************************************************** 060 061 062 /** 063 * Opens a Connection to a Web Browser using a Web-Socket. This class will now be 064 * ready to accept {@link #send(Script, Promise)} messages to the browser. 065 * 066 * @param url This is a {@code URL} that is generated by the browser, and has a base 067 * {@code URL} that is just {@code 127.0.0.1}, followed by a <B STYLE='color:red'>port 068 * number</B>. There will also be an <B STYLE='color:red;'>identifier-code</B>. 069 * 070 * @throws IOException Throws if there are problems connecting the socket. 071 * 072 * @throws WebSocketException Throws if the NeoVisionaries Package encounters a problem 073 * building the socket connection. 074 */ 075 public WebSocketSender(final String url, final ConnRecord connRec) 076 throws IOException, WebSocketException 077 { 078 Objects.requireNonNull(url, "Parameter 'url' has been passed null."); 079 Objects.requireNonNull(connRec, "Parameter 'connRec' has been passed null."); 080 081 this.connRec = connRec; 082 083 final WebSocketListener webSocketListener = new WSAdapter(this, this.promises); 084 085 this.webSocket = new WebSocketFactory() 086 .createSocket(url) 087 .addListener(webSocketListener) 088 .connect(); 089 090 System.out.println 091 (BYELLOW_BKGND + BBLACK + " Web Socket Connection Opened: " + RESET + ' ' + url); 092 } 093 094 095 // ******************************************************************************************** 096 // ******************************************************************************************** 097 // Two Instance Methods 098 // ******************************************************************************************** 099 // ******************************************************************************************** 100 101 102 /** Closes the {@link WebSocket} connection to the Browser's Remote Debug Port. */ 103 public void disconnect() { webSocket.disconnect(); } 104 105 private final AtomicInteger messageID = new AtomicInteger(0); 106 107 /** 108 * This method transmits a request to a Browser's Remote-Debugging Port over the 109 * {@code WebSocket}. It keeps the {@link Promise} that was created by the {@link Script} that 110 * sent this request, and saves that {@code Promise} until the Web-Socket receives a response 111 * about the request. 112 * 113 * @param promise This is a {@code Promise} which is automatically generated by the 114 * {@link Script} object that is sending the request. 115 */ 116 void send(final Script script, final Promise promise) 117 { 118 final int requestID = messageID.updateAndGet((int i) -> (i == Integer.MAX_VALUE) ? 0 : i + 1); 119 120 final String jsonRequest = 121 "{\"id\":" + requestID + ',' + script.requestJSONString.substring(1); 122 123 // System.out.println("jsonRequest:\n" + jsonRequest); 124 // Torello.Java.Q.BP(); 125 126 final String msg = 127 CTITLE + 128 BCYAN + " Sending JSON:\n" + RESET + 129 StrIndent.indent(jsonRequest, 4) + '\n'; 130 131 // Print the request-message that is about to be sent, and then send it. 132 this.connRec.app(msg); 133 134 this.promises.put(requestID, promise); 135 136 try 137 { this.webSocket.sendText(jsonRequest); } 138 139 catch (Exception e) 140 { 141 final String errMsg = 142 "Error attempting to send Json Request:\n" + 143 e.getMessage() + "\n"; 144 145 this.connRec.err(errMsg); 146 147 // ensure we don't leave a dangling entry 148 this.promises.remove(requestID, promise); 149 150 promise.completeExceptionally( 151 new AsynchronousException( 152 "Exception while sending JSON Request:\n" + e.getMessage() 153 + "\nSee cause for details.", e 154 ) 155 ); 156 157 return; // don't rethrow; caller already has the signal via the promise 158 } 159 } 160}