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 
  26 package jdk.nashorn.api.scripting;
  27 
  28 import java.security.AccessController;
  29 import java.security.PrivilegedAction;
  30 import java.util.AbstractMap;
  31 import java.util.ArrayList;
  32 import java.util.Collection;
  33 import java.util.Collections;
  34 import java.util.LinkedHashSet;
  35 import java.util.Iterator;
  36 import java.util.List;
  37 import java.util.Map;
  38 import java.util.Set;
  39 import java.util.concurrent.Callable;
  40 import javax.script.Bindings;
  41 import jdk.nashorn.internal.runtime.Context;
  42 import jdk.nashorn.internal.runtime.ScriptFunction;
  43 import jdk.nashorn.internal.runtime.ScriptObject;
  44 import jdk.nashorn.internal.runtime.ScriptRuntime;
  45 
  46 /**
  47  * Mirror object that wraps a given ScriptObject instance. User can
  48  * access ScriptObject via the javax.script.Bindings interface or
  49  * netscape.javascript.JSObject interface.
  50  */
  51 public final class ScriptObjectMirror extends JSObject implements Bindings {
  52     private final ScriptObject sobj;
  53     private final ScriptObject global;
  54 
  55     ScriptObjectMirror(final ScriptObject sobj, final ScriptObject global) {
  56         this.sobj = sobj;
  57         this.global = global;
  58     }
  59 
  60     @Override
  61     public boolean equals(final Object other) {
  62         if (other instanceof ScriptObjectMirror) {
  63             return sobj.equals(((ScriptObjectMirror)other).sobj);
  64         }
  65 
  66         return false;
  67     }
  68 
  69     @Override
  70     public int hashCode() {
  71         return sobj.hashCode();
  72     }
  73 
  74     @Override
  75     public String toString() {
  76         return inGlobal(new Callable<String>() {
  77             @Override
  78             public String call() {
  79                 return ScriptRuntime.safeToString(sobj);
  80             }
  81         });
  82     }
  83 
  84     private <V> V inGlobal(final Callable<V> callable) {
  85         final ScriptObject oldGlobal = NashornScriptEngine.getNashornGlobal();
  86         final boolean globalChanged = (oldGlobal != global);
  87         if (globalChanged) {
  88             NashornScriptEngine.setNashornGlobal(global);
  89         }
  90         try {
  91             return callable.call();
  92         } catch (final RuntimeException e) {
  93             throw e;
  94         } catch (final Exception e) {
  95             throw new AssertionError("Cannot happen", e);
  96         } finally {
  97             if (globalChanged) {
  98                 NashornScriptEngine.setNashornGlobal(oldGlobal);
  99             }
 100         }
 101     }
 102 
 103     // JSObject methods
 104     @Override
 105     public Object call(final String methodName, final Object args[]) {
 106         final ScriptObject oldGlobal = NashornScriptEngine.getNashornGlobal();
 107         final boolean globalChanged = (oldGlobal != global);
 108 
 109         try {
 110             if (globalChanged) {
 111                 NashornScriptEngine.setNashornGlobal(global);
 112             }
 113 
 114             final Object val = sobj.get(methodName);
 115             if (! (val instanceof ScriptFunction)) {
 116                 throw new RuntimeException("No such method: " + methodName);
 117             }
 118 
 119             final Object[] modArgs = globalChanged? wrapArray(args, oldGlobal) : args;
 120             return wrap(ScriptRuntime.checkAndApply((ScriptFunction)val, sobj, unwrapArray(modArgs, global)), global);
 121         } catch (final RuntimeException | Error e) {
 122             throw e;
 123         } catch (final Throwable t) {
 124             throw new RuntimeException(t);
 125         } finally {
 126             if (globalChanged) {
 127                 NashornScriptEngine.setNashornGlobal(oldGlobal);
 128             }
 129         }
 130     }
 131 
 132     @Override
 133     public Object eval(final String s) {
 134         return inGlobal(new Callable<Object>() {
 135             @Override
 136             public Object call() {
 137                 final Context context = AccessController.doPrivileged(
 138                         new PrivilegedAction<Context>() {
 139                             @Override
 140                             public Context run() {
 141                                 return Context.getContext();
 142                             }
 143                         });
 144                 return wrap(context.eval(global, s, null, null, false), global);
 145             }
 146         });
 147     }
 148 
 149     @Override
 150     public Object getMember(final String name) {
 151         return inGlobal(new Callable<Object>() {
 152             @Override public Object call() {
 153                 return wrap(sobj.get(name), global);
 154             }
 155         });
 156     }
 157 
 158     @Override
 159     public Object getSlot(final int index) {
 160         return inGlobal(new Callable<Object>() {
 161             @Override public Object call() {
 162                 return wrap(sobj.get(index), global);
 163             }
 164         });
 165     }
 166 
 167     @Override
 168     public void removeMember(final String name) {
 169         remove(name);
 170     }
 171 
 172     @Override
 173     public void setMember(final String name, final Object value) {
 174         put(name, value);
 175     }
 176 
 177     @Override
 178     public void setSlot(final int index, final Object value) {
 179         inGlobal(new Callable<Void>() {
 180             @Override public Void call() {
 181                 sobj.set(index, unwrap(value, global), global.isStrictContext());
 182                 return null;
 183             }
 184         });
 185     }
 186 
 187     @Override
 188     public void clear() {
 189         inGlobal(new Callable<Object>() {
 190             @Override public Object call() {
 191                 sobj.clear();
 192                 return null;
 193             }
 194         });
 195     }
 196 
 197     @Override
 198     public boolean containsKey(final Object key) {
 199         return inGlobal(new Callable<Boolean>() {
 200             @Override public Boolean call() {
 201                 return sobj.containsKey(unwrap(key, global));
 202             }
 203         });
 204     }
 205 
 206     @Override
 207     public boolean containsValue(final Object value) {
 208         return inGlobal(new Callable<Boolean>() {
 209             @Override public Boolean call() {
 210                 return sobj.containsValue(unwrap(value, global));
 211             }
 212         });
 213     }
 214 
 215     @Override
 216     public Set<Map.Entry<String, Object>> entrySet() {
 217         return inGlobal(new Callable<Set<Map.Entry<String, Object>>>() {
 218             @Override public Set<Map.Entry<String, Object>> call() {
 219                 final Iterator<String>               iter    = sobj.propertyIterator();
 220                 final Set<Map.Entry<String, Object>> entries = new LinkedHashSet<>();
 221 
 222                 while (iter.hasNext()) {
 223                     final String key   = iter.next();
 224                     final Object value = translateUndefined(wrap(sobj.get(key), global));
 225                     entries.add(new AbstractMap.SimpleImmutableEntry<>(key, value));
 226                 }
 227 
 228                 return Collections.unmodifiableSet(entries);
 229             }
 230         });
 231     }
 232 
 233     @Override
 234     public Object get(final Object key) {
 235         return inGlobal(new Callable<Object>() {
 236             @Override public Object call() {
 237                 return translateUndefined(wrap(sobj.get(key), global));
 238             }
 239         });
 240     }
 241 
 242     @Override
 243     public boolean isEmpty() {
 244         return inGlobal(new Callable<Boolean>() {
 245             @Override public Boolean call() {
 246                 return sobj.isEmpty();
 247             }
 248         });
 249     }
 250 
 251     @Override
 252     public Set<String> keySet() {
 253         return inGlobal(new Callable<Set<String>>() {
 254             @Override public Set<String> call() {
 255                 final Iterator<String> iter   = sobj.propertyIterator();
 256                 final Set<String>      keySet = new LinkedHashSet<>();
 257 
 258                 while (iter.hasNext()) {
 259                     keySet.add(iter.next());
 260                 }
 261 
 262                 return Collections.unmodifiableSet(keySet);
 263             }
 264         });
 265     }
 266 
 267     @Override
 268     public Object put(final String key, final Object value) {
 269         final ScriptObject oldGlobal = NashornScriptEngine.getNashornGlobal();
 270         final boolean globalChanged = (oldGlobal != global);
 271         return inGlobal(new Callable<Object>() {
 272             @Override public Object call() {
 273                 final Object modValue = globalChanged? wrap(value, oldGlobal) : value;
 274                 return translateUndefined(wrap(sobj.put(key, unwrap(modValue, global)), global));
 275             }
 276         });
 277     }
 278 
 279     @Override
 280     public void putAll(final Map<? extends String, ? extends Object> map) {
 281         final ScriptObject oldGlobal = NashornScriptEngine.getNashornGlobal();
 282         final boolean globalChanged = (oldGlobal != global);
 283         final boolean strict = sobj.isStrictContext();
 284         inGlobal(new Callable<Object>() {
 285             @Override public Object call() {
 286                 for (final Map.Entry<? extends String, ? extends Object> entry : map.entrySet()) {
 287                     final Object value = entry.getValue();
 288                     final Object modValue = globalChanged? wrap(value, oldGlobal) : value;
 289                     sobj.set(entry.getKey(), unwrap(modValue, global), strict);
 290                 }
 291                 return null;
 292             }
 293         });
 294     }
 295 
 296     @Override
 297     public Object remove(final Object key) {
 298         return inGlobal(new Callable<Object>() {
 299             @Override public Object call() {
 300                 return wrap(sobj.remove(unwrap(key, global)), global);
 301             }
 302         });
 303     }
 304 
 305     /**
 306      * Delete a property from this object.
 307      *
 308      * @param key the property to be deleted
 309      *
 310      * @return if the delete was successful or not
 311      */
 312     public boolean delete(final Object key) {
 313         return inGlobal(new Callable<Boolean>() {
 314             @Override public Boolean call() {
 315                 return sobj.delete(unwrap(key, global));
 316             }
 317         });
 318     }
 319 
 320     @Override
 321     public int size() {
 322         return inGlobal(new Callable<Integer>() {
 323             @Override public Integer call() {
 324                 return sobj.size();
 325             }
 326         });
 327     }
 328 
 329     @Override
 330     public Collection<Object> values() {
 331         return inGlobal(new Callable<Collection<Object>>() {
 332             @Override public Collection<Object> call() {
 333                 final List<Object>     values = new ArrayList<>(size());
 334                 final Iterator<Object> iter   = sobj.valueIterator();
 335 
 336                 while (iter.hasNext()) {
 337                     values.add(translateUndefined(wrap(iter.next(), global)));
 338                 }
 339 
 340                 return Collections.unmodifiableList(values);
 341             }
 342         });
 343     }
 344 
 345     // Support for ECMAScript Object API on mirrors
 346 
 347     /**
 348      * Return the __proto__ of this object.
 349      * @return __proto__ object.
 350      */
 351     public Object getProto() {
 352         return inGlobal(new Callable<Object>() {
 353             @Override public Object call() {
 354                 return wrap(getScriptObject().getProto(), global);
 355             }
 356         });
 357     }
 358 
 359     /**
 360      * ECMA 8.12.1 [[GetOwnProperty]] (P)
 361      *
 362      * @param key property key
 363      *
 364      * @return Returns the Property Descriptor of the named own property of this
 365      * object, or undefined if absent.
 366      */
 367     public Object getOwnPropertyDescriptor(final String key) {
 368         return inGlobal(new Callable<Object>() {
 369             @Override public Object call() {
 370                 return wrap(getScriptObject().getOwnPropertyDescriptor(key), global);
 371             }
 372         });
 373     }
 374 
 375     /**
 376      * return an array of own property keys associated with the object.
 377      *
 378      * @param all True if to include non-enumerable keys.
 379      * @return Array of keys.
 380      */
 381     public String[] getOwnKeys(final boolean all) {
 382         return inGlobal(new Callable<String[]>() {
 383             @Override public String[] call() {
 384                 return getScriptObject().getOwnKeys(all);
 385             }
 386         });
 387     }
 388 
 389     /**
 390      * Flag this script object as non extensible
 391      *
 392      * @return the object after being made non extensible
 393      */
 394     public ScriptObjectMirror preventExtensions() {
 395         return inGlobal(new Callable<ScriptObjectMirror>() {
 396             @Override public ScriptObjectMirror call() {
 397                 getScriptObject().preventExtensions();
 398                 return ScriptObjectMirror.this;
 399             }
 400         });
 401     }
 402 
 403     /**
 404      * Check if this script object is extensible
 405      * @return true if extensible
 406      */
 407     public boolean isExtensible() {
 408         return inGlobal(new Callable<Boolean>() {
 409             @Override public Boolean call() {
 410                 return getScriptObject().isExtensible();
 411             }
 412         });
 413     }
 414 
 415     /**
 416      * ECMAScript 15.2.3.8 - seal implementation
 417      * @return the sealed script object
 418      */
 419     public ScriptObjectMirror seal() {
 420         return inGlobal(new Callable<ScriptObjectMirror>() {
 421             @Override public ScriptObjectMirror call() {
 422                 getScriptObject().seal();
 423                 return ScriptObjectMirror.this;
 424             }
 425         });
 426     }
 427 
 428     /**
 429      * Check whether this script object is sealed
 430      * @return true if sealed
 431      */
 432     public boolean isSealed() {
 433         return inGlobal(new Callable<Boolean>() {
 434             @Override public Boolean call() {
 435                 return getScriptObject().isSealed();
 436             }
 437         });
 438     }
 439 
 440     /**
 441      * ECMA 15.2.39 - freeze implementation. Freeze this script object
 442      * @return the frozen script object
 443      */
 444     public ScriptObjectMirror freeze() {
 445         return inGlobal(new Callable<ScriptObjectMirror>() {
 446             @Override public ScriptObjectMirror call() {
 447                 getScriptObject().freeze();
 448                 return ScriptObjectMirror.this;
 449             }
 450         });
 451     }
 452 
 453     /**
 454      * Check whether this script object is frozen
 455      * @return true if frozen
 456      */
 457     public boolean isFrozen() {
 458         return inGlobal(new Callable<Boolean>() {
 459             @Override public Boolean call() {
 460                 return getScriptObject().isFrozen();
 461             }
 462         });
 463     }
 464 
 465     // ECMAScript instanceof check
 466 
 467     /**
 468      * Checking whether a script object is an instance of another by
 469      * walking the proto chain
 470      *
 471      * @param instance instace to check
 472      * @return true if 'instance' is an instance of this object
 473      */
 474     public boolean isInstance(final ScriptObjectMirror instance) {
 475         // if not belongs to my global scope, return false
 476         if (instance == null || global != instance.global) {
 477             return false;
 478         }
 479 
 480         return inGlobal(new Callable<Boolean>() {
 481             @Override public Boolean call() {
 482                 return getScriptObject().isInstance(instance.getScriptObject());
 483             }
 484         });
 485     }
 486 
 487     /**
 488      * Utility to check if given object is ECMAScript undefined value
 489      *
 490      * @param obj object to check
 491      * @return true if 'obj' is ECMAScript undefined value
 492      */
 493     public static boolean isUndefined(final Object obj) {
 494         return obj == ScriptRuntime.UNDEFINED;
 495     }
 496 
 497     /**
 498      * is this a function object?
 499      *
 500      * @return if this mirror wraps a ECMAScript function instance
 501      */
 502     public boolean isFunction() {
 503         return getScriptObject() instanceof ScriptFunction;
 504     }
 505 
 506     /**
 507      * is this a 'use strict' function object?
 508      *
 509      * @return true if this mirror represents a ECMAScript 'use strict' function
 510      */
 511     public boolean isStrictFunction() {
 512         return isFunction() && ((ScriptFunction)getScriptObject()).isStrict();
 513     }
 514 
 515     /**
 516      * is this an array object?
 517      *
 518      * @return if this mirror wraps a ECMAScript array object
 519      */
 520     public boolean isArray() {
 521         return getScriptObject().isArray();
 522     }
 523 
 524     // These are public only so that Context can access these.
 525 
 526     /**
 527      * Make a script object mirror on given object if needed.
 528      *
 529      * @param obj object to be wrapped
 530      * @param homeGlobal global to which this object belongs
 531      * @return wrapped object
 532      */
 533     public static Object wrap(final Object obj, final ScriptObject homeGlobal) {
 534         return (obj instanceof ScriptObject) ? new ScriptObjectMirror((ScriptObject)obj, homeGlobal) : obj;
 535     }
 536 
 537     /**
 538      * Unwrap a script object mirror if needed.
 539      *
 540      * @param obj object to be unwrapped
 541      * @param homeGlobal global to which this object belongs
 542      * @return unwrapped object
 543      */
 544     public static Object unwrap(final Object obj, final ScriptObject homeGlobal) {
 545         if (obj instanceof ScriptObjectMirror) {
 546             final ScriptObjectMirror mirror = (ScriptObjectMirror)obj;
 547             return (mirror.global == homeGlobal)? mirror.sobj : obj;
 548         }
 549 
 550         return obj;
 551     }
 552 
 553     /**
 554      * Wrap an array of object to script object mirrors if needed.
 555      *
 556      * @param args array to be unwrapped
 557      * @param homeGlobal global to which this object belongs
 558      * @return wrapped array
 559      */
 560     public static Object[] wrapArray(final Object[] args, final ScriptObject homeGlobal) {
 561         if (args == null || args.length == 0) {
 562             return args;
 563         }
 564 
 565         final Object[] newArgs = new Object[args.length];
 566         int index = 0;
 567         for (final Object obj : args) {
 568             newArgs[index] = wrap(obj, homeGlobal);
 569             index++;
 570         }
 571         return newArgs;
 572     }
 573 
 574     /**
 575      * Unwrap an array of script object mirrors if needed.
 576      *
 577      * @param args array to be unwrapped
 578      * @param homeGlobal global to which this object belongs
 579      * @return unwrapped array
 580      */
 581     public static Object[] unwrapArray(final Object[] args, final ScriptObject homeGlobal) {
 582         if (args == null || args.length == 0) {
 583             return args;
 584         }
 585 
 586         final Object[] newArgs = new Object[args.length];
 587         int index = 0;
 588         for (final Object obj : args) {
 589             newArgs[index] = unwrap(obj, homeGlobal);
 590             index++;
 591         }
 592         return newArgs;
 593     }
 594 
 595     // package-privates below this.
 596     ScriptObject getScriptObject() {
 597         return sobj;
 598     }
 599 
 600     static Object translateUndefined(Object obj) {
 601         return (obj == ScriptRuntime.UNDEFINED)? null : obj;
 602     }
 603 }