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.concurrent.ConcurrentHashMap;
015import java.util.List;
016import java.util.Objects;
017import java.lang.reflect.Constructor;
018import java.util.function.Consumer;
019
020/**
021 * This class implements a connection to a Web-Browser using the Remote Debug Protocol over
022 * Web-Sockets.
023 * 
024 * <H3 STYLE='background: black; color: white; padding: 0.5em;'>Browser Remote Debug Protocol
025 * Connection Class</H3>
026 * 
027 * <BR />Java is capable of communicating with either a Headless instance of Google Chrome - <I>or
028 * any browser that implements the Remote Debuggin Protocol</I>.  It is not mandatory to run the
029 * browser in headless mode, but it is more common.
030 */
031@SuppressWarnings({"rawtypes", "unchecked"})
032public class WebSocketSender implements Sender<String>
033{
034    // ********************************************************************************************
035    // ********************************************************************************************
036    // Main Fields
037    // ********************************************************************************************
038    // ********************************************************************************************
039
040
041    // The Browser Connection
042    private final WebSocket webSocket;
043
044    // Stores the lists of promises
045    private ConcurrentHashMap<Integer, Promise<JsonObject, ? extends Object>> promises =
046        new ConcurrentHashMap<>();
047
048
049    // ********************************************************************************************
050    // ********************************************************************************************
051    // User-Provided Handler Fields
052    // ********************************************************************************************
053    // ********************************************************************************************
054
055
056    final Consumer<BrowserEvent> eventHandler;
057
058    final Consumer<RDPError> rdpErrorHandler;
059
060    final Consumer<BrowserError> browserErrorHandler;
061
062
063    // ********************************************************************************************
064    // ********************************************************************************************
065    // Output Log Text Fields
066    // ********************************************************************************************
067    // ********************************************************************************************
068
069
070    private AppendableSafe raw =
071        new AppendableSafe(System.out, WebSocketSender::handleLogIOE);
072
073    private AppendableSafe app =
074        new AppendableSafe(System.out, WebSocketSender::handleLogIOE);
075
076    private AppendableSafe err =
077        new AppendableSafe(System.out, WebSocketSender::handleLogIOE);
078
079    AppendableSafe raw() { return raw; }
080    AppendableSafe app() { return app; }
081    AppendableSafe err() { return err; }
082
083    /** <EMBED CLASS='external-html' DATA-FILE-ID=WSS_RAW_LOG> */
084    public AppendableSafe setRawFrameLog(Appendable a)
085    { return this.raw = new AppendableSafe(a, WebSocketSender::handleLogIOE); }
086
087    /** <EMBED CLASS='external-html' DATA-FILE-ID=WSS_APP_LOG> */
088    public AppendableSafe setApplicationLayerLog(Appendable a)
089    { return this.app = new AppendableSafe(a, WebSocketSender::handleLogIOE); }
090
091    /** <EMBED CLASS='external-html' DATA-FILE-ID=WSS_ERR_LOG> */
092    public AppendableSafe setErrorLog(Appendable a)
093    { return this.app = new AppendableSafe(a, WebSocketSender::handleLogIOE); }
094
095    private static void handleLogIOE(final IOException ioe)
096    {
097        System.err.println(
098            "There has been an exception throw while attempting to write to an output log.\n" +
099            "Cannot Proceed"
100        );
101
102        throw new UnreachableError();
103    }
104
105
106    // ********************************************************************************************
107    // ********************************************************************************************
108    // Constructor
109    // ********************************************************************************************
110    // ********************************************************************************************
111
112
113    /**
114     * Opens a Connection to a Web Browser using a Web-Socket.  This class will now be
115     * ready to accept {@link #send(int, String, Promise)} messages to the browser.
116     * 
117     * @param url This is a {@code URL} that is generated by the browser, and has a base
118     * {@code URL} that is just {@code 127.0.0.1}, followed by a <B STYLE='color:red'>port
119     * number</B>.  There will also be an <B STYLE='color:red;'>identifier-code</B>.
120     * 
121     * @throws IOException Throws if there are problems connecting the socket.
122     * 
123     * @throws WebSocketException Throws if the NeoVisionaries Package encounters a problem
124     * building the socket connection.
125     */
126    public WebSocketSender(
127            final String                    url,
128            final Consumer<BrowserEvent>    eventHandler,
129            final Consumer<RDPError>        rdpErrorHandler,
130            final Consumer<BrowserError>    browserErrorHandler
131        )
132        throws IOException, WebSocketException
133    {
134        Objects.requireNonNull(url,             "Parameter 'url' has been passed null.");
135        Objects.requireNonNull(eventHandler,    "Parameter 'eventHandler' has been passed null.");
136
137        this.rdpErrorHandler = (rdpErrorHandler == null)
138            ? RDPError.NO_OP
139            : rdpErrorHandler;
140
141        this.browserErrorHandler = (browserErrorHandler == null)
142            ? (BrowserError be) -> {}
143            : browserErrorHandler;
144
145        this.eventHandler = eventHandler;
146
147        final WebSocketListener webSocketListener =
148            new WSAdapter(this, this.promises);
149
150        this.webSocket = new WebSocketFactory()
151            .createSocket(url)
152            .addListener(webSocketListener)
153            .connect();
154
155        System.out.println
156            (BYELLOW_BKGND + BBLACK + " Web Socket Connection Opened: " + RESET + url);
157    }
158
159
160    // ********************************************************************************************
161    // ********************************************************************************************
162    // Two Instance Methods
163    // ********************************************************************************************
164    // ********************************************************************************************
165
166
167    /** Closes the {@link WebSocket} connection to the Browser's Remote Debug Port. */
168    public void disconnect() { webSocket.disconnect(); }
169
170    /**
171     * This method is the implementation-method for the {@link Sender} Functional-Interface.  This
172     * message accepts a <B STYLE='color; red;'>Request &amp; ID</B> pair, and then transmits that
173     * request to a Browser's Remote-Debugging Port over the {@code WebSocket}.  It keeps the
174     * {@link Promise} that was created by the {@link Script} that sent this request, and saves
175     * that {@code Promise} until the Web-Socket receives a response about the request.
176     * 
177     * @param requestID This may be any number.  It is used to map requests sent over the Web
178     * Socket to responses received from it.
179     * 
180     * @param requestJSON This is the JSON Method Request sent to the Browser
181     * 
182     * @param promise This is a {@code Promise} which is automatically generated by the 
183     * {@link Script} object that is sending the request.
184     */
185    public void send(int requestID, String requestJSON, Promise promise)
186    {
187        this.promises.put(requestID, promise);
188
189        // Print the request-message that is about to be sent, and then send it.
190        app.append(BGREEN_BKGND + " Sending JSON: " + RESET + '\n' + requestJSON);
191
192        try
193            { webSocket.sendText(requestJSON); }
194
195        catch (Exception e)
196        {
197            throw new AsynchronousException(
198                "When attempting to send a JSON Request, an Exception was thrown:\n" + 
199                e.getMessage() + "\nSee Exception getCause() for details.", e
200            );
201        }
202    }
203}