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
package Torello.Browser;

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

import NeoVisionaries.WebSockets.*;

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

import static Torello.Java.C.*;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import java.util.List;
import java.util.Objects;
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
{
    private static final String CTITLE =
        BCYAN_BKGND + BBLACK + StringParse.rightSpacePad(" [Class WebSocketSender]", 30) + RESET;


    // ********************************************************************************************
    // ********************************************************************************************
    // Main Fields
    // ********************************************************************************************
    // ********************************************************************************************


    /** The Browser Web-Socket Connection */
    public final WebSocket webSocket;

    // Stores the lists of promises
    private ConcurrentHashMap<Integer, Promise> promises = new ConcurrentHashMap<>();

    final ConnRecord connRec;


    // ********************************************************************************************
    // ********************************************************************************************
    // Constructor
    // ********************************************************************************************
    // ********************************************************************************************


    /**
     * Opens a Connection to a Web Browser using a Web-Socket.  This class will now be
     * ready to accept {@link #send(Script, 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(final String url, final ConnRecord connRec)
        throws IOException, WebSocketException
    {
        Objects.requireNonNull(url, "Parameter 'url' has been passed null.");
        Objects.requireNonNull(connRec, "Parameter 'connRec' has been passed null.");

        this.connRec = connRec;

        final WebSocketListener webSocketListener = new WSAdapter(this, this.promises);

        this.webSocket = new WebSocketFactory()
            .createSocket(url)
            .addListener(webSocketListener)
            .connect();

        System.out.println
            (BYELLOW_BKGND + BBLACK + " Web Socket Connection Opened: " + RESET + ' ' + url);
    }


    // ********************************************************************************************
    // ********************************************************************************************
    // Two Instance Methods
    // ********************************************************************************************
    // ********************************************************************************************


    /** Closes the {@link WebSocket} connection to the Browser's Remote Debug Port. */
    public void disconnect() { webSocket.disconnect(); }

    private final AtomicInteger messageID = new AtomicInteger(0);

    /**
     * This method transmits a 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 promise This is a {@code Promise} which is automatically generated by the 
     * {@link Script} object that is sending the request.
     */
    void send(final Script script, final Promise promise)
    {
        final int requestID = messageID.updateAndGet((int i) -> (i == Integer.MAX_VALUE) ? 0 : i + 1);

        final String jsonRequest =
            "{\"id\":" + requestID + ',' + script.requestJSONString.substring(1);

        // System.out.println("jsonRequest:\n" + jsonRequest);
        // Torello.Java.Q.BP();

        final String msg = 
            CTITLE +
            BCYAN + " Sending JSON:\n" + RESET +
            StrIndent.indent(jsonRequest, 4) + '\n';

        // Print the request-message that is about to be sent, and then send it.
        this.connRec.app(msg);

        this.promises.put(requestID, promise);

        try
            { this.webSocket.sendText(jsonRequest); }

        catch (Exception e)
        {
            final String errMsg =
                "Error attempting to send Json Request:\n" +
                e.getMessage() + "\n";

            this.connRec.err(errMsg);

            // ensure we don't leave a dangling entry
            this.promises.remove(requestID, promise);

            promise.completeExceptionally(
                new AsynchronousException(
                    "Exception while sending JSON Request:\n" + e.getMessage()
                    + "\nSee cause for details.", e
                )
            );

            return; // don't rethrow; caller already has the signal via the promise
        }
    }
}