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
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
package Torello.Browser;

import java.util.*;
import javax.json.*;
import java.io.*;

import java.net.URL;
import java.util.function.Function;
import java.util.function.Consumer;

import Torello.Java.*;
import Torello.Java.Additional.*;

import static Torello.Java.C.*;

import Torello.HTML.Scrape;
import NeoVisionaries.WebSockets.WebSocketException;

/**
 * This class helps to start an headless-instance of a Web-Browser, and open a connection to 
 * that browser.
 * 
 * <EMBED CLASS='external-html' DATA-FILE-ID=BRDPC>
 */
public class BRDPC
{
    private BRDPC() { }

    // ********************************************************************************************
    // ********************************************************************************************
    // Looking up the name of all registered events
    // ********************************************************************************************
    // ********************************************************************************************


    // Converts a Browser-Event name to the **PARTIALLY-QUALIFIED** Event-Name
    @SuppressWarnings("unchecked")
    private static final TreeMap<String, String> eventNames = (TreeMap<String, String>) 
        LFEC.readObjectFromFile_JAR
            (BRDPC.class, "data-files/EventNames.tmdat", true, TreeMap.class);

    /*
     * Chrome only provides the name of the event, this will return the partially qualified name
     * of the class - <B STYLE='color: red;'><I>including the containing / parent class name, 
     * which is needed to construct an instance of the event-class</I></B>.
     * 
     * <BR /><BR /><B>NOTE:</B> The package name <B>({@code Torello.Browser})</B> is not included
     * in the returned {@code String}.
     */
    static final String getEventClassName(String eventName)
    { return eventNames.get(eventName); }


    // ********************************************************************************************
    // ********************************************************************************************
    //
    // ********************************************************************************************
    // ********************************************************************************************


    /** A singleton instance of the web-socket connection. */
    public static WebSocketSender defaultSender = null;

    // A lambda used by many of the types in this package.
    static final Function<JsonObject, Ret0> NoReturnValues = (JsonObject jo) -> Ret0.R0;

    /**
     * A commmon location for the Chrome-Binary on a Windows Machine.
     * 
     * <BR /><BR /><B STYLE='color: red;'>IMPORTANT:</B> When this class' 
     * {@link #startBrowserExecutable(boolean, Integer, String, int)} method is invoked, it
     * will attempt to run the program specified by this field.  <B><I>It is of utmost importance
     * to set this field to whatever location has your Chrome-Binary!</I></B>
     */
    public static String CHROME_EXE =
        "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe";

    /**
     * A common location for the Chome-Canary Binary on a Windows Machine.
     * 
     * <BR /><BR /><B STYLE='color: red;'>IMPORANT:</B> I don't actually know when or why one
     * would use the <B>Canary</B>-Binary, rather than the standard <B>Chrome</B> executable.
     * I have read that the original intent of this particular application (which is a full 
     * fledged instance of <B>Chrome</B>) - was to provide a headless version of the standard
     * Google-Browser.
     * 
     * <BR /><BR />The current version of a <B>Chrome</B>-Executable seems to accept
     * {@code Remote Debug Port} commands quite readily, without having to use <B>Canary</B>.
     * I will leave it here as a discussion-point.
     */
    public static String CANARY_EXE =
        "C:\\Users\\ralph\\AppData\\Local\\Google\\Chrome SxS\\Application\\Chrome";

    /**
     * Configuration-flag for setting the verbosity of this library-package.
     * 
     * <BR /><BR />To sent more vebose messages to your {@code StorageWriter} instance (field
     * {@link #sw}), change the value of this field to {@code TRUE}.
     */
    public static boolean QUIET = false;

    /**
     * The log output.  Output text will be sent to this field.  This field is not declared 
     * {@code final}, and (obviously) may be set to any desired log output.  This field is not
     * exactly thread-safe - <I>it is shared by all threads which use the headless browser
     * library tools!</I>
     * 
     * <BR /><BR />Because web-sockets, themselves are a multi-threaded application, there isn't
     * really a way to identify which thread has caused a particular response.  Some percentage of
     * the messages received from the browser will be events, and in a multi-threaded application
     * of this tool, there would be no way to identify which thread caused which event.
     */
    public static StorageWriter sw = new StorageWriter();


    // ********************************************************************************************
    // ********************************************************************************************
    // Starting the Browser, from Java rather than the command-line
    // ********************************************************************************************
    // ********************************************************************************************


    /**
     * <H3 CLASS=JDBanner>A browser-instance must be loaded before using this package's commands
     * </H3>
     * 
     * <BR />
     * Starts a headless (or full/complete) browser instance.  All this method does is use the 
     * Standard-Java {@code java.lang.Process} class to start an operating-system executable.  This
     * just means invoking the executable specified by {@link #CHROME_EXE}.
     * 
     * @param headless If {@code FALSE} is passed to this method, then the option {@code -headless}
     * will not be passed to the browser at startup.  In such scenarios, an actual browser should
     * popup on your desktop.  Normally, this parameter should receive {@code TRUE}.
     * 
     * @param port  It is standard operating procedure to pass {@code 9222} to this parameter.
     * {@code 9223} is also very common.  You may pass null to this parameter, and the default
     * value will be used (which is {@code 9223}).
     * 
     * @param initialURL You may elect to have the page open at the specified {@code URL}.  This
     * parameter may be null, and when it is, it shall be ignored.
     * 
     * @param maxCount Some delay is inserted between the starting of a browser executable, and
     * this method exiting.
     * 
     * @throws IOException If there are any web-socket or http problems when attempting to connect
     * to the browser.
     */
    public static void startBrowserExecutable
        (boolean headless, Integer port, String initialURL, int maxCount)
        throws IOException
    {
        if (port == null) port = 9223;

        if (maxCount < 2) maxCount = 2;

        Vector<String> comm = new Vector<>();

        comm.add(CHROME_EXE);

        if (! headless) comm.add("-disable-gpu");

        comm.add("-remote-debugging-port=" + port);

        if (headless) comm.add("-headless");

        // I don't know as much about Google-Chrome as you might think...
        // I don't know what this does...
        // comm.add("-no-sandbox");

        if (initialURL != null) comm.add(initialURL);

        String[] COMM = comm.toArray(new String[0]);

        for (String s : COMM) System.out.print(s + " "); System.out.println();

        Process pro = java.lang.Runtime.getRuntime().exec(COMM);

        R reader1 = new R(pro.getInputStream());
        R reader2 = new R(pro.getErrorStream());

        Thread t1 = new Thread(reader1);
        Thread t2 = new Thread(reader2);

        t1.setDaemon(true);
        t2.setDaemon(true);

        t1.start();
        t2.start();

        int i = 0;

        System.out.println("Waiting to read the code.");

        int counter = 0;
        while (reader1.stillReading && reader2.stillReading)
            if (i++ % 500000000 == 0)
            {
                System.out.print(".");
                if (++counter == maxCount) break;
            }
    }

    private static class R implements Runnable
    {
        private BufferedReader  br;
        public  boolean         stillReading = true;

        public R(InputStream is)
        { this.br = new BufferedReader(new InputStreamReader(is)); }

        public void run()
        {
            try
            {
                String  s       = null;
                long    time    = System.currentTimeMillis();

                while ((s = br.readLine()) != null)
                {
                    System.out.println(s);

                    long current = System.currentTimeMillis();

                    if ((time - current) > 4000) break;

                    time = current;
                }
                stillReading = false;
            }

            catch (IOException ioe)
                { System.out.println(EXCC.toString(ioe));  System.exit(0); }
        }
    }


    // ********************************************************************************************
    // ********************************************************************************************
    // Retrieving Browser Information
    // ********************************************************************************************
    // ********************************************************************************************


    /**
     * You may use this method (if you so choose, for whatever reason) to retrieve the Remote
     * Debugging Port API from Google.  This method will return a (very long) JSON-File, as a
     * {@code java.lang.String}
     * 
     * @param port This should be the port that was used to start the server.  This is, by default,
     * port {@code 9223}.
     * 
     * @return A very-long JSON-{@code String} containing the currently-exported API that your
     * web-browser provides.
     * 
     * @throws IOException If there are problems sending this request, or retrieving the response,
     * this throws.
     */
    public static String readAPIFromServer(Integer port) throws IOException
    {
        if (port == null) port = 9223;

        String  urlStr  = "http://127.0.0.1:" + port + "/json/protocol";
        URL     url     = new URL(urlStr);

        System.out.println("Querying WS Server for JSON\n" + url);

        // NOTE: This is a very large JSON File - more than 20,000 lines of text
        return Scrape.scrapePage(url);
    }

    /**
     * This is supposed to be the easy part.  There is a very long "Identifier String" that
     * contains some connection information.
     * 
     * <BR /><BR /><B STYLE='color: red;'>NOTE:</B> There is really no need to use this method,
     * as this "Connection Stuff" is all taken care of inside this class (and the class
     * {@link WebSocketSender}).
     * 
     * <BR /><BR />If, for some reason, there is a need to sue the feature of having multiple
     * browser connections, and it is deemed important to build your own instance of 
     * {@link WebSocketSender} - <I>you will have to retrieve this {@code URL} information in order
     * to construct a {@link WebSocketSender} instance.</I>
     * 
     * @param port The browser-port upon. which the {@code WebSocket} was connected
     * 
     * @return An instance of {@link Ret2}:
     * 
     * <BR /><BR /><UL CLASS=JDUL>
     * <LI><B>{@code String}</B> (Browser {@code URL})
     *      <BR /><BR />The complete {@code URL} to use when connecting to the browser over its
     *      RDP Connection.
     *      <BR /><BR />
     *      </LI>
     * <LI><B>{@code String}</B> (Browser {@code URL})
     *      <BR /><BR />A "Connection Code" that the browser uses to identify instances of a 
     *      Web-Socket Connection.
     *      <BR /><BR />
     *      </LI>
     * </UL>
     * 
     * @throws IOException When connecting to the browser, this is always a possibility.
     */
    public static Ret2<String, String> readURLToUseFromServer(Integer port) throws IOException
    {
        if (port == null) port = 9223;

        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // Retrieve the Web-Socket Address from URL - Attemt #1
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

        URL url = new URL("http://127.0.0.1:" + port + "/json/list");

        if (! QUIET) sw.println(
            '\n' +
            BRED + "ATTEMPT #1:" + RESET + " (Works when there is already a page opened)\n" +
            "Querying WebSocketServer for Listening Address Using:\n\t" +
            BYELLOW + url.toString() + RESET
        );

        try
        {
            String          json    = Scrape.scrapePage(url);
            StringReader    sr      = new StringReader(json);

            if (! QUIET) sw.println("Attempt #1, Server Responded With:\n" + json);

            if (json.length() > 10) // else it gave us an empty response
            {
                JsonArray    jArr    = Json.createReader(sr).readArray();
                JsonObject   jObj    = null; // used in next-section

                if ((jArr != null) && (jArr.size() > 0))
                {        
                    String res  = jArr.getJsonObject(0).getString("webSocketDebuggerUrl", null);
                    String code = StringParse.fromLastFrontSlashPos(res);

                    if (res != null) return new Ret2<>(res, code);
                }
            }
        }

        catch (Exception e)
        {
            sw.println(
                BGREEN + "NOTE: FOR ATTEMPT #1, THIS IS SOMEWHAT EXPECTED" + RESET + '\n' +
                BRED + "Exception Message Attempt #1:\n\t" + RESET +
                e.getMessage() + '\n' +
                "-------------------------------------------------------------------\n" +
                EXCC.toString(e) +
                "-------------------------------------------------------------------\n"
            );
        }


        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // Retrieve the Web-Socket Address from URL #2
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

        url = new URL("http://127.0.0.1:" + port + "/json/version");

        // Again, I'm not 100% what chrome is really doing here...
        // url = new URL("http://127.0.0.1/json/version");

        if (! QUIET) sw.println(
            BRED + "ATTEMPT #2:" + RESET + " (Creates an entirely new connection)\n" +
            "Querying WebSocketServer for Listening Address Using:\n\t" +
            BYELLOW + url.toString() + RESET
        );

        try
        {
            String          json    = Scrape.scrapePage(url);
            StringReader    sr      = new StringReader(json);
            JsonObject      jObj    = Json.createReader(sr).readObject();

            if (! QUIET)
                sw.println("Attempt #2, Server Responded With:\n" + json);

            if (jObj != null)
            {
                String res  = jObj.getString("webSocketDebuggerUrl", null);
                String code = StringParse.fromLastFrontSlashPos(res);

                sw.println(
                    "res:  " + res + '\n' +
                    "code: " + code
                );

                if ((res != null) && res.startsWith("ws://")) return new Ret2<>(res, code);
            }
        }

        catch (Exception e)
        {
            sw.println(
                BRED + "Exception Message Attempt #2:\n\t" + RESET +
                e.getMessage() + '\n' +
                "-------------------------------------------------------------------\n" +
                EXCC.toString(e) +
                "-------------------------------------------------------------------\n"
            );
        }
    
        // Failed
        sw.println(BRED + "Unable to retrieve Chrome Web-Socket URL.  Sorry buddy." + RESET);
        return null;
    }

    /**
     * After an instance of Chrome has been started, this method should be used to create a 
     * <B>{@code WebSocket}</B>-Connection to the browser.
     * 
     * <BR /><BR /><H2 CLASS=JDBanner>You must invoked this method before sending commands</H2>
     * 
     * <I CLASS=JDBanner>After starting a browser instance using the method 
     * {@link #startBrowserExecutable(boolean, Integer, String, int)}, you will also have to open
     * a Web-Socket Connection to the browser using this method.  Then (and only then), is it 
     * possible to begin sending requets to the browser.</I>
     * 
     * @param port You should just elect to use {@code 9223}, that is what is recommended by all of
     * the sources on that Internet that explain this browser-feature.  Chrome will be listening on
     * whatever port you started the headless version with.
     * 
     * <BR /><BR /><B>NOTE:</B> You don't have to start the browser in headless mode.  Controlling
     * a full-fleged, and opened, browser is allowed with Chrome.
     * 
     * @param quiet You may configure the verbosity-level for the
     * <B>{@code Websocket}</B>-Connection to be different from the verbosity-level for this class
     * (class {@code BRDPC}).
     * 
     * @param eventHandler It is advisable to register an event-handler.  This parameter may be
     * passed null, and if it is, events will not be handled (they are then ignored).
     * 
     * @throws IOException Connecting to the browser may cause this exception.
     * 
     * @throws WebSocketException The {@code NeoVisionaries} package for {@code WebSockets} may
     * throw this when attempting to build the connection.
     */
    public static boolean buildDefaultWebSocketConnection
        (Integer port, boolean quiet, Consumer<BrowserEvent> eventHandler)
        throws IOException, WebSocketException
    {
        Ret2<String, String> browserListenerURL = readURLToUseFromServer(port);

        if ((browserListenerURL == null) || (browserListenerURL.a == null))
        {
            sw.println("Unable to retrieve a Web-Socket Connection.  No Request URL's");
            return false;
        }

        BRDPC.defaultSender = new WebSocketSender(
            browserListenerURL.a, quiet,
            (eventHandler == null)
                ? null
                : (Object o) -> eventHandler.accept((BrowserEvent) o)
        );

        if (! QUIET) sw.println
            ("Web Socket Connection Opened:\n" + BYELLOW + browserListenerURL.a + RESET);

        return true;
    }    


    // ********************************************************************************************
    // ********************************************************************************************
    // Exception throw methdods used by all of the Library-Classes
    // ********************************************************************************************
    // ********************************************************************************************


    /** Configuration to Override the null-check. */
    public static final boolean ALLOW_NULLABLE_PARAMETERS = false;

    /**
     * In order to differentiate between <B>{@code 'optional'}</B> (and required) parameters and
     * fields, quite an amount of effort was invested in checking that required variables are
     * not allowed to be null.
     * 
     * <BR /><BR />This method ensures a consistent-looking error message whenever there is a
     * null parameter passed that should not have been null.
     */
    static void throwNPE(String requiredPropertyParameterName)
    {
        if (ALLOW_NULLABLE_PARAMETERS) return;

        throw new NullPointerException(
            "The parameter '" + requiredPropertyParameterName + "' is not marked is optional. " +
            "This means that null may not be passed to this parameter in this method.  However " +
            "this parameter received a null value."
        );
    }

    /**
     * In addition to null checks, a lot of the Browser's API provides its own version of the
     * Java {@code 'enum'} by listing valid-{@code String's} (as pre-defined lists) that an
     * {@code String} parameter or field will accept.
     * 
     * <BR /><BR />If a programmer ever provides a {@code String} to a {@code String} parameter
     * or field for which an enumerated-list of valid-values of the parameter or field has been
     * provided, <I>but that value doesn't meet the enumerated-list's requiremets</I>, then an
     * {@code IllegalArgumentException} will throw.
     * 
     * @param parameterName The name of the offending parameter.
     * @param parameterValue The {@code String}-Value of the offending parameter.
     * 
     * @throws IllegalArgumentException This method shall this exception, everytime.
     */
    static void throwIAE(String parameterName, String parameterValue, String... enumStrArr)
    {
        // NOTE: This presumes that 'non-optional' parameters have already been null-checked
        if (parameterValue == null) return;

        if (StrCmpr.equalsNAND(parameterValue, enumStrArr))

            throw new IllegalArgumentException(
                "The parameter '" + parameterName + "' is a String parameter whose values are " +
                "limited to the Enumerated Strings listed in this method's documentation page.  " +
                "Unfortunately, this method received [" + parameterValue + "] as a String, " +
                "which is not among the listed values for this String Parameter."
            );
    }

    /**
     * Also throws exception for enumerated strings
     */
    static void checkIAE
        (String parameterName, String parameterValue, String enumName, String... enumStrArr)
    {
        // NOTE: This presumes that 'non-optional' parameters have already been null-checked
        if (parameterValue == null) return;

        if (StrCmpr.equalsNAND(parameterValue, enumStrArr))

            throw new IllegalArgumentException(
                "The parameter '" + parameterName + "' is a String parameter whose values are " +
                "limited to the Enumerated Strings listed in String-Array[] '" + enumName + "'.  " +
                "Unfortunately, this method received [" + parameterValue + "] as a String, " +
                "and that is not among the listed values for the String Parameter."
            );
    }
}