1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 | package Torello.Browser; import javax.json.*; import java.io.*; import NeoVisionaries.WebSockets.*; import Torello.Java.*; import Torello.Java.Additional.*; import static Torello.Java.C.*; import java.util.TreeMap; import java.util.List; import java.lang.reflect.Constructor; import java.util.function.Consumer; /** * This class implements a connection to a Web-Browser using the Remote Debug Protocol over * Web-Sockets. * * <H3 STYLE='background: black; color: white; padding: 0.5em;'>Browser Remote Debug Protocol * Connection Class</H3> * * <BR />Java is capable of communicating with either a Headless instance of Google Chrome - <I>or * any browser that implements the Remote Debuggin Protocol</I>. It is not mandatory to run the * browser in headless mode, but it is more common. */ @SuppressWarnings({"rawtypes", "unchecked"}) public class WebSocketSender implements Sender<String> { // The Browser Connection private final WebSocket webSocket; private Consumer<Object> eventHandler = null; /** A Verbose Flag. This field is {@code public}, and may be set as needed.. */ public boolean QUIET; /** * An output printer. This field is {@code public}, and may be set as needed. */ public StorageWriter sw = null; // Stores the lists of promises private TreeMap<Integer, Promise<JsonObject, ? extends Object>> promises = new TreeMap<>(); /** Closes the {@link WebSocket} connection to the Browser's Remote Debug Port. */ public void disconnect() { webSocket.disconnect(); } // ******************************************************************************************** // ******************************************************************************************** // Web-Socket Message Handler // ******************************************************************************************** // ******************************************************************************************** // Top-Level Handler private void HANDLE(String message) { if (! QUIET) if (sw != null) sw.println (BYELLOW + "Received JSON Message From Chrome:\n\t" + RESET + message); JsonObject jo = Json .createReader(new StringReader(message)) .readObject(); int idReceived = jo.getInt("id", -1); String method = jo.getString("method", null); // If the WebSocket Message had an ID, then that ID should map to one of the Promises // stored in the TreeMap if (idReceived != -1) { Promise<JsonObject, ? extends Object> promise = promises.remove(idReceived); // THIS LINE DOESN'T WORK BECAUSE: The NeoVisionaries thing doesn't seem to // "bubble-up" exceptions at all... I guess it catches them, and just sort // of hangs... if (promise == null) { System.out.println( "ID Received: [" + idReceived + "], is unknown, or already handled.\n" + "Throwing Exception, WebSockets Package will Catch This." ); throw new AsynchronousException ("ID Received: [" + idReceived + "], is unknown, or already handled."); } synchronized (promise) { HANDLE_PROMISE(idReceived, promise, jo); promise.notifyAll(); } } else if (method != null) HANDLE_EVENT(jo, method); else sw.println( BRED + "UNRECOGNIZED MESSAGE RECEIEVED\n\t" + RESET + '\t' + jo.toString() ); } // The Browser has sent a message about a particular Send-Request. Report this response // to the promise. private final void HANDLE_PROMISE (int idReceived, Promise<JsonObject, ?> promise, JsonObject jo) { if (! QUIET) if (sw != null) sw.println (BCYAN + "RESPONSE RECEIVED - PROCESSING " + RESET + "(ID: " + idReceived + ")"); // Check for errors first JsonObject error = jo.getJsonObject("error"); if (error != null) { int code = error.getInt("code", -1); String errorMessage = error.getString("message", "-"); if (! QUIET) if (sw != null) sw.println (BRED + "RECEIVED ERROR RESPONSE" + RESET + " - EXITING..."); promise.acceptError(code, errorMessage); } else { JsonObject commandResultJSON = jo.getJsonObject("result"); if (commandResultJSON == null) throw new JsonException("Response JSON String did not contain a response."); promise.acceptResponse(commandResultJSON); } } // A Browser Generated Event has fired. Report this even. private void HANDLE_EVENT(JsonObject jo, String eventName) { try { eventName = eventName.replace(".", "$"); // Convert the class-name from a string to a fully-qualified class name String className = BRDPC.getEventClassName(eventName); if (className == null) { sw.println( "Un-Typed Browser Event Received:\n" + StrIndent.indent( // NOTE: This is an "imperative" when you request long-winded HTML, // this method will otherwise print hundreds, or even thousands of // lines of HTML to the screen without this. // // null ==> abbrevStr, 100 ==> Max-Line-Len, 10 ==> Max-Lines, // true ==> Compact-Consecutive-Blank-Lines StrPrint.widthHeightAbbrev(jo.toString(), null, 100, 10, true), // 4 ==> Indent by four space chars 4 )); return; } Class<?> c = Class.forName("Torello.Browser." + className); if (c == null) { sw.println( "Failed to load Event-Class: " + '[' + BCYAN + "Torello.Browser." + className + RESET + ']' ); return; } Constructor<?> ctr = c.getConstructor(JsonObject.class); JsonObject params = jo.getJsonObject("params"); Object event = null; try { event = ctr.newInstance(params); } catch (Exception e) { sw.println( "Failed to build event-class using constructor: " + '[' + BCYAN + "Torello.Browser." + className + RESET + "\n" + "Received JSON:\n" + StrIndent.indent(jo.toString(), 4) ); return; } if ((! QUIET) || (eventHandler == null)) if (sw != null) sw.println( BCYAN + "Event Received:\n" + RESET + " Event-Class: " + c.getName() + '\n' + BPURPLE + StrIndent.indent(event.toString(), 8) + RESET ); if (eventHandler != null) eventHandler.accept(event); else if (sw != null) sw.println("No Event-Handler, Event Ignored."); } catch (Exception e) { if (! QUIET) if (sw != null) sw.println (BRED + "EVENT THINGY FAILED\n" + RESET + EXCC.toString(e)); System.exit(1); } } // ******************************************************************************************** // ******************************************************************************************** // Class Constructor // ******************************************************************************************** // ******************************************************************************************** /** * Opens a Connection to a Web Browser using a Web-Socket. This class will now be * ready to accept {@link #send(int, String, Promise)} messages to the browser. * * @param url This is a {@code URL} that is generated by the browser, and has a base * {@code URL} that is just {@code 127.0.0.1}, followed by a <B STYLE='color:red'>port * number</B>. There will also be an <B STYLE='color:red;'>identifier-code</B>. * * @throws IOException Throws if there are problems connecting the socket. * * @throws WebSocketException Throws if the NeoVisionaries Package encounters a problem * building the socket connection. */ public WebSocketSender(String url, boolean quiet, Consumer<Object> eventHandler) throws IOException, WebSocketException { final WebSocketListener webSocketListener = new WSAdapter(); this.QUIET = quiet; this.eventHandler = eventHandler; this.webSocket = new WebSocketFactory() .createSocket(url) .addListener(webSocketListener) .connect(); } private class WSAdapter extends WebSocketAdapter { // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** // Message Receivers // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** @Override public void onTextMessage(WebSocket ws, String message) { HANDLE(message); } @Override public void onTextMessage(WebSocket ws, byte[] data) { System.out.println("data.length: " + data.length); Q.BP("A Data-Text Message has been received... Exit or Continue?"); } // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** // The Error-Checking Handlers // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** @Override public void onError(WebSocket ws, WebSocketException cause) { EX(cause, "onError"); } @Override public void onFrameError (WebSocket ws, WebSocketException cause, WebSocketFrame frame) { EX(cause, "onFrameError"); } @Override public void onMessageError (WebSocket ws, WebSocketException cause, List<WebSocketFrame> frames) { EX(cause, "onMessageError"); } @Override public void onMessageDecompressionError (WebSocket ws, WebSocketException cause, byte[] data) { EX(cause, "onMessageDecompressionError"); } @Override public void onTextMessageError(WebSocket ws, WebSocketException cause, byte[] data) { EX(cause, "onTextMessageError"); } @Override public void onSendError(WebSocket ws, WebSocketException cause, WebSocketFrame frame) { EX(cause, "onSendError"); } @Override public void onUnexpectedError(WebSocket ws, WebSocketException cause) { EX(cause, "onUnexpectedError"); } private void EX(Exception e, String handlerMethodName) { System.out.println( BRED + handlerMethodName + RESET + "(WebSocket, WebSocketExceptionn" + e.toString() ); e.printStackTrace(); } } // ******************************************************************************************** // ******************************************************************************************** // "Send" method implementation of this Functional Interface // ******************************************************************************************** // ******************************************************************************************** /** * This method is the implementation-method for the {@link Sender} Functional-Interface. This * message accepts a <B STYLE='color; red;'>Request & ID</B> pair, and then transmits that * request to a Browser's Remote-Debugging Port over the {@code WebSocket}. It keeps the * {@link Promise} that was created by the {@link Script} that sent this request, and saves * that {@code Promise} until the Web-Socket receives a response about the request. * * @param requestID This may be any number. It is used to map requests sent over the Web * Socket to responses received from it. * * @param requestJSON This is the JSON Method Request sent to the Browser * * @param promise This is a {@code Promise} which is automatically generated by the * {@link Script} object that is sending the request. */ public void send(int requestID, String requestJSON, Promise promise) { synchronized (promise) { promises.put(requestID, promise); // Print the request-message that is about to be sent, and then send it. if (! QUIET) if (sw != null) sw.println(BYELLOW + "Sending JSON:\n\t" + RESET + requestJSON); try { webSocket.sendText(requestJSON); } catch (Exception e) { throw new AsynchronousException( "When attempting to send a JSON Request, an Exception was thrown:\n" + e.getMessage() + "\nSee Exception getCause() for details.", e ); } } } } |