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
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.concurrent.ConcurrentHashMap;
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 implements Sender<String>
{
    // ********************************************************************************************
    // ********************************************************************************************
    // Main Fields
    // ********************************************************************************************
    // ********************************************************************************************


    // The Browser Connection
    private final WebSocket webSocket;

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


    // ********************************************************************************************
    // ********************************************************************************************
    // User-Provided Handler Fields
    // ********************************************************************************************
    // ********************************************************************************************


    final Consumer<BrowserEvent> eventHandler;

    final Consumer<RDPError> rdpErrorHandler;

    final Consumer<BrowserError> browserErrorHandler;


    // ********************************************************************************************
    // ********************************************************************************************
    // Output Log Text Fields
    // ********************************************************************************************
    // ********************************************************************************************


    private AppendableSafe raw =
        new AppendableSafe(System.out, WebSocketSender::handleLogIOE);

    private AppendableSafe app =
        new AppendableSafe(System.out, WebSocketSender::handleLogIOE);

    private AppendableSafe err =
        new AppendableSafe(System.out, WebSocketSender::handleLogIOE);

    AppendableSafe raw() { return raw; }
    AppendableSafe app() { return app; }
    AppendableSafe err() { return err; }

    /** <EMBED CLASS='external-html' DATA-FILE-ID=WSS_RAW_LOG> */
    public AppendableSafe setRawFrameLog(Appendable a)
    { return this.raw = new AppendableSafe(a, WebSocketSender::handleLogIOE); }

    /** <EMBED CLASS='external-html' DATA-FILE-ID=WSS_APP_LOG> */
    public AppendableSafe setApplicationLayerLog(Appendable a)
    { return this.app = new AppendableSafe(a, WebSocketSender::handleLogIOE); }

    /** <EMBED CLASS='external-html' DATA-FILE-ID=WSS_ERR_LOG> */
    public AppendableSafe setErrorLog(Appendable a)
    { return this.app = new AppendableSafe(a, WebSocketSender::handleLogIOE); }

    private static void handleLogIOE(final IOException ioe)
    {
        System.err.println(
            "There has been an exception throw while attempting to write to an output log.\n" +
            "Cannot Proceed"
        );

        throw new UnreachableError();
    }


    // ********************************************************************************************
    // ********************************************************************************************
    // 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(
            final String                    url,
            final Consumer<BrowserEvent>    eventHandler,
            final Consumer<RDPError>        rdpErrorHandler,
            final Consumer<BrowserError>    browserErrorHandler
        )
        throws IOException, WebSocketException
    {
        Objects.requireNonNull(url,             "Parameter 'url' has been passed null.");
        Objects.requireNonNull(eventHandler,    "Parameter 'eventHandler' has been passed null.");

        this.rdpErrorHandler = (rdpErrorHandler == null)
            ? RDPError.NO_OP
            : rdpErrorHandler;

        this.browserErrorHandler = (browserErrorHandler == null)
            ? (BrowserError be) -> {}
            : browserErrorHandler;

        this.eventHandler = eventHandler;

        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(); }

    /**
     * This method is the implementation-method for the {@link Sender} Functional-Interface.  This
     * message accepts a <B STYLE='color; red;'>Request &amp; 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)
    {
        this.promises.put(requestID, promise);

        // Print the request-message that is about to be sent, and then send it.
        app.append(BGREEN_BKGND + " Sending JSON: " + RESET + '\n' + 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
            );
        }
    }
}