ScriptObjectMirror.java revision 1183:dbfbf5423642
1/*
2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.api.scripting;
27
28import java.nio.ByteBuffer;
29import java.security.AccessControlContext;
30import java.security.AccessController;
31import java.security.Permissions;
32import java.security.PrivilegedAction;
33import java.security.ProtectionDomain;
34import java.util.AbstractMap;
35import java.util.ArrayList;
36import java.util.Collection;
37import java.util.Collections;
38import java.util.Iterator;
39import java.util.LinkedHashSet;
40import java.util.List;
41import java.util.Map;
42import java.util.Objects;
43import java.util.Set;
44import java.util.concurrent.Callable;
45import javax.script.Bindings;
46import jdk.nashorn.internal.objects.Global;
47import jdk.nashorn.internal.runtime.ConsString;
48import jdk.nashorn.internal.runtime.Context;
49import jdk.nashorn.internal.runtime.JSType;
50import jdk.nashorn.internal.runtime.ScriptFunction;
51import jdk.nashorn.internal.runtime.ScriptObject;
52import jdk.nashorn.internal.runtime.ScriptRuntime;
53import jdk.nashorn.internal.runtime.arrays.ArrayData;
54import jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor;
55
56/**
57 * Mirror object that wraps a given Nashorn Script object.
58 *
59 * @since 1.8u40
60 */
61@jdk.Exported
62public final class ScriptObjectMirror extends AbstractJSObject implements Bindings {
63    private static AccessControlContext getContextAccCtxt() {
64        final Permissions perms = new Permissions();
65        perms.add(new RuntimePermission(Context.NASHORN_GET_CONTEXT));
66        return new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, perms) });
67    }
68
69    private static final AccessControlContext GET_CONTEXT_ACC_CTXT = getContextAccCtxt();
70
71    private final ScriptObject sobj;
72    private final Global  global;
73    private final boolean strict;
74
75    @Override
76    public boolean equals(final Object other) {
77        if (other instanceof ScriptObjectMirror) {
78            return sobj.equals(((ScriptObjectMirror)other).sobj);
79        }
80
81        return false;
82    }
83
84    @Override
85    public int hashCode() {
86        return sobj.hashCode();
87    }
88
89    @Override
90    public String toString() {
91        return inGlobal(new Callable<String>() {
92            @Override
93            public String call() {
94                return ScriptRuntime.safeToString(sobj);
95            }
96        });
97    }
98
99    // JSObject methods
100
101    @Override
102    public Object call(final Object thiz, final Object... args) {
103        final Global oldGlobal = Context.getGlobal();
104        final boolean globalChanged = (oldGlobal != global);
105
106        try {
107            if (globalChanged) {
108                Context.setGlobal(global);
109            }
110
111            if (sobj instanceof ScriptFunction) {
112                final Object[] modArgs = globalChanged? wrapArray(args, oldGlobal) : args;
113                final Object self = globalChanged? wrap(thiz, oldGlobal) : thiz;
114                return wrap(ScriptRuntime.apply((ScriptFunction)sobj, unwrap(self, global), unwrapArray(modArgs, global)), global);
115            }
116
117            throw new RuntimeException("not a function: " + toString());
118        } catch (final NashornException ne) {
119            throw ne.initEcmaError(global);
120        } catch (final RuntimeException | Error e) {
121            throw e;
122        } catch (final Throwable t) {
123            throw new RuntimeException(t);
124        } finally {
125            if (globalChanged) {
126                Context.setGlobal(oldGlobal);
127            }
128        }
129    }
130
131    @Override
132    public Object newObject(final Object... args) {
133        final Global oldGlobal = Context.getGlobal();
134        final boolean globalChanged = (oldGlobal != global);
135
136        try {
137            if (globalChanged) {
138                Context.setGlobal(global);
139            }
140
141            if (sobj instanceof ScriptFunction) {
142                final Object[] modArgs = globalChanged? wrapArray(args, oldGlobal) : args;
143                return wrap(ScriptRuntime.construct((ScriptFunction)sobj, unwrapArray(modArgs, global)), global);
144            }
145
146            throw new RuntimeException("not a constructor: " + toString());
147        } catch (final NashornException ne) {
148            throw ne.initEcmaError(global);
149        } catch (final RuntimeException | Error e) {
150            throw e;
151        } catch (final Throwable t) {
152            throw new RuntimeException(t);
153        } finally {
154            if (globalChanged) {
155                Context.setGlobal(oldGlobal);
156            }
157        }
158    }
159
160    @Override
161    public Object eval(final String s) {
162        return inGlobal(new Callable<Object>() {
163            @Override
164            public Object call() {
165                final Context context = AccessController.doPrivileged(
166                        new PrivilegedAction<Context>() {
167                            @Override
168                            public Context run() {
169                                return Context.getContext();
170                            }
171                        }, GET_CONTEXT_ACC_CTXT);
172                return wrap(context.eval(global, s, sobj, null, false), global);
173            }
174        });
175    }
176
177    /**
178     * Call member function
179     * @param functionName function name
180     * @param args         arguments
181     * @return return value of function
182     */
183    public Object callMember(final String functionName, final Object... args) {
184        Objects.requireNonNull(functionName);
185        final Global oldGlobal = Context.getGlobal();
186        final boolean globalChanged = (oldGlobal != global);
187
188        try {
189            if (globalChanged) {
190                Context.setGlobal(global);
191            }
192
193            final Object val = sobj.get(functionName);
194            if (val instanceof ScriptFunction) {
195                final Object[] modArgs = globalChanged? wrapArray(args, oldGlobal) : args;
196                return wrap(ScriptRuntime.apply((ScriptFunction)val, sobj, unwrapArray(modArgs, global)), global);
197            } else if (val instanceof JSObject && ((JSObject)val).isFunction()) {
198                return ((JSObject)val).call(sobj, args);
199            }
200
201            throw new NoSuchMethodException("No such function " + functionName);
202        } catch (final NashornException ne) {
203            throw ne.initEcmaError(global);
204        } catch (final RuntimeException | Error e) {
205            throw e;
206        } catch (final Throwable t) {
207            throw new RuntimeException(t);
208        } finally {
209            if (globalChanged) {
210                Context.setGlobal(oldGlobal);
211            }
212        }
213    }
214
215    @Override
216    public Object getMember(final String name) {
217        Objects.requireNonNull(name);
218        return inGlobal(new Callable<Object>() {
219            @Override public Object call() {
220                return wrap(sobj.get(name), global);
221            }
222        });
223    }
224
225    @Override
226    public Object getSlot(final int index) {
227        return inGlobal(new Callable<Object>() {
228            @Override public Object call() {
229                return wrap(sobj.get(index), global);
230            }
231        });
232    }
233
234    @Override
235    public boolean hasMember(final String name) {
236        Objects.requireNonNull(name);
237        return inGlobal(new Callable<Boolean>() {
238            @Override public Boolean call() {
239                return sobj.has(name);
240            }
241        });
242    }
243
244    @Override
245    public boolean hasSlot(final int slot) {
246        return inGlobal(new Callable<Boolean>() {
247            @Override public Boolean call() {
248                return sobj.has(slot);
249            }
250        });
251    }
252
253    @Override
254    public void removeMember(final String name) {
255        Objects.requireNonNull(name);
256        remove(name);
257    }
258
259    @Override
260    public void setMember(final String name, final Object value) {
261        Objects.requireNonNull(name);
262        put(name, value);
263    }
264
265    @Override
266    public void setSlot(final int index, final Object value) {
267        inGlobal(new Callable<Void>() {
268            @Override public Void call() {
269                sobj.set(index, unwrap(value, global), getCallSiteFlags());
270                return null;
271            }
272        });
273    }
274
275    /**
276     * Nashorn extension: setIndexedPropertiesToExternalArrayData.
277     * set indexed properties be exposed from a given nio ByteBuffer.
278     *
279     * @param buf external buffer - should be a nio ByteBuffer
280     */
281    public void setIndexedPropertiesToExternalArrayData(final ByteBuffer buf) {
282        inGlobal(new Callable<Void>() {
283            @Override public Void call() {
284                sobj.setArray(ArrayData.allocate(buf));
285                return null;
286            }
287        });
288    }
289
290
291    @Override
292    public boolean isInstance(final Object obj) {
293        if (! (obj instanceof ScriptObjectMirror)) {
294            return false;
295        }
296
297        final ScriptObjectMirror instance = (ScriptObjectMirror)obj;
298        // if not belongs to my global scope, return false
299        if (global != instance.global) {
300            return false;
301        }
302
303        return inGlobal(new Callable<Boolean>() {
304            @Override public Boolean call() {
305                return sobj.isInstance(instance.sobj);
306            }
307        });
308    }
309
310    @Override
311    public String getClassName() {
312        return sobj.getClassName();
313    }
314
315    @Override
316    public boolean isFunction() {
317        return sobj instanceof ScriptFunction;
318    }
319
320    @Override
321    public boolean isStrictFunction() {
322        return isFunction() && ((ScriptFunction)sobj).isStrict();
323    }
324
325    @Override
326    public boolean isArray() {
327        return sobj.isArray();
328    }
329
330    // javax.script.Bindings methods
331
332    @Override
333    public void clear() {
334        inGlobal(new Callable<Object>() {
335            @Override public Object call() {
336                sobj.clear(strict);
337                return null;
338            }
339        });
340    }
341
342    @Override
343    public boolean containsKey(final Object key) {
344        checkKey(key);
345        return inGlobal(new Callable<Boolean>() {
346            @Override public Boolean call() {
347                return sobj.containsKey(key);
348            }
349        });
350    }
351
352    @Override
353    public boolean containsValue(final Object value) {
354        return inGlobal(new Callable<Boolean>() {
355            @Override public Boolean call() {
356                return sobj.containsValue(unwrap(value, global));
357            }
358        });
359    }
360
361    @Override
362    public Set<Map.Entry<String, Object>> entrySet() {
363        return inGlobal(new Callable<Set<Map.Entry<String, Object>>>() {
364            @Override public Set<Map.Entry<String, Object>> call() {
365                final Iterator<String>               iter    = sobj.propertyIterator();
366                final Set<Map.Entry<String, Object>> entries = new LinkedHashSet<>();
367
368                while (iter.hasNext()) {
369                    final String key   = iter.next();
370                    final Object value = translateUndefined(wrap(sobj.get(key), global));
371                    entries.add(new AbstractMap.SimpleImmutableEntry<>(key, value));
372                }
373
374                return Collections.unmodifiableSet(entries);
375            }
376        });
377    }
378
379    @Override
380    public Object get(final Object key) {
381        checkKey(key);
382        return inGlobal(new Callable<Object>() {
383            @Override public Object call() {
384                return translateUndefined(wrap(sobj.get(key), global));
385            }
386        });
387    }
388
389    @Override
390    public boolean isEmpty() {
391        return inGlobal(new Callable<Boolean>() {
392            @Override public Boolean call() {
393                return sobj.isEmpty();
394            }
395        });
396    }
397
398    @Override
399    public Set<String> keySet() {
400        return inGlobal(new Callable<Set<String>>() {
401            @Override public Set<String> call() {
402                final Iterator<String> iter   = sobj.propertyIterator();
403                final Set<String>      keySet = new LinkedHashSet<>();
404
405                while (iter.hasNext()) {
406                    keySet.add(iter.next());
407                }
408
409                return Collections.unmodifiableSet(keySet);
410            }
411        });
412    }
413
414    @Override
415    public Object put(final String key, final Object value) {
416        checkKey(key);
417        final ScriptObject oldGlobal = Context.getGlobal();
418        final boolean globalChanged = (oldGlobal != global);
419        return inGlobal(new Callable<Object>() {
420            @Override public Object call() {
421                final Object modValue = globalChanged? wrap(value, oldGlobal) : value;
422                return translateUndefined(wrap(sobj.put(key, unwrap(modValue, global), strict), global));
423            }
424        });
425    }
426
427    @Override
428    public void putAll(final Map<? extends String, ? extends Object> map) {
429        Objects.requireNonNull(map, "map is null");
430        final ScriptObject oldGlobal = Context.getGlobal();
431        final boolean globalChanged = (oldGlobal != global);
432        inGlobal(new Callable<Object>() {
433            @Override public Object call() {
434                for (final Map.Entry<? extends String, ? extends Object> entry : map.entrySet()) {
435                    final Object value = entry.getValue();
436                    final Object modValue = globalChanged? wrap(value, oldGlobal) : value;
437                    final String key = entry.getKey();
438                    checkKey(key);
439                    sobj.set(key, unwrap(modValue, global), getCallSiteFlags());
440                }
441                return null;
442            }
443        });
444    }
445
446    @Override
447    public Object remove(final Object key) {
448        checkKey(key);
449        return inGlobal(new Callable<Object>() {
450            @Override public Object call() {
451                return translateUndefined(wrap(sobj.remove(key, strict), global));
452            }
453        });
454    }
455
456    /**
457     * Delete a property from this object.
458     *
459     * @param key the property to be deleted
460     *
461     * @return if the delete was successful or not
462     */
463    public boolean delete(final Object key) {
464        return inGlobal(new Callable<Boolean>() {
465            @Override public Boolean call() {
466                return sobj.delete(unwrap(key, global), strict);
467            }
468        });
469    }
470
471    @Override
472    public int size() {
473        return inGlobal(new Callable<Integer>() {
474            @Override public Integer call() {
475                return sobj.size();
476            }
477        });
478    }
479
480    @Override
481    public Collection<Object> values() {
482        return inGlobal(new Callable<Collection<Object>>() {
483            @Override public Collection<Object> call() {
484                final List<Object>     values = new ArrayList<>(size());
485                final Iterator<Object> iter   = sobj.valueIterator();
486
487                while (iter.hasNext()) {
488                    values.add(translateUndefined(wrap(iter.next(), global)));
489                }
490
491                return Collections.unmodifiableList(values);
492            }
493        });
494    }
495
496    // Support for ECMAScript Object API on mirrors
497
498    /**
499     * Return the __proto__ of this object.
500     * @return __proto__ object.
501     */
502    public Object getProto() {
503        return inGlobal(new Callable<Object>() {
504            @Override public Object call() {
505                return wrap(sobj.getProto(), global);
506            }
507        });
508    }
509
510    /**
511     * Set the __proto__ of this object.
512     * @param proto new proto for this object
513     */
514    public void setProto(final Object proto) {
515        inGlobal(new Callable<Void>() {
516            @Override public Void call() {
517                sobj.setPrototypeOf(unwrap(proto, global));
518                return null;
519            }
520        });
521    }
522
523    /**
524     * ECMA 8.12.1 [[GetOwnProperty]] (P)
525     *
526     * @param key property key
527     *
528     * @return Returns the Property Descriptor of the named own property of this
529     * object, or undefined if absent.
530     */
531    public Object getOwnPropertyDescriptor(final String key) {
532        return inGlobal(new Callable<Object>() {
533            @Override public Object call() {
534                return wrap(sobj.getOwnPropertyDescriptor(key), global);
535            }
536        });
537    }
538
539    /**
540     * return an array of own property keys associated with the object.
541     *
542     * @param all True if to include non-enumerable keys.
543     * @return Array of keys.
544     */
545    public String[] getOwnKeys(final boolean all) {
546        return inGlobal(new Callable<String[]>() {
547            @Override public String[] call() {
548                return sobj.getOwnKeys(all);
549            }
550        });
551    }
552
553    /**
554     * Flag this script object as non extensible
555     *
556     * @return the object after being made non extensible
557     */
558    public ScriptObjectMirror preventExtensions() {
559        return inGlobal(new Callable<ScriptObjectMirror>() {
560            @Override public ScriptObjectMirror call() {
561                sobj.preventExtensions();
562                return ScriptObjectMirror.this;
563            }
564        });
565    }
566
567    /**
568     * Check if this script object is extensible
569     * @return true if extensible
570     */
571    public boolean isExtensible() {
572        return inGlobal(new Callable<Boolean>() {
573            @Override public Boolean call() {
574                return sobj.isExtensible();
575            }
576        });
577    }
578
579    /**
580     * ECMAScript 15.2.3.8 - seal implementation
581     * @return the sealed script object
582     */
583    public ScriptObjectMirror seal() {
584        return inGlobal(new Callable<ScriptObjectMirror>() {
585            @Override public ScriptObjectMirror call() {
586                sobj.seal();
587                return ScriptObjectMirror.this;
588            }
589        });
590    }
591
592    /**
593     * Check whether this script object is sealed
594     * @return true if sealed
595     */
596    public boolean isSealed() {
597        return inGlobal(new Callable<Boolean>() {
598            @Override public Boolean call() {
599                return sobj.isSealed();
600            }
601        });
602    }
603
604    /**
605     * ECMA 15.2.39 - freeze implementation. Freeze this script object
606     * @return the frozen script object
607     */
608    public ScriptObjectMirror freeze() {
609        return inGlobal(new Callable<ScriptObjectMirror>() {
610            @Override public ScriptObjectMirror call() {
611                sobj.freeze();
612                return ScriptObjectMirror.this;
613            }
614        });
615    }
616
617    /**
618     * Check whether this script object is frozen
619     * @return true if frozen
620     */
621    public boolean isFrozen() {
622        return inGlobal(new Callable<Boolean>() {
623            @Override public Boolean call() {
624                return sobj.isFrozen();
625            }
626        });
627    }
628
629    /**
630     * Utility to check if given object is ECMAScript undefined value
631     *
632     * @param obj object to check
633     * @return true if 'obj' is ECMAScript undefined value
634     */
635    public static boolean isUndefined(final Object obj) {
636        return obj == ScriptRuntime.UNDEFINED;
637    }
638
639    /**
640     * Utility to convert this script object to the given type.
641     *
642     * @param <T> destination type to convert to
643     * @param type destination type to convert to
644     * @return converted object
645     */
646    public <T> T to(final Class<T> type) {
647        return inGlobal(new Callable<T>() {
648            @Override
649            public T call() {
650                return type.cast(ScriptUtils.convert(sobj, type));
651            }
652        });
653    }
654
655    /**
656     * Make a script object mirror on given object if needed. Also converts ConsString instances to Strings.
657     *
658     * @param obj object to be wrapped/converted
659     * @param homeGlobal global to which this object belongs. Not used for ConsStrings.
660     * @return wrapped/converted object
661     */
662    public static Object wrap(final Object obj, final Object homeGlobal) {
663        if(obj instanceof ScriptObject) {
664            return homeGlobal instanceof Global ? new ScriptObjectMirror((ScriptObject)obj, (Global)homeGlobal) : obj;
665        }
666        if(obj instanceof ConsString) {
667            return obj.toString();
668        }
669        return obj;
670    }
671
672    /**
673     * Unwrap a script object mirror if needed.
674     *
675     * @param obj object to be unwrapped
676     * @param homeGlobal global to which this object belongs
677     * @return unwrapped object
678     */
679    public static Object unwrap(final Object obj, final Object homeGlobal) {
680        if (obj instanceof ScriptObjectMirror) {
681            final ScriptObjectMirror mirror = (ScriptObjectMirror)obj;
682            return (mirror.global == homeGlobal)? mirror.sobj : obj;
683        }
684
685        return obj;
686    }
687
688    /**
689     * Wrap an array of object to script object mirrors if needed.
690     *
691     * @param args array to be unwrapped
692     * @param homeGlobal global to which this object belongs
693     * @return wrapped array
694     */
695    public static Object[] wrapArray(final Object[] args, final Object homeGlobal) {
696        if (args == null || args.length == 0) {
697            return args;
698        }
699
700        final Object[] newArgs = new Object[args.length];
701        int index = 0;
702        for (final Object obj : args) {
703            newArgs[index] = wrap(obj, homeGlobal);
704            index++;
705        }
706        return newArgs;
707    }
708
709    /**
710     * Unwrap an array of script object mirrors if needed.
711     *
712     * @param args array to be unwrapped
713     * @param homeGlobal global to which this object belongs
714     * @return unwrapped array
715     */
716    public static Object[] unwrapArray(final Object[] args, final Object homeGlobal) {
717        if (args == null || args.length == 0) {
718            return args;
719        }
720
721        final Object[] newArgs = new Object[args.length];
722        int index = 0;
723        for (final Object obj : args) {
724            newArgs[index] = unwrap(obj, homeGlobal);
725            index++;
726        }
727        return newArgs;
728    }
729
730    /**
731     * Are the given objects mirrors to same underlying object?
732     *
733     * @param obj1 first object
734     * @param obj2 second object
735     * @return true if obj1 and obj2 are identical script objects or mirrors of it.
736     */
737    public static boolean identical(final Object obj1, final Object obj2) {
738        final Object o1 = (obj1 instanceof ScriptObjectMirror)?
739            ((ScriptObjectMirror)obj1).sobj : obj1;
740
741        final Object o2 = (obj2 instanceof ScriptObjectMirror)?
742            ((ScriptObjectMirror)obj2).sobj : obj2;
743
744        return o1 == o2;
745    }
746
747    // package-privates below this.
748
749    ScriptObjectMirror(final ScriptObject sobj, final Global global) {
750        assert sobj != null : "ScriptObjectMirror on null!";
751        assert global != null : "home Global is null";
752
753        this.sobj = sobj;
754        this.global = global;
755        this.strict = global.isStrictContext();
756    }
757
758    // accessors for script engine
759    ScriptObject getScriptObject() {
760        return sobj;
761    }
762
763    Global getHomeGlobal() {
764        return global;
765    }
766
767    static Object translateUndefined(final Object obj) {
768        return (obj == ScriptRuntime.UNDEFINED)? null : obj;
769    }
770
771    private int getCallSiteFlags() {
772        return strict ? NashornCallSiteDescriptor.CALLSITE_STRICT : 0;
773    }
774
775    // internals only below this.
776    private <V> V inGlobal(final Callable<V> callable) {
777        final Global oldGlobal = Context.getGlobal();
778        final boolean globalChanged = (oldGlobal != global);
779        if (globalChanged) {
780            Context.setGlobal(global);
781        }
782        try {
783            return callable.call();
784        } catch (final NashornException ne) {
785            throw ne.initEcmaError(global);
786        } catch (final RuntimeException e) {
787            throw e;
788        } catch (final Exception e) {
789            throw new AssertionError("Cannot happen", e);
790        } finally {
791            if (globalChanged) {
792                Context.setGlobal(oldGlobal);
793            }
794        }
795    }
796
797    /**
798     * Ensures the key is not null, empty string, or a non-String object. The contract of the {@link Bindings}
799     * interface requires that these are not accepted as keys.
800     * @param key the key to check
801     * @throws NullPointerException if key is null
802     * @throws ClassCastException if key is not a String
803     * @throws IllegalArgumentException if key is empty string
804     */
805    private static void checkKey(final Object key) {
806        Objects.requireNonNull(key, "key can not be null");
807
808        if (!(key instanceof String)) {
809            throw new ClassCastException("key should be a String. It is " + key.getClass().getName() + " instead.");
810        } else if (((String)key).length() == 0) {
811            throw new IllegalArgumentException("key can not be empty");
812        }
813    }
814
815    @Override
816    public double toNumber() {
817        return inGlobal(new Callable<Double>() {
818            @Override public Double call() {
819                return JSType.toNumber(sobj);
820            }
821        });
822    }
823}
824