001package Torello.Java.Additional;
002
003import java.util.function.Function;
004import javax.json.JsonObject;
005
006/**
007 * A promise keeps the processing logic for converting a response from an asychronous connection
008 * into a result that the end-user can utilize.
009 * 
010 * <BR /><BR /><EMBED CLASS='external-html' DATA-FILE-ID=PROMISE>
011 * 
012 * @param <RESPONSE>    <EMBED CLASS="external-html" DATA-FILE-ID=RESPONSE>
013 * @param <RESULT>      <EMBED CLASS='external-html' DATA-FILE-ID=RESULT>
014 */
015public class Promise<RESPONSE, RESULT> implements java.io.Serializable
016{
017    /** <EMBED CLASS='external-html' DATA-FILE-ID=SVUID> */
018    protected static final long serialVersionUID = 1;
019
020    // This value is updated by the 'acceptResponse' method
021    private RESULT result = null;
022
023    // If an error has occured, these two fields will hold more information
024    private boolean response        = false;
025    private boolean error           = false;
026    private int     errorCode       = 0;
027    private String  errorMessage    = null;
028
029    /**
030     * Asks {@code 'this'} instance of {@code Promise} whether or not an error has been reported.
031     * 
032     * @return {@code TRUE} if there were errors reported by the asynchronous channel, and
033     * {@code FALSE} otherwise.
034     */
035    public synchronized boolean hadError() { return error; }
036
037    /**
038     * Asks {@code 'this'} instance of {@code Promise} whether any response has been reported.
039     * This shall return {@code TRUE} if <I>either an error, or a response has been received</I>.
040     * 
041     * @return {@code TRUE} if a response or an error has been reported to {@code 'this'} instance,
042     * and {@code FALSE} otherwise.
043     */
044    public synchronized boolean hadResponse() { return response || error; }
045
046    /**
047     * Gets all current-state of this {@code Promise}
048     * 
049     * @return The current state of this {@code Promise}, as an instance of {@link Ret5}.
050     * 
051     * <BR /><BR /><UL CLASS=JDUL>
052     * <LI>{@code Ret5.a (Boolean)}:
053     *      <BR />Whether or not a response has been reported
054     *      <BR /><BR /></LI>
055     * <LI> {@code Ret5.b (Boolean)}:
056     *      <BR />Whether or not an error has been reported
057     *      <BR /><BR /></LI>
058     * <LI> {@code Ret5.c (RESULT)}:
059     *      <BR />The result that has been returned, or null no result has been received
060     *      <BR /><BR /></LI>
061     * <LI> {@code Ret5.d (Integer)}:
062     *      <BR />The error-code that was received, or 0 if no errors have been reported
063     *      <BR /><BR /></LI>
064     * <LI> {@code Ret5.e (String)}:
065     *      <BR />The error-message (if an error was reported), or null.
066     *      </LI>
067     * </UL> 
068     */
069    public synchronized Ret5<Boolean, Boolean, RESULT, Integer, String> getState()
070    { return new Ret5<>(response, error, result, errorCode, errorMessage); }
071
072    /**
073     * This allows a user to retrieve the result of {@code 'this'} asynchronous {@code Promise}.
074     * Note that if no result has been received yet, this method throws an exception.
075     * 
076     * @return The result of the asynchronous call.
077     * 
078     * @throws AynchronousException This throws if this {@code Promise} has not received a result
079     * yet.
080     */
081    public synchronized RESULT result()
082    {
083        if (! response) throw new AsynchronousException("No response has been received yet");
084        return result;
085    }
086
087    /**
088     * This is the "Respone Processor".  As of the writing of this class, the only use that the
089     * three classes: Script, Promise &amp; Sender has been for implementing the Google Chrome
090     * Browser Remote Debug Protocol.  (Although perhaps these classes will one day be used with a
091     * different asychronous protocol)
092     * 
093     * In this current case (Browser RDP), this receiver is actually doing the "JSON Binding" to
094     * bind the JsonObject received from the Browser into a Java Class that the user can actually
095     * use.
096     */
097    public final Function<RESPONSE, RESULT> receiver;
098
099    /**
100     * Constructing an instance of {@code Promise} only requires this response-processor (a.k.a. 
101     * a 'receiver').
102     * 
103     * @param receiver This receiver needs to be able to convert the raw asynchronous response
104     * - <I>which is just a {@link JsonObject} when used by the Browser RDP Web-Socket Channel</I>
105     * - into an instance of {@code 'RESULT'} that the end user can actually use.
106     */
107    public Promise(Function<RESPONSE, RESULT> receiver)
108    { this.receiver = receiver; }
109
110    /**
111     * When building a communication-system that uses these {@code Script & Promise} classes, that
112     * system needs to invoke {@code 'acceptResponse'} whenever a server-response has been received.
113     * 
114     * <BR /><BR />With a {@code WebSocket} connection to a Web-Browser via the Remote Debugging
115     * Port, mapping the response &amp; request is done by checking the request-ID, and <I>keeping a
116     * mapping of ID <B>=&gt;</B> "un-answered Promise Objects."</I>
117     * 
118     * @param response The response received from the communications channel, Web-Socket, browser
119     * etc...
120     */
121    public synchronized final void acceptResponse(RESPONSE response)
122    {
123        if (this.error) throw new AsynchronousException(
124            "This Promise cannot accept a response object because it has already received " +
125            "an rrror.  Current Error Code: [" +
126            this.errorCode + "]" +
127            ((errorMessage != null) ? ("\nCurrent Error Message: \"" + errorMessage + "\"") : "")
128        );
129
130        if (this.response) throw new AsynchronousException
131            ("This Promise has already received a response.  A second response is not allowed.");
132
133        this.result     = receiver.apply(response);
134        this.response   = true;
135    }
136
137    /**
138     * If an error occurs in communications-channel logic, that error may be reported to this
139     * {@code Promise} by calling {@code 'acceptError'}.
140     * 
141     * @param errorCode A meaningful error-code.
142     * @param errorMessage A meaningful error-message
143     * 
144     * @throws AsynchronousException If {@code 'this'} instance of {@code 'Promise'} has already
145     * received an error (via {@code 'acceptError'}), or if {@code 'this'} has accepted a response
146     * (via {@link #acceptResponse(Object)}).
147     */
148    public synchronized final void acceptError(int errorCode, String errorMessage)
149    {
150        if (this.error) throw new AsynchronousException(
151            "There has already been an error reported to this Promise.  Current Error Code: [" +
152            this.errorCode + "]" +
153            ((errorMessage != null) ? ("\nError Message: \"" + errorMessage + "\"") : "")
154        );
155
156        if (this.response) throw new AsynchronousException
157            ("This Promise has already received a response.  It is too late to report an error.");
158
159        this.error          = true;
160        this.errorCode      = errorCode;
161        this.errorMessage   = errorMessage;
162    }
163
164    /**
165     * This method will cede the current {@code Thread's} control until the Asynchronous Channel
166     * has called this class' {@link #acceptResponse(Object)} method.
167     * 
168     * @return Once {@code 'this'} instance of {@code 'Promise'} has been notified of a result, it
169     * will return that result, as an instance of Type-Parameter {@code 'RESULT'}.
170     * 
171     * @throws AsynchronousException If a {@code java.lang.InterruptedException} is thrown while
172     * awating this {@code Promise}, that exception will be wrapped into an instance of 
173     * {@code 'AsynchronousException'}, and then thrown.  This wrapper-exception is an un-checked,
174     * {@code RuntimeException}.
175     */
176    public synchronized final RESULT await()
177    {
178        try
179        {
180            // while ((result == null) && (errorCode == 0)) this.wait();
181            while ((! response) && (! error)) this.wait();
182
183            return result;
184        }
185        catch (InterruptedException e)
186        {
187            throw new AsynchronousException
188                ("While awaiting this Promise, an InterruptedException was thrown", e);
189        }
190    }
191}