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 com.gargoylesoftware.htmlunit.javascript.FunctionWrapper;
22  import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
23  import com.gargoylesoftware.htmlunit.javascript.PostponedAction;
24  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
25  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
26  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
27  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
28  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxStaticFunction;
29  
30  import net.sourceforge.htmlunit.corejs.javascript.Context;
31  import net.sourceforge.htmlunit.corejs.javascript.Function;
32  import net.sourceforge.htmlunit.corejs.javascript.JavaScriptException;
33  import net.sourceforge.htmlunit.corejs.javascript.NativeArray;
34  import net.sourceforge.htmlunit.corejs.javascript.NativeObject;
35  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
36  import net.sourceforge.htmlunit.corejs.javascript.Undefined;
37  
38  /**
39   * A JavaScript object for {@code Promise}.
40   *
41   * @author Ahmed Ashour
42   * @author Marc Guillemot
43   */
44  @JsxClass({CHROME, FF, EDGE})
45  public class Promise extends SimpleScriptable {
46  
47      private Object value_;
48      /** To be set only by {@link #all(Context, Scriptable, Object[], Function)}. */
49      private Promise[] all_;
50      private boolean resolve_ = true;
51      private String exceptionDetails_;
52  
53      /**
54       * Default constructor.
55       */
56      public Promise() {
57      }
58  
59      /**
60       * Facility constructor.
61       * @param window the owning window
62       */
63      public Promise(final Window window) {
64          setParentScope(window);
65          setPrototype(window.getPrototype(Promise.class));
66      }
67  
68      /**
69       * Constructor new promise with the given {@code object}.
70       *
71       * @param object the object
72       */
73      @JsxConstructor
74      public Promise(final Object object) {
75          if (object instanceof Promise) {
76              value_ = ((Promise) object).value_;
77          }
78          else if (object instanceof NativeObject) {
79              final NativeObject nativeObject = (NativeObject) object;
80              value_ = nativeObject.get("then", nativeObject);
81          }
82          else {
83              value_ = object;
84          }
85      }
86  
87      /**
88       * Returns a {@link Promise} object that is resolved with the given value.
89       *
90       * @param context the context
91       * @param thisObj this object
92       * @param args the arguments
93       * @param function the function
94       * @return a {@link Promise}
95       */
96      @JsxStaticFunction
97      public static Promise resolve(final Context context, final Scriptable thisObj, final Object[] args,
98              final Function function) {
99          final Promise promise = new Promise(args.length != 0 ? args[0] : Undefined.instance);
100         promise.setResolve(true);
101         promise.setParentScope(thisObj.getParentScope());
102         promise.setPrototype(getWindow(thisObj).getPrototype(promise.getClass()));
103         return promise;
104     }
105 
106     /**
107      * Returns a {@link Promise} object that is rejected with the given value.
108      *
109      * @param context the context
110      * @param thisObj this object
111      * @param args the arguments
112      * @param function the function
113      * @return a {@link Promise}
114      */
115     @JsxStaticFunction
116     public static Promise reject(final Context context, final Scriptable thisObj, final Object[] args,
117             final Function function) {
118         final Promise promise = new Promise(args.length != 0 ? args[0] : Undefined.instance);
119         promise.setResolve(false);
120         promise.setParentScope(thisObj.getParentScope());
121         promise.setPrototype(getWindow(thisObj).getPrototype(promise.getClass()));
122         return promise;
123     }
124 
125     private void setResolve(final boolean resolve) {
126         resolve_ = resolve;
127     }
128 
129     /**
130      * Also sets the value of this promise.
131      */
132     private boolean isResolved(final Function onRejected) {
133         if (all_ != null) {
134             final Object[] values = new Object[all_.length];
135             for (int i = 0; i < all_.length; i++) {
136                 final Promise p = all_[i];
137                 if (!p.isResolved(onRejected)) {
138                     value_ = p.value_;
139                     return false;
140                 }
141 
142                 if (p.value_ instanceof Function) {
143                     // TODO
144                 }
145                 else {
146                     values[i] = p.value_;
147                 }
148             }
149             value_ = Context.getCurrentContext().newArray(getParentScope(), values);
150         }
151         return resolve_;
152     }
153 
154     /**
155      * Returns a {@link Promise} that resolves when all of the promises in the iterable argument have resolved,
156      * or rejects with the reason of the first passed promise that rejects.
157      *
158      * @param context the context
159      * @param thisObj this object
160      * @param args the arguments
161      * @param function the function
162      * @return a {@link Promise}
163      */
164     @JsxStaticFunction
165     public static Promise all(final Context context, final Scriptable thisObj, final Object[] args,
166             final Function function) {
167         final Promise promise = new Promise();
168         promise.setResolve(true);
169         if (args.length == 0) {
170             promise.all_ = new Promise[0];
171         }
172         else {
173             final NativeArray array = (NativeArray) args[0];
174             final int length = (int) array.getLength();
175             promise.all_ = new Promise[length];
176             for (int i = 0; i < length; i++) {
177                 final Object o = array.get(i);
178                 if (o instanceof Promise) {
179                     promise.all_[i] = (Promise) o;
180                 }
181                 else {
182                     promise.all_[i] = resolve(null, thisObj, new Object[] {o}, null);
183                 }
184             }
185         }
186         promise.setParentScope(thisObj.getParentScope());
187         promise.setPrototype(getWindow(thisObj).getPrototype(promise.getClass()));
188         return promise;
189     }
190 
191     /**
192      * It takes two arguments, both are callback functions for the success and failure cases of the Promise.
193      *
194      * @param onFulfilled success function
195      * @param onRejected failure function
196      * @return {@link Promise}
197      */
198     @JsxFunction
199     public Promise then(final Function onFulfilled, final Function onRejected) {
200         final Window window = getWindow();
201         final Promise promise = new Promise(window);
202         final Promise thisPromise = this;
203 
204         PostponedAction thenAction = new PostponedAction(window.getDocument().getPage(), "Promise.then") {
205 
206             @Override
207             public void execute() throws Exception {
208                 Context.enter();
209                 try {
210                     Object newValue = null;
211                     final Function toExecute = isResolved(onRejected) ? onFulfilled : onRejected;
212                     if (value_ instanceof Function) {
213                         final WasCalledFunctionWrapper wrapper = new WasCalledFunctionWrapper(toExecute);
214                         try {
215                             ((Function) value_).call(Context.getCurrentContext(), window, thisPromise,
216                                     new Object[] {wrapper, onRejected});
217                             if (wrapper.wasCalled_) {
218                                 newValue = wrapper.value_;
219                             }
220                         }
221                         catch (final JavaScriptException e) {
222                             if (onRejected == null) {
223                                 promise.exceptionDetails_ = e.details();
224                             }
225                             else if (!wrapper.wasCalled_) {
226                                 newValue = onRejected.call(Context.getCurrentContext(), window, thisPromise,
227                                         new Object[] {e.getValue()});
228                             }
229                         }
230                     }
231                     else {
232                         newValue = toExecute.call(Context.getCurrentContext(), window, thisPromise,
233                                 new Object[] {value_});
234                     }
235                     promise.value_ = newValue;
236                 }
237                 finally {
238                     Context.exit();
239                 }
240             }
241         };
242 
243         final JavaScriptEngine jsEngine
244             = (JavaScriptEngine) window.getWebWindow().getWebClient().getJavaScriptEngine();
245         jsEngine.addPostponedAction(thenAction);
246 
247         return promise;
248     }
249 
250     /**
251      * Returns a Promise and deals with rejected cases only.
252      *
253      * @param onRejected failure function
254      * @return {@link Promise}
255      */
256     @JsxFunction(functionName = "catch")
257     public Promise catch_js(final Function onRejected) {
258         final Window window = getWindow();
259         final Promise promise = new Promise(window);
260         final Promise thisPromise = this;
261 
262         final PostponedAction thenAction = new PostponedAction(window.getDocument().getPage(), "Promise.catch") {
263 
264             @Override
265             public void execute() throws Exception {
266                 Context.enter();
267                 try {
268                     final Object newValue = onRejected.call(Context.getCurrentContext(), window, thisPromise,
269                             new Object[] {exceptionDetails_});
270                     promise.value_ = newValue;
271                 }
272                 finally {
273                     Context.exit();
274                 }
275             }
276         };
277 
278         final JavaScriptEngine jsEngine
279             = (JavaScriptEngine) window.getWebWindow().getWebClient().getJavaScriptEngine();
280         jsEngine.addPostponedAction(thenAction);
281 
282         return promise;
283     }
284 
285     private static class WasCalledFunctionWrapper extends FunctionWrapper {
286         private boolean wasCalled_;
287         private Object value_;
288 
289         WasCalledFunctionWrapper(final Function wrapped) {
290             super(wrapped);
291         }
292 
293         /**
294          * {@inheritDoc}
295          */
296         @Override
297         public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) {
298             wasCalled_ = true;
299             value_ = super.call(cx, scope, thisObj, args);
300             return value_;
301         }
302     }
303 }