001package Torello.Browser; 002 003 004import javax.json.*; 005import java.io.*; 006 007import NeoVisionaries.WebSockets.*; 008 009import Torello.Java.*; 010import Torello.Java.Additional.*; 011 012import static Torello.Java.C.*; 013 014import java.util.TreeMap; 015import java.util.List; 016import java.lang.reflect.Constructor; 017import java.util.function.Consumer; 018 019/** 020 * This class implements a connection to a Web-Browser using the Remote Debug Protocol over 021 * Web-Sockets. 022 * 023 * <H3 STYLE='background: black; color: white; padding: 0.5em;'>Browser Remote Debug Protocol 024 * Connection Class</H3> 025 * 026 * <BR />Java is capable of communicating with either a Headless instance of Google Chrome - <I>or 027 * any browser that implements the Remote Debuggin Protocol</I>. It is not mandatory to run the 028 * browser in headless mode, but it is more common. 029 */ 030@SuppressWarnings({"rawtypes", "unchecked"}) 031public class WebSocketSender implements Sender<String> 032{ 033 // The Browser Connection 034 private final WebSocket webSocket; 035 036 private Consumer<Object> eventHandler = null; 037 038 /** A Verbose Flag. This field is {@code public}, and may be set as needed.. */ 039 public boolean QUIET; 040 041 /** 042 * An output printer. This field is {@code public}, and may be set as needed. 043 */ 044 public StorageWriter sw = null; 045 046 // Stores the lists of promises 047 private TreeMap<Integer, Promise<JsonObject, ? extends Object>> promises = new TreeMap<>(); 048 049 /** Closes the {@link WebSocket} connection to the Browser's Remote Debug Port. */ 050 public void disconnect() { webSocket.disconnect(); } 051 052 053 // ******************************************************************************************** 054 // ******************************************************************************************** 055 // Web-Socket Message Handler 056 // ******************************************************************************************** 057 // ******************************************************************************************** 058 059 060 // Top-Level Handler 061 private void HANDLE(String message) 062 { 063 if (! QUIET) if (sw != null) sw.println 064 (BYELLOW + "Received JSON Message From Chrome:\n\t" + RESET + message); 065 066 JsonObject jo = Json 067 .createReader(new StringReader(message)) 068 .readObject(); 069 070 int idReceived = jo.getInt("id", -1); 071 String method = jo.getString("method", null); 072 073 // If the WebSocket Message had an ID, then that ID should map to one of the Promises 074 // stored in the TreeMap 075 076 if (idReceived != -1) 077 { 078 Promise<JsonObject, ? extends Object> promise = promises.remove(idReceived); 079 080 // THIS LINE DOESN'T WORK BECAUSE: The NeoVisionaries thing doesn't seem to 081 // "bubble-up" exceptions at all... I guess it catches them, and just sort 082 // of hangs... 083 084 if (promise == null) 085 { 086 System.out.println( 087 "ID Received: [" + idReceived + "], is unknown, or already handled.\n" + 088 "Throwing Exception, WebSockets Package will Catch This." 089 ); 090 091 throw new AsynchronousException 092 ("ID Received: [" + idReceived + "], is unknown, or already handled."); 093 } 094 095 synchronized (promise) 096 { HANDLE_PROMISE(idReceived, promise, jo); promise.notifyAll(); } 097 } 098 099 else if (method != null) HANDLE_EVENT(jo, method); 100 101 else 102 103 sw.println( 104 BRED + "UNRECOGNIZED MESSAGE RECEIEVED\n\t" + RESET + '\t' + 105 jo.toString() 106 ); 107 } 108 109 // The Browser has sent a message about a particular Send-Request. Report this response 110 // to the promise. 111 private final void HANDLE_PROMISE 112 (int idReceived, Promise<JsonObject, ?> promise, JsonObject jo) 113 { 114 if (! QUIET) if (sw != null) sw.println 115 (BCYAN + "RESPONSE RECEIVED - PROCESSING " + RESET + "(ID: " + idReceived + ")"); 116 117 // Check for errors first 118 JsonObject error = jo.getJsonObject("error"); 119 120 if (error != null) 121 { 122 int code = error.getInt("code", -1); 123 String errorMessage = error.getString("message", "-"); 124 125 if (! QUIET) if (sw != null) sw.println 126 (BRED + "RECEIVED ERROR RESPONSE" + RESET + " - EXITING..."); 127 128 promise.acceptError(code, errorMessage); 129 } 130 131 else 132 { 133 JsonObject commandResultJSON = jo.getJsonObject("result"); 134 135 if (commandResultJSON == null) 136 throw new JsonException("Response JSON String did not contain a response."); 137 138 promise.acceptResponse(commandResultJSON); 139 } 140 } 141 142 // A Browser Generated Event has fired. Report this even. 143 private void HANDLE_EVENT(JsonObject jo, String eventName) 144 { 145 try 146 { 147 eventName = eventName.replace(".", "$"); 148 149 // Convert the class-name from a string to a fully-qualified class name 150 String className = BRDPC.getEventClassName(eventName); 151 152 if (className == null) 153 { 154 sw.println( 155 "Un-Typed Browser Event Received:\n" + 156 157 StrIndent.indent( 158 159 // NOTE: This is an "imperative" when you request long-winded HTML, 160 // this method will otherwise print hundreds, or even thousands of 161 // lines of HTML to the screen without this. 162 // 163 // null ==> abbrevStr, 100 ==> Max-Line-Len, 10 ==> Max-Lines, 164 // true ==> Compact-Consecutive-Blank-Lines 165 166 StrPrint.widthHeightAbbrev(jo.toString(), null, 100, 10, true), 167 168 // 4 ==> Indent by four space chars 169 4 170 )); 171 172 return; 173 } 174 175 Class<?> c = Class.forName("Torello.Browser." + className); 176 177 if (c == null) 178 { 179 sw.println( 180 "Failed to load Event-Class: " + 181 '[' + BCYAN + "Torello.Browser." + className + RESET + ']' 182 ); 183 184 return; 185 } 186 187 Constructor<?> ctr = c.getConstructor(JsonObject.class); 188 JsonObject params = jo.getJsonObject("params"); 189 Object event = null; 190 191 try 192 { event = ctr.newInstance(params); } 193 194 catch (Exception e) 195 { 196 sw.println( 197 "Failed to build event-class using constructor: " + 198 '[' + BCYAN + "Torello.Browser." + className + RESET + "\n" + 199 "Received JSON:\n" + 200 StrIndent.indent(jo.toString(), 4) 201 ); 202 203 return; 204 } 205 206 if ((! QUIET) || (eventHandler == null)) if (sw != null) sw.println( 207 BCYAN + "Event Received:\n" + RESET + 208 " Event-Class: " + c.getName() + '\n' + 209 BPURPLE + StrIndent.indent(event.toString(), 8) + RESET 210 ); 211 212 if (eventHandler != null) eventHandler.accept(event); 213 else if (sw != null) sw.println("No Event-Handler, Event Ignored."); 214 } 215 216 catch (Exception e) 217 { 218 if (! QUIET) if (sw != null) sw.println 219 (BRED + "EVENT THINGY FAILED\n" + RESET + EXCC.toString(e)); 220 221 System.exit(1); 222 } 223 } 224 225 226 // ******************************************************************************************** 227 // ******************************************************************************************** 228 // Class Constructor 229 // ******************************************************************************************** 230 // ******************************************************************************************** 231 232 233 /** 234 * Opens a Connection to a Web Browser using a Web-Socket. This class will now be 235 * ready to accept {@link #send(int, String, Promise)} messages to the browser. 236 * 237 * @param url This is a {@code URL} that is generated by the browser, and has a base 238 * {@code URL} that is just {@code 127.0.0.1}, followed by a <B STYLE='color:red'>port 239 * number</B>. There will also be an <B STYLE='color:red;'>identifier-code</B>. 240 * 241 * @throws IOException Throws if there are problems connecting the socket. 242 * 243 * @throws WebSocketException Throws if the NeoVisionaries Package encounters a problem 244 * building the socket connection. 245 */ 246 public WebSocketSender(String url, boolean quiet, Consumer<Object> eventHandler) 247 throws IOException, WebSocketException 248 { 249 final WebSocketListener webSocketListener = new WSAdapter(); 250 251 this.QUIET = quiet; 252 this.eventHandler = eventHandler; 253 254 this.webSocket = new WebSocketFactory() 255 .createSocket(url) 256 .addListener(webSocketListener) 257 .connect(); 258 } 259 260 private class WSAdapter extends WebSocketAdapter 261 { 262 // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 263 // Message Receivers 264 // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 265 266 @Override 267 public void onTextMessage(WebSocket ws, String message) 268 { HANDLE(message); } 269 270 @Override 271 public void onTextMessage(WebSocket ws, byte[] data) 272 { 273 System.out.println("data.length: " + data.length); 274 Q.BP("A Data-Text Message has been received... Exit or Continue?"); 275 } 276 277 278 // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 279 // The Error-Checking Handlers 280 // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 281 282 @Override 283 public void onError(WebSocket ws, WebSocketException cause) 284 { EX(cause, "onError"); } 285 286 @Override 287 public void onFrameError 288 (WebSocket ws, WebSocketException cause, WebSocketFrame frame) 289 { EX(cause, "onFrameError"); } 290 291 @Override 292 public void onMessageError 293 (WebSocket ws, WebSocketException cause, List<WebSocketFrame> frames) 294 { EX(cause, "onMessageError"); } 295 296 @Override 297 public void onMessageDecompressionError 298 (WebSocket ws, WebSocketException cause, byte[] data) 299 { EX(cause, "onMessageDecompressionError"); } 300 301 @Override 302 public void onTextMessageError(WebSocket ws, WebSocketException cause, byte[] data) 303 { EX(cause, "onTextMessageError"); } 304 305 @Override 306 public void onSendError(WebSocket ws, WebSocketException cause, WebSocketFrame frame) 307 { EX(cause, "onSendError"); } 308 309 @Override 310 public void onUnexpectedError(WebSocket ws, WebSocketException cause) 311 { EX(cause, "onUnexpectedError"); } 312 313 private void EX(Exception e, String handlerMethodName) 314 { 315 System.out.println( 316 BRED + handlerMethodName + RESET + "(WebSocket, WebSocketExceptionn" + 317 e.toString() 318 ); 319 320 e.printStackTrace(); 321 } 322 } 323 324 // ******************************************************************************************** 325 // ******************************************************************************************** 326 // "Send" method implementation of this Functional Interface 327 // ******************************************************************************************** 328 // ******************************************************************************************** 329 330 331 /** 332 * This method is the implementation-method for the {@link Sender} Functional-Interface. This 333 * message accepts a <B STYLE='color; red;'>Request & ID</B> pair, and then transmits that 334 * request to a Browser's Remote-Debugging Port over the {@code WebSocket}. It keeps the 335 * {@link Promise} that was created by the {@link Script} that sent this request, and saves 336 * that {@code Promise} until the Web-Socket receives a response about the request. 337 * 338 * @param requestID This may be any number. It is used to map requests sent over the Web 339 * Socket to responses received from it. 340 * 341 * @param requestJSON This is the JSON Method Request sent to the Browser 342 * 343 * @param promise This is a {@code Promise} which is automatically generated by the 344 * {@link Script} object that is sending the request. 345 */ 346 public void send(int requestID, String requestJSON, Promise promise) 347 { 348 synchronized (promise) 349 { 350 promises.put(requestID, promise); 351 352 // Print the request-message that is about to be sent, and then send it. 353 if (! QUIET) if (sw != null) 354 sw.println(BYELLOW + "Sending JSON:\n\t" + RESET + requestJSON); 355 356 try 357 { webSocket.sendText(requestJSON); } 358 359 catch (Exception e) 360 { 361 throw new AsynchronousException( 362 "When attempting to send a JSON Request, an Exception was thrown:\n" + 363 e.getMessage() + "\nSee Exception getCause() for details.", e 364 ); 365 } 366 } 367 } 368}