001package Torello.Browser;
002
003import javax.json.*;
004import java.io.*;
005
006import NeoVisionaries.WebSockets.*;
007
008import Torello.Java.*;
009import Torello.Java.Additional.AppendableSafe;
010
011import static Torello.Java.C.*;
012
013import java.util.concurrent.ConcurrentHashMap;
014import java.util.concurrent.atomic.AtomicInteger;
015
016import java.util.List;
017import java.util.Objects;
018import java.lang.reflect.Constructor;
019import java.util.function.Consumer;
020
021/**
022 * This class implements a connection to a Web-Browser using the Remote Debug Protocol over
023 * Web-Sockets.
024 * 
025 * <H3 STYLE='background: black; color: white; padding: 0.5em;'>Browser Remote Debug Protocol
026 * Connection Class</H3>
027 * 
028 * <BR />Java is capable of communicating with either a Headless instance of Google Chrome - <I>or
029 * any browser that implements the Remote Debuggin Protocol</I>.  It is not mandatory to run the
030 * browser in headless mode, but it is more common.
031 */
032@SuppressWarnings({"rawtypes", "unchecked"})
033public class WebSocketSender
034{
035    private static final String CTITLE =
036        BCYAN_BKGND + BBLACK + StringParse.rightSpacePad(" [Class WebSocketSender]", 30) + RESET;
037
038
039    // ********************************************************************************************
040    // ********************************************************************************************
041    // Main Fields
042    // ********************************************************************************************
043    // ********************************************************************************************
044
045
046    /** The Browser Web-Socket Connection */
047    public final WebSocket webSocket;
048
049    // Stores the lists of promises
050    private ConcurrentHashMap<Integer, Promise> promises = new ConcurrentHashMap<>();
051
052    final ConnRecord connRec;
053
054
055    // ********************************************************************************************
056    // ********************************************************************************************
057    // Constructor
058    // ********************************************************************************************
059    // ********************************************************************************************
060
061
062    /**
063     * Opens a Connection to a Web Browser using a Web-Socket.  This class will now be
064     * ready to accept {@link #send(Script, Promise)} messages to the browser.
065     * 
066     * @param url This is a {@code URL} that is generated by the browser, and has a base
067     * {@code URL} that is just {@code 127.0.0.1}, followed by a <B STYLE='color:red'>port
068     * number</B>.  There will also be an <B STYLE='color:red;'>identifier-code</B>.
069     * 
070     * @throws IOException Throws if there are problems connecting the socket.
071     * 
072     * @throws WebSocketException Throws if the NeoVisionaries Package encounters a problem
073     * building the socket connection.
074     */
075    public WebSocketSender(final String url, final ConnRecord connRec)
076        throws IOException, WebSocketException
077    {
078        Objects.requireNonNull(url, "Parameter 'url' has been passed null.");
079        Objects.requireNonNull(connRec, "Parameter 'connRec' has been passed null.");
080
081        this.connRec = connRec;
082
083        final WebSocketListener webSocketListener = new WSAdapter(this, this.promises);
084
085        this.webSocket = new WebSocketFactory()
086            .createSocket(url)
087            .addListener(webSocketListener)
088            .connect();
089
090        System.out.println
091            (BYELLOW_BKGND + BBLACK + " Web Socket Connection Opened: " + RESET + ' ' + url);
092    }
093
094
095    // ********************************************************************************************
096    // ********************************************************************************************
097    // Two Instance Methods
098    // ********************************************************************************************
099    // ********************************************************************************************
100
101
102    /** Closes the {@link WebSocket} connection to the Browser's Remote Debug Port. */
103    public void disconnect() { webSocket.disconnect(); }
104
105    private final AtomicInteger messageID = new AtomicInteger(0);
106
107    /**
108     * This method transmits a request to a Browser's Remote-Debugging Port over the
109     * {@code WebSocket}.  It keeps the {@link Promise} that was created by the {@link Script} that
110     * sent this request, and saves that {@code Promise} until the Web-Socket receives a response
111     * about the request.
112     * 
113     * @param promise This is a {@code Promise} which is automatically generated by the 
114     * {@link Script} object that is sending the request.
115     */
116    void send(final Script script, final Promise promise)
117    {
118        final int requestID = messageID.updateAndGet((int i) -> (i == Integer.MAX_VALUE) ? 0 : i + 1);
119
120        final String jsonRequest =
121            "{\"id\":" + requestID + ',' + script.requestJSONString.substring(1);
122
123        // System.out.println("jsonRequest:\n" + jsonRequest);
124        // Torello.Java.Q.BP();
125
126        final String msg = 
127            CTITLE +
128            BCYAN + " Sending JSON:\n" + RESET +
129            StrIndent.indent(jsonRequest, 4) + '\n';
130
131        // Print the request-message that is about to be sent, and then send it.
132        this.connRec.app(msg);
133
134        this.promises.put(requestID, promise);
135
136        try
137            { this.webSocket.sendText(jsonRequest); }
138
139        catch (Exception e)
140        {
141            final String errMsg =
142                "Error attempting to send Json Request:\n" +
143                e.getMessage() + "\n";
144
145            this.connRec.err(errMsg);
146
147            // ensure we don't leave a dangling entry
148            this.promises.remove(requestID, promise);
149
150            promise.completeExceptionally(
151                new AsynchronousException(
152                    "Exception while sending JSON Request:\n" + e.getMessage()
153                    + "\nSee cause for details.", e
154                )
155            );
156
157            return; // don't rethrow; caller already has the signal via the promise
158        }
159    }
160}