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