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 & 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 & request is done by checking the request-ID, and <I>keeping a 134 * mapping of ID <B>=></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}