View Javadoc
1   /*
2    * Copyright (c) 2002-2017 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package com.gargoylesoftware.htmlunit.javascript.host;
16  
17  import static com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine.KEY_STARTING_SCOPE;
18  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
19  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
20  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
21  
22  import java.util.ArrayList;
23  import java.util.List;
24  import java.util.Stack;
25  
26  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
27  import com.gargoylesoftware.htmlunit.javascript.background.BasicJavaScriptJob;
28  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
29  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
30  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
31  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxStaticFunction;
32  
33  import net.sourceforge.htmlunit.corejs.javascript.BaseFunction;
34  import net.sourceforge.htmlunit.corejs.javascript.Context;
35  import net.sourceforge.htmlunit.corejs.javascript.Function;
36  import net.sourceforge.htmlunit.corejs.javascript.JavaScriptException;
37  import net.sourceforge.htmlunit.corejs.javascript.NativeArray;
38  import net.sourceforge.htmlunit.corejs.javascript.NativeObject;
39  import net.sourceforge.htmlunit.corejs.javascript.ScriptRuntime;
40  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
41  import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
42  import net.sourceforge.htmlunit.corejs.javascript.TopLevel;
43  import net.sourceforge.htmlunit.corejs.javascript.Undefined;
44  
45  /**
46   * A JavaScript object for {@code Promise}.
47   *
48   * @author Ahmed Ashour
49   * @author Marc Guillemot
50   * @author Ronald Brill
51   */
52  @JsxClass({CHROME, FF, EDGE})
53  public class Promise extends SimpleScriptable {
54  
55      private enum PromiseState { PENDING, FULFILLED, REJECTED }
56      private PromiseState state_ = PromiseState.PENDING;
57      private Object value_;
58  
59      private boolean race_;
60      private Promise[] all_;
61  
62      private List<BasicJavaScriptJob> settledJobs_;
63      private List<Promise> dependentPromises_;
64  
65      /**
66       * Default constructor.
67       */
68      public Promise() {
69      }
70  
71      /**
72       * Facility constructor.
73       * @param window the owning window
74       */
75      public Promise(final Window window) {
76          setParentScope(window);
77          setPrototype(window.getPrototype(Promise.class));
78      }
79  
80      /**
81       * Constructor new promise with the given {@code object}.
82       *
83       * @param object the object
84       */
85      @JsxConstructor
86      public Promise(final Object object) {
87          if (!(object instanceof Function)) {
88              throw ScriptRuntime.typeError("Promise resolver is not a function");
89          }
90  
91          final Function fun = (Function) object;
92          final Window window = getWindow(fun);
93          this.setParentScope(window);
94          this.setPrototype(window.getPrototype(this.getClass()));
95          final Promise thisPromise = this;
96  
97          final Function resolve = new BaseFunction(window, ScriptableObject.getFunctionPrototype(window)) {
98              @Override
99              public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj,
100                                         final Object[] args) {
101                 thisPromise.settle(true, args.length != 0 ? args[0] : Undefined.instance, window);
102                 return thisPromise;
103             }
104         };
105 
106         final Function reject = new BaseFunction(window, ScriptableObject.getFunctionPrototype(window)) {
107             @Override
108             public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj,
109                                         final Object[] args) {
110                 thisPromise.settle(false, args.length != 0 ? args[0] : Undefined.instance, window);
111                 return thisPromise;
112             }
113         };
114 
115         final Context cx = Context.getCurrentContext();
116         try {
117             // KEY_STARTING_SCOPE maintains a stack of scopes
118             @SuppressWarnings("unchecked")
119             Stack<Scriptable> stack = (Stack<Scriptable>) cx.getThreadLocal(KEY_STARTING_SCOPE);
120             if (null == stack) {
121                 stack = new Stack<>();
122                 cx.putThreadLocal(KEY_STARTING_SCOPE, stack);
123             }
124             stack.push(window);
125             try {
126                 fun.call(cx, window, window, new Object[] {resolve, reject});
127             }
128             finally {
129                 stack.pop();
130             }
131 
132             window.getWebWindow().getWebClient().getJavaScriptEngine().processPostponedActions();
133         }
134         catch (final JavaScriptException e) {
135             thisPromise.settle(false, e.getValue(), window);
136         }
137     }
138 
139     /**
140      * Returns a {@link Promise} object that is resolved with the given value.
141      *
142      * @param context the context
143      * @param thisObj this object
144      * @param args the arguments
145      * @param function the function
146      * @return a {@link Promise}
147      */
148     @JsxStaticFunction
149     public static Promise resolve(final Context context, final Scriptable thisObj, final Object[] args,
150             final Function function) {
151         return create(thisObj, args, PromiseState.FULFILLED);
152     }
153 
154     /**
155      * Returns a {@link Promise} object that is rejected with the given value.
156      *
157      * @param context the context
158      * @param thisObj this object
159      * @param args the arguments
160      * @param function the function
161      * @return a {@link Promise}
162      */
163     @JsxStaticFunction
164     public static Promise reject(final Context context, final Scriptable thisObj, final Object[] args,
165             final Function function) {
166         return create(thisObj, args, PromiseState.REJECTED);
167     }
168 
169     private static Promise create(final Scriptable thisObj, final Object[] args, final PromiseState state) {
170         // fulfilled promises are returned
171         if (args.length != 0 && args[0] instanceof Promise && state == PromiseState.FULFILLED) {
172             return (Promise) args[0];
173         }
174 
175         final Promise promise;
176         if (args.length > 0) {
177             final Object arg = args[0];
178             if (arg instanceof NativeObject) {
179                 final NativeObject nativeObject = (NativeObject) arg;
180                 promise = new Promise(nativeObject.get("then", nativeObject));
181             }
182             else {
183                 promise = new Promise();
184                 promise.value_ = arg;
185                 promise.state_ = state;
186             }
187         }
188         else {
189             promise = new Promise();
190             promise.value_ = Undefined.instance;
191             promise.state_ = state;
192         }
193 
194         promise.setParentScope(thisObj.getParentScope());
195         promise.setPrototype(getWindow(thisObj).getPrototype(promise.getClass()));
196         return promise;
197     }
198 
199     private void settle(final boolean fulfilled, final Object newValue, final Window window) {
200         if (state_ != PromiseState.PENDING) {
201             return;
202         }
203 
204         if (all_ != null) {
205             settleAll(window);
206             return;
207         }
208         settleThis(fulfilled, newValue, window);
209     }
210 
211     private void settleThis(final boolean fulfilled, final Object newValue, final Window window) {
212         value_ = newValue;
213 
214         if (fulfilled) {
215             state_ = PromiseState.FULFILLED;
216         }
217         else {
218             state_ = PromiseState.REJECTED;
219         }
220 
221         if (settledJobs_ != null) {
222             for (BasicJavaScriptJob job : settledJobs_) {
223                 window.getWebWindow().getJobManager().addJob(job, window.getDocument().getPage());
224             }
225             settledJobs_ = null;
226         }
227 
228         if (dependentPromises_ != null) {
229             for (Promise promise : dependentPromises_) {
230                 promise.settle(fulfilled, newValue, window);
231             }
232             dependentPromises_ = null;
233         }
234     }
235 
236     private void settleAll(final Window window) {
237         if (race_) {
238             for (Promise promise : all_) {
239                 if (promise.state_ == PromiseState.REJECTED) {
240                     settleThis(false, promise.value_, window);
241                     return;
242                 }
243                 else if (promise.state_ == PromiseState.FULFILLED) {
244                     settleThis(true, promise.value_, window);
245                     return;
246                 }
247             }
248             return;
249         }
250 
251         final ArrayList<Object> values = new ArrayList<>(all_.length);
252         for (Promise promise : all_) {
253             if (promise.state_ == PromiseState.REJECTED) {
254                 settleThis(false, promise.value_, window);
255                 return;
256             }
257             else if (promise.state_ == PromiseState.PENDING) {
258                 return;
259             }
260             values.add(promise.value_);
261         }
262 
263         final NativeArray jsValues = new NativeArray(values.toArray());
264         ScriptRuntime.setBuiltinProtoAndParent(jsValues, window, TopLevel.Builtins.Array);
265         settleThis(true, jsValues, window);
266     }
267 
268     /**
269      * Returns a {@link Promise} that resolves when all of the promises in the iterable argument have resolved,
270      * or rejects with the reason of the first passed promise that rejects.
271      *
272      * @param context the context
273      * @param thisObj this object
274      * @param args the arguments
275      * @param function the function
276      * @return a {@link Promise}
277      */
278     @JsxStaticFunction
279     public static Promise all(final Context context, final Scriptable thisObj, final Object[] args,
280             final Function function) {
281         return all(false, thisObj, args);
282     }
283 
284     /**
285      * Returns a {@link Promise} that that resolves or rejects as soon as one of the promises
286      * in the iterable resolves or rejects, with the value or reason from that promise.
287      *
288      * @param context the context
289      * @param thisObj this object
290      * @param args the arguments
291      * @param function the function
292      * @return a {@link Promise}
293      */
294     @JsxStaticFunction
295     public static Promise race(final Context context, final Scriptable thisObj, final Object[] args,
296             final Function function) {
297         return all(true, thisObj, args);
298     }
299 
300     private static Promise all(final boolean race, final Scriptable thisObj, final Object[] args) {
301         final Window window = getWindow(thisObj);
302         final Promise returnPromise = new Promise(window);
303 
304         if (args.length == 0) {
305             returnPromise.all_ = new Promise[0];
306         }
307         else if (args[0] instanceof NativeArray) {
308             final NativeArray array = (NativeArray) args[0];
309             final int length = (int) array.getLength();
310             returnPromise.all_ = new Promise[length];
311             for (int i = 0; i < length; i++) {
312                 final Object o = array.get(i);
313                 if (o instanceof Promise) {
314                     returnPromise.all_[i] = (Promise) o;
315                     if (returnPromise.all_[i].dependentPromises_ == null) {
316                         returnPromise.all_[i].dependentPromises_ = new ArrayList<Promise>(2);
317                     }
318                     returnPromise.all_[i].dependentPromises_.add(returnPromise);
319                 }
320                 else {
321                     returnPromise.all_[i] = create(thisObj, new Object[] {o}, PromiseState.FULFILLED);
322                 }
323             }
324         }
325         else {
326             // TODO
327         }
328         returnPromise.race_ = race;
329 
330         returnPromise.settleAll(window);
331         return returnPromise;
332     }
333 
334     /**
335      * It takes two arguments, both are callback functions for the success and failure cases of the Promise.
336      *
337      * @param onFulfilled success function
338      * @param onRejected failure function
339      * @return {@link Promise}
340      */
341     @JsxFunction
342     public Promise then(final Object onFulfilled, final Object onRejected) {
343         final Window window = getWindow();
344         final Promise returnPromise = new Promise(window);
345 
346         final Promise thisPromise = this;
347 
348         final BasicJavaScriptJob job = new BasicJavaScriptJob() {
349 
350             @Override
351             public void run() {
352                 final Context cx = Context.enter();
353                 try {
354                     Function toExecute = null;
355                     if (thisPromise.state_ == PromiseState.FULFILLED && onFulfilled instanceof Function) {
356                         toExecute = (Function) onFulfilled;
357                     }
358                     else if (thisPromise.state_ == PromiseState.REJECTED && onRejected instanceof Function) {
359                         toExecute = (Function) onRejected;
360                     }
361 
362                     try {
363                         final Object callbackResult;
364                         if (toExecute == null) {
365                             final Promise dummy = new Promise();
366                             dummy.state_ = thisPromise.state_;
367                             dummy.value_ = thisPromise.value_;
368                             callbackResult = dummy;
369                         }
370                         else {
371                             // KEY_STARTING_SCOPE maintains a stack of scopes
372                             @SuppressWarnings("unchecked")
373                             Stack<Scriptable> stack = (Stack<Scriptable>) cx.getThreadLocal(KEY_STARTING_SCOPE);
374                             if (null == stack) {
375                                 stack = new Stack<>();
376                                 cx.putThreadLocal(KEY_STARTING_SCOPE, stack);
377                             }
378                             stack.push(window);
379                             try {
380                                 callbackResult = toExecute.call(cx, window, thisPromise, new Object[] {value_});
381                             }
382                             finally {
383                                 stack.pop();
384                             }
385 
386                             window.getWebWindow().getWebClient().getJavaScriptEngine().processPostponedActions();
387                         }
388                         if (callbackResult instanceof Promise) {
389                             final Promise resultPromise = (Promise) callbackResult;
390                             if (resultPromise.state_ == PromiseState.FULFILLED) {
391                                 returnPromise.settle(true, resultPromise.value_, window);
392                             }
393                             else if (resultPromise.state_ == PromiseState.REJECTED) {
394                                 returnPromise.settle(false, resultPromise.value_, window);
395                             }
396                             else {
397                                 if (resultPromise.dependentPromises_ == null) {
398                                     resultPromise.dependentPromises_ = new ArrayList<Promise>(2);
399                                 }
400                                 resultPromise.dependentPromises_.add(returnPromise);
401                             }
402                         }
403                         else {
404                             returnPromise.settle(true, callbackResult, window);
405                         }
406                     }
407                     catch (final JavaScriptException e) {
408                         returnPromise.settle(false, e.getValue(), window);
409                     }
410                 }
411                 finally {
412                     Context.exit();
413                 }
414             }
415 
416             /** {@inheritDoc} */
417             @Override
418             public String toString() {
419                 return super.toString() + " Promise.then";
420             }
421         };
422 
423         if (state_ == PromiseState.FULFILLED || state_ == PromiseState.REJECTED) {
424             window.getWebWindow().getJobManager().addJob(job, window.getDocument().getPage());
425         }
426         else {
427             if (settledJobs_ == null) {
428                 settledJobs_ = new ArrayList<BasicJavaScriptJob>(2);
429             }
430             settledJobs_.add(job);
431         }
432 
433         return returnPromise;
434     }
435 
436     /**
437      * Returns a Promise and deals with rejected cases only.
438      *
439      * @param onRejected failure function
440      * @return {@link Promise}
441      */
442     @JsxFunction(functionName = "catch")
443     public Promise catch_js(final Object onRejected) {
444         return then(null, onRejected);
445     }
446 }