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 &amp; 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}