001package Torello.Java.Additional;
002
003import Torello.Java.StrPrint;
004
005import java.util.concurrent.CompletableFuture;
006import java.util.concurrent.ExecutionException;
007import java.util.function.Consumer;
008import java.util.function.Function;
009import javax.json.JsonObject;
010
011/**
012 * A promise keeps the processing logic for converting a response from an asychronous connection
013 * into a result that the end-user can utilize.
014 * 
015 * <BR /><BR /><EMBED CLASS='external-html' DATA-FILE-ID=PROMISE>
016 * 
017 * @param <RESPONSE>    <EMBED CLASS='external-html' DATA-FILE-ID=RESPONSE>
018 * @param <RESULT>      <EMBED CLASS='external-html' DATA-FILE-ID=RESULT>
019 */
020public class Promise<RESPONSE, RESULT> implements java.io.Serializable
021{
022    /** <EMBED CLASS='external-html' DATA-FILE-ID=SVUID> */
023    protected static final long serialVersionUID = 1;
024
025    /** When assigned {@code TRUE}, prints Diagnostic Information via {@cod System.out} */
026    public static boolean DEBUGGING = true;
027
028    // This value is updated by the 'acceptResponse' method
029    private RESULT                          result = null;
030    private final CompletableFuture<RESULT> future = new CompletableFuture<>();
031
032    // If an error has occured, these two fields will hold more information
033    private boolean response        = false;
034    private boolean error           = false;
035    private int     errorCode       = 0;
036    private String  errorMessage    = null;
037
038    /**
039     * Asks {@code 'this'} instance of {@code Promise} whether or not an error has been reported.
040     * 
041     * @return {@code TRUE} if there were errors reported by the asynchronous channel, and
042     * {@code FALSE} otherwise.
043     */
044    public boolean hadError() { return this.error; }
045
046    /**
047     * Asks {@code 'this'} instance of {@code Promise} whether any response has been reported.
048     * This shall return {@code TRUE} if <I>either an error, or a response has been received</I>.
049     * 
050     * @return {@code TRUE} if a response or an error has been reported to {@code 'this'} instance,
051     * and {@code FALSE} otherwise.
052     */
053    public boolean hadResponse() { return this.response || this.error; }
054
055    /**
056     * Gets all current-state of this {@code Promise}
057     * 
058     * @return The current state of this {@code Promise}, as an instance of {@link Ret5}.
059     * 
060     * <BR /><BR /><UL CLASS=JDUL>
061     * 
062     * <LI> {@code Ret5.a (Boolean)}:
063     *      <BR />Whether or not a response has been reported
064     *      <BR /><BR />
065     *      </LI>
066     * 
067     * <LI> {@code Ret5.b (Boolean)}:
068     *      <BR />Whether or not an error has been reported
069     *      <BR /><BR />
070     *      </LI>
071     * 
072     * <LI> {@code Ret5.c (RESULT)}:
073     *      <BR />The result that has been returned, or null if no result has been received
074     *      <BR /><BR />
075     *      </LI>
076     * 
077     * <LI> {@code Ret5.d (Integer)}:
078     *      <BR />The error-code that was received, or 0 if no errors have been reported
079     *      <BR /><BR />
080     *      </LI>
081     * 
082     * <LI> {@code Ret5.e (String)}:
083     *      <BR />The error-message (if an error was reported), or null.
084     *      </LI>
085     * </UL> 
086     */
087    public Ret5<Boolean, Boolean, RESULT, Integer, String> getState()
088    { return new Ret5<>(this.response, this.error, this.result, this.errorCode, this.errorMessage); }
089
090    /**
091     * This allows a user to retrieve the result of {@code 'this'} asynchronous {@code Promise}.
092     * Note that if no result has been received yet, this method throws an exception.
093     * 
094     * @return The result of the asynchronous call.
095     * 
096     * @throws AynchronousException This throws if this {@code Promise} has not received a result
097     * yet.
098     */
099    public RESULT result()
100    {
101        if (! this.response) throw new AsynchronousException("No response has been received yet");
102        return this.result;
103    }
104
105    /**
106     * This is the "Respone Processor".  As of the writing of this class, the only use that the
107     * three classes: Script, Promise &amp; Sender has been for implementing the Google Chrome
108     * Browser Remote Debug Protocol.  (Although perhaps these classes will one day be used with a
109     * different asychronous protocol)
110     * 
111     * In this current case (Browser RDP), this receiver is actually doing the "JSON Binding" to
112     * bind the JsonObject received from the Browser into a Java Class that the user can actually
113     * use.
114     */
115    public final Function<RESPONSE, RESULT> receiver;
116
117    /**
118     * Constructing an instance of {@code Promise} only requires this response-processor (a.k.a. 
119     * a 'receiver').
120     * 
121     * @param receiver This receiver needs to be able to convert the raw asynchronous response
122     * - <I>which is just a {@link JsonObject} when used by the Browser RDP Web-Socket Channel</I>
123     * - into an instance of {@code 'RESULT'} that the end user can actually use.
124     */
125    public Promise(Function<RESPONSE, RESULT> receiver)
126    { this.receiver = receiver; }
127
128    /**
129     * When building a communication-system that uses these {@code Script & Promise} classes, that
130     * system needs to invoke {@code 'acceptResponse'} whenever a server-response has been received.
131     * 
132     * <BR /><BR />With a {@code WebSocket} connection to a Web-Browser via the Remote Debugging
133     * Port, mapping the response &amp; request is done by checking the request-ID, and <I>keeping a
134     * mapping of ID <B>=&gt;</B> "un-answered Promise Objects."</I>
135     * 
136     * @param response The response received from the communications channel, Web-Socket, browser
137     * etc...
138     */
139    public final void acceptResponse(RESPONSE response)
140    {
141        if (DEBUGGING) System.out.println(
142            "Promise.acceptResponse(RESPONSE response) invoked\n" +
143            "response=" + StrPrint.abbrev(response.toString(), true, true, null, 80)
144        );
145
146        if (this.error) throw new AsynchronousException(
147            "This Promise cannot accept a response object because it has already received " +
148            "an error.  Current Error Code: [" + this.errorCode + "]" +
149            ((this.errorMessage != null)
150                ? ("\nCurrent Error Message: \"" + this.errorMessage + "\"")
151                : "")
152        );
153
154        if (this.response) throw new AsynchronousException
155            ("This Promise has already received a response.  A second response is not allowed.");
156        
157        this.result     = this.receiver.apply(response);
158        this.response   = true;
159
160        // ✅ COMPLETE the future with the result
161        this.future.complete(this.result);
162    }
163
164    /**
165     * If an error occurs in communications-channel logic, that error may be reported to this
166     * {@code Promise} by calling {@code 'acceptError'}.
167     * 
168     * @param errorCode A meaningful error-code.
169     * @param errorMessage A meaningful error-message
170     * 
171     * @throws AsynchronousException If {@code 'this'} instance of {@code 'Promise'} has already
172     * received an error (via {@code 'acceptError'}), or if {@code 'this'} has accepted a response
173     * (via {@link #acceptResponse(Object)}).
174     */
175    public final void acceptError(int errorCode, String errorMessage)
176    {
177        if (DEBUGGING) System.out.println(
178            "Promise.acceptThrowable(int errorcode, String errorMessage) invoked\n" +
179            "errorCode=" + errorCode + ", errorMessage=[" + errorMessage + ']'
180        );
181
182        if (this.error) throw new AsynchronousException(
183            "There has already been an error reported to this Promise.  Current Error Code: [" +
184            this.errorCode + "]" +
185            ((errorMessage != null) ? ("\nError Message: \"" + errorMessage + "\"") : "")
186        );
187
188        if (this.response) throw new AsynchronousException
189            ("This Promise has already received a response.  It is too late to report an error.");
190
191        this.error          = true;
192        this.errorCode      = errorCode;
193        this.errorMessage   = errorMessage;
194
195        this.future.completeExceptionally(
196            new AsynchronousException
197                ("Received async error: code=" + errorCode + ", msg=" + errorMessage)
198        );
199    }
200
201    /**
202     * Reports an asynchronous failure to this {@code Promise} by attaching a {@code Throwable} 
203     * instance and an error code. This method allows the caller to propagate a real exception 
204     * (such as an {@code IOException}, {@code NullPointerException}, etc.) as the cause of 
205     * asynchronous failure, while still assigning a numeric error code for internal tracking.
206     * 
207     * <BR /><BR />This method may only be called once per {@code Promise}. If this 
208     * {@code Promise} has already received a result (via {@link #acceptResponse(Object)}), 
209     * or if it has already received an error (via {@link #acceptError(int, String)} or another 
210     * call to {@code acceptThrowable}), an {@link AsynchronousException} will be thrown.
211     * 
212     * <BR /><BR />Internally, this method completes the underlying {@code CompletableFuture}
213     * by calling {@code completeExceptionally(...)} with a new {@code AsynchronousException}, 
214     * which wraps the original {@code Throwable} as part of the message and the cause chain.
215     * 
216     * @param errorCode A numeric error code, used to distinguish between different error types 
217     * or failure scenarios. This value will be accessible via {@link #getState()}.
218     * 
219     * @param t The {@code Throwable} instance that triggered or represents this asynchronous
220     * failure. Its message will be used as the {@code errorMessage} for this {@code Promise}, and
221     * the throwable itself will be wrapped into an {@code AsynchronousException}.
222     * 
223     * @throws AsynchronousException If this {@code Promise} has already been completed, either 
224     * by a response or an earlier error.
225     */
226    public final void acceptThrowable(int errorCode, Throwable t)
227    {
228        if (DEBUGGING) System.out.println(
229            "Promise.acceptThrowable(int errorCode, Throwable t) invoked:\n" +
230            "errorCode=" + errorCode + ", " +
231            "Throwable.class=" + t.getClass().getSimpleName() + ", " +
232            "Throwable.message=[" + t.getMessage() + ']'
233        );
234
235        if (this.error) throw new AsynchronousException(
236            "There has already been an error reported to this Promise.  Current Error Code: [" +
237            this.errorCode + "]" +
238            ((this.errorMessage != null) ? ("\nError Message: \"" + this.errorMessage + "\"") : "")
239        );
240
241        if (this.response) throw new AsynchronousException
242            ("This Promise has already received a response.  It is too late to report an error.");
243
244        this.error      = true;
245        this.errorCode  = errorCode;
246
247        final String msg = t.getMessage();
248
249        this.errorMessage = 
250            "Throwable [" + t.getClass().getSimpleName() + "] " +
251            ((msg != null)
252                ? ("Message: " + msg)
253                : "was Thrown.");
254
255        this.future.completeExceptionally(
256            new AsynchronousException(
257                "Promise Completed with Throw\n" +
258                "Please review Throwable Cause-Chain for Details.\n" +
259                "Error Message: \"" + this.errorMessage + "\"\n" +
260                "Error Code:    [" + errorCode + "]\n" +
261                t
262            ));
263    }
264
265    /**
266     * This method will cede the current {@code Thread's} control until the Asynchronous Channel
267     * has called this class' {@link #acceptResponse(Object)} method.
268     * 
269     * @return Once {@code 'this'} instance of {@code 'Promise'} has been notified of a result, it
270     * will return that result, as an instance of Type-Parameter {@code 'RESULT'}.
271     * 
272     * @throws AsynchronousException If a {@code java.lang.InterruptedException} is thrown while
273     * awating this {@code Promise}, that exception will be wrapped into an instance of 
274     * {@code 'AsynchronousException'}, and then thrown.  This wrapper-exception is an un-checked,
275     * {@code RuntimeException}.
276     */
277    public RESULT await()
278    {
279        try 
280            { return this.future.get(); }
281
282        catch (InterruptedException e)
283        {
284            throw new AsynchronousException(
285                "While awaiting this Promise, an InterruptedException was thrown.\n" +
286                "See Exception getCause() for details.", e
287            );
288        }
289
290        catch (ExecutionException e)
291        {
292            Throwable cause = e.getCause();
293
294            if (cause instanceof RuntimeException) throw (RuntimeException) cause;
295        
296            if (cause instanceof Error) throw (Error) cause;
297        
298            // throw new AsynchronousException("ExecutionException occurred", e);
299            throw new AsynchronousException(
300                "While awaiting this Promise, an ExecutionException was thrown.\n" +
301                "See Exception getCause() for details.",
302                e
303            );
304        }
305    }
306
307    /**
308     * Registers a callback to be executed asynchronously <I>after</I> this {@code Promise} receives
309     * its result. This method allows for chaining asynchronous logic without blocking the current
310     * thread.
311     * 
312     * <BR /><BR />This callback is passed the result of this {@code Promise} once the corresponding
313     * {@link #acceptResponse(Object)} method has been called, and the {@code receiver} has produced a
314     * valid {@code RESULT} object.
315     * 
316     * <BR /><BR />Unlike {@link #await()}, this method returns immediately and does <B>not block</B> the
317     * current thread.
318     * 
319     * <BR /><BR />Example usage:
320     * <DIV CLASS=SNIP>{@code
321     *     Target.createTarget(...).exec(browserSender).then(MyClass::handleCreatedTarget);
322     * }</DIV>
323     * 
324     * @param handler A {@code Consumer} that will be invoked asynchronously with the result of this
325     * {@code Promise} when it becomes available.
326     */
327    public void then(Consumer<RESULT> handler) { this.future.thenAccept(handler); }
328}