1 /*
   2  * Copyright (c) 2005, 2006, 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 com.sun.script.javascript;
  27 import sun.org.mozilla.javascript.internal.*;
  28 import javax.script.*;
  29 import java.util.*;
  30 
  31 /**
  32  * ExternalScriptable is an implementation of Scriptable
  33  * backed by a JSR 223 ScriptContext instance.
  34  *
  35  * @author Mike Grogan
  36  * @author A. Sundararajan
  37  * @since 1.6
  38  */
  39 
  40 final class ExternalScriptable implements Scriptable {
  41     /* Underlying ScriptContext that we use to store
  42      * named variables of this scope.
  43      */
  44     private ScriptContext context;
  45 
  46     /* JavaScript allows variables to be named as numbers (indexed
  47      * properties). This way arrays, objects (scopes) are treated uniformly.
  48      * Note that JSR 223 API supports only String named variables and
  49      * so we can't store these in Bindings. Also, JavaScript allows name
  50      * of the property name to be even empty String! Again, JSR 223 API
  51      * does not support empty name. So, we use the following fallback map
  52      * to store such variables of this scope. This map is not exposed to
  53      * JSR 223 API. We can just script objects "as is" and need not convert.
  54      */
  55     private Map<Object, Object> indexedProps;
  56 
  57     // my prototype
  58     private Scriptable prototype;
  59     // my parent scope, if any
  60     private Scriptable parent;
  61 
  62     ExternalScriptable(ScriptContext context) {
  63         this(context, new HashMap<Object, Object>());
  64     }
  65 
  66     ExternalScriptable(ScriptContext context, Map<Object, Object> indexedProps) {
  67         if (context == null) {
  68             throw new NullPointerException("context is null");
  69         }
  70         this.context = context;
  71         this.indexedProps = indexedProps;
  72     }
  73 
  74     ScriptContext getContext() {
  75         return context;
  76     }
  77 
  78     private boolean isEmpty(String name) {
  79         return name.equals("");
  80     }
  81 
  82     /**
  83      * Return the name of the class.
  84      */
  85     public String getClassName() {
  86         return "Global";
  87     }
  88 
  89     /**
  90      * Returns the value of the named property or NOT_FOUND.
  91      *
  92      * If the property was created using defineProperty, the
  93      * appropriate getter method is called.
  94      *
  95      * @param name the name of the property
  96      * @param start the object in which the lookup began
  97      * @return the value of the property (may be null), or NOT_FOUND
  98      */
  99     public synchronized Object get(String name, Scriptable start) {
 100         if (isEmpty(name)) {
 101             if (indexedProps.containsKey(name)) {
 102                 return indexedProps.get(name);
 103             } else {
 104                 return NOT_FOUND;
 105             }
 106         } else {
 107             synchronized (context) {
 108                 int scope = context.getAttributesScope(name);
 109                 if (scope != -1) {
 110                     Object value = context.getAttribute(name, scope);
 111                     return Context.javaToJS(value, this);
 112                 } else {
 113                     return NOT_FOUND;
 114                 }
 115             }
 116         }
 117     }
 118 
 119     /**
 120      * Returns the value of the indexed property or NOT_FOUND.
 121      *
 122      * @param index the numeric index for the property
 123      * @param start the object in which the lookup began
 124      * @return the value of the property (may be null), or NOT_FOUND
 125      */
 126     public synchronized Object get(int index, Scriptable start) {
 127         Integer key = new Integer(index);
 128         if (indexedProps.containsKey(index)) {
 129             return indexedProps.get(key);
 130         } else {
 131             return NOT_FOUND;
 132         }
 133     }
 134 
 135     /**
 136      * Returns true if the named property is defined.
 137      *
 138      * @param name the name of the property
 139      * @param start the object in which the lookup began
 140      * @return true if and only if the property was found in the object
 141      */
 142     public synchronized boolean has(String name, Scriptable start) {
 143         if (isEmpty(name)) {
 144             return indexedProps.containsKey(name);
 145         } else {
 146             synchronized (context) {
 147                 return context.getAttributesScope(name) != -1;
 148             }
 149         }
 150     }
 151 
 152     /**
 153      * Returns true if the property index is defined.
 154      *
 155      * @param index the numeric index for the property
 156      * @param start the object in which the lookup began
 157      * @return true if and only if the property was found in the object
 158      */
 159     public synchronized boolean has(int index, Scriptable start) {
 160         Integer key = new Integer(index);
 161         return indexedProps.containsKey(key);
 162     }
 163 
 164     /**
 165      * Sets the value of the named property, creating it if need be.
 166      *
 167      * @param name the name of the property
 168      * @param start the object whose property is being set
 169      * @param value value to set the property to
 170      */
 171     public void put(String name, Scriptable start, Object value) {
 172         if (start == this) {
 173             synchronized (this) {
 174                 if (isEmpty(name)) {
 175                     indexedProps.put(name, value);
 176                 } else {
 177                     synchronized (context) {
 178                         int scope = context.getAttributesScope(name);
 179                         if (scope == -1) {
 180                             scope = ScriptContext.ENGINE_SCOPE;
 181                         }
 182                         context.setAttribute(name, jsToJava(value), scope);
 183                     }
 184                 }
 185             }
 186         } else {
 187             start.put(name, start, value);
 188         }
 189     }
 190 
 191     /**
 192      * Sets the value of the indexed property, creating it if need be.
 193      *
 194      * @param index the numeric index for the property
 195      * @param start the object whose property is being set
 196      * @param value value to set the property to
 197      */
 198     public void put(int index, Scriptable start, Object value) {
 199         if (start == this) {
 200             synchronized (this) {
 201                 indexedProps.put(new Integer(index), value);
 202             }
 203         } else {
 204             start.put(index, start, value);
 205         }
 206     }
 207 
 208     /**
 209      * Removes a named property from the object.
 210      *
 211      * If the property is not found, no action is taken.
 212      *
 213      * @param name the name of the property
 214      */
 215     public synchronized void delete(String name) {
 216         if (isEmpty(name)) {
 217             indexedProps.remove(name);
 218         } else {
 219             synchronized (context) {
 220                 int scope = context.getAttributesScope(name);
 221                 if (scope != -1) {
 222                     context.removeAttribute(name, scope);
 223                 }
 224             }
 225         }
 226     }
 227 
 228     /**
 229      * Removes the indexed property from the object.
 230      *
 231      * If the property is not found, no action is taken.
 232      *
 233      * @param index the numeric index for the property
 234      */
 235     public void delete(int index) {
 236         indexedProps.remove(new Integer(index));
 237     }
 238 
 239     /**
 240      * Get the prototype of the object.
 241      * @return the prototype
 242      */
 243     public Scriptable getPrototype() {
 244         return prototype;
 245     }
 246 
 247     /**
 248      * Set the prototype of the object.
 249      * @param prototype the prototype to set
 250      */
 251     public void setPrototype(Scriptable prototype) {
 252         this.prototype = prototype;
 253     }
 254 
 255     /**
 256      * Get the parent scope of the object.
 257      * @return the parent scope
 258      */
 259     public Scriptable getParentScope() {
 260         return parent;
 261     }
 262 
 263     /**
 264      * Set the parent scope of the object.
 265      * @param parent the parent scope to set
 266      */
 267     public void setParentScope(Scriptable parent) {
 268         this.parent = parent;
 269     }
 270 
 271      /**
 272      * Get an array of property ids.
 273      *
 274      * Not all property ids need be returned. Those properties
 275      * whose ids are not returned are considered non-enumerable.
 276      *
 277      * @return an array of Objects. Each entry in the array is either
 278      *         a java.lang.String or a java.lang.Number
 279      */
 280     public synchronized Object[] getIds() {
 281         String[] keys = getAllKeys();
 282         int size = keys.length + indexedProps.size();
 283         Object[] res = new Object[size];
 284         System.arraycopy(keys, 0, res, 0, keys.length);
 285         int i = keys.length;
 286         // now add all indexed properties
 287         for (Object index : indexedProps.keySet()) {
 288             res[i++] = index;
 289         }
 290         return res;
 291     }
 292 
 293     /**
 294      * Get the default value of the object with a given hint.
 295      * The hints are String.class for type String, Number.class for type
 296      * Number, Scriptable.class for type Object, and Boolean.class for
 297      * type Boolean. <p>
 298      *
 299      * A <code>hint</code> of null means "no hint".
 300      *
 301      * See ECMA 8.6.2.6.
 302      *
 303      * @param hint the type hint
 304      * @return the default value
 305      */
 306     public Object getDefaultValue(Class typeHint) {
 307         for (int i=0; i < 2; i++) {
 308             boolean tryToString;
 309             if (typeHint == ScriptRuntime.StringClass) {
 310                 tryToString = (i == 0);
 311             } else {
 312                 tryToString = (i == 1);
 313             }
 314 
 315             String methodName;
 316             Object[] args;
 317             if (tryToString) {
 318                 methodName = "toString";
 319                 args = ScriptRuntime.emptyArgs;
 320             } else {
 321                 methodName = "valueOf";
 322                 args = new Object[1];
 323                 String hint;
 324                 if (typeHint == null) {
 325                     hint = "undefined";
 326                 } else if (typeHint == ScriptRuntime.StringClass) {
 327                     hint = "string";
 328                 } else if (typeHint == ScriptRuntime.ScriptableClass) {
 329                     hint = "object";
 330                 } else if (typeHint == ScriptRuntime.FunctionClass) {
 331                     hint = "function";
 332                 } else if (typeHint == ScriptRuntime.BooleanClass
 333                            || typeHint == Boolean.TYPE)
 334                 {
 335                     hint = "boolean";
 336                 } else if (typeHint == ScriptRuntime.NumberClass ||
 337                          typeHint == ScriptRuntime.ByteClass ||
 338                          typeHint == Byte.TYPE ||
 339                          typeHint == ScriptRuntime.ShortClass ||
 340                          typeHint == Short.TYPE ||
 341                          typeHint == ScriptRuntime.IntegerClass ||
 342                          typeHint == Integer.TYPE ||
 343                          typeHint == ScriptRuntime.FloatClass ||
 344                          typeHint == Float.TYPE ||
 345                          typeHint == ScriptRuntime.DoubleClass ||
 346                          typeHint == Double.TYPE)
 347                 {
 348                     hint = "number";
 349                 } else {
 350                     throw Context.reportRuntimeError(
 351                         "Invalid JavaScript value of type " +
 352                         typeHint.toString());
 353                 }
 354                 args[0] = hint;
 355             }
 356             Object v = ScriptableObject.getProperty(this, methodName);
 357             if (!(v instanceof Function))
 358                 continue;
 359             Function fun = (Function) v;
 360             Context cx = RhinoScriptEngine.enterContext();
 361             try {
 362                 v = fun.call(cx, fun.getParentScope(), this, args);
 363             } finally {
 364                 cx.exit();
 365             }
 366             if (v != null) {
 367                 if (!(v instanceof Scriptable)) {
 368                     return v;
 369                 }
 370                 if (typeHint == ScriptRuntime.ScriptableClass
 371                     || typeHint == ScriptRuntime.FunctionClass)
 372                 {
 373                     return v;
 374                 }
 375                 if (tryToString && v instanceof Wrapper) {
 376                     // Let a wrapped java.lang.String pass for a primitive
 377                     // string.
 378                     Object u = ((Wrapper)v).unwrap();
 379                     if (u instanceof String)
 380                         return u;
 381                 }
 382             }
 383         }
 384         // fall through to error
 385         String arg = (typeHint == null) ? "undefined" : typeHint.getName();
 386         throw Context.reportRuntimeError(
 387                   "Cannot find default value for object " + arg);
 388     }
 389 
 390     /**
 391      * Implements the instanceof operator.
 392      *
 393      * @param instance The value that appeared on the LHS of the instanceof
 394      *              operator
 395      * @return true if "this" appears in value's prototype chain
 396      *
 397      */
 398     public boolean hasInstance(Scriptable instance) {
 399         // Default for JS objects (other than Function) is to do prototype
 400         // chasing.
 401         Scriptable proto = instance.getPrototype();
 402         while (proto != null) {
 403             if (proto.equals(this)) return true;
 404             proto = proto.getPrototype();
 405         }
 406         return false;
 407     }
 408 
 409     private String[] getAllKeys() {
 410         ArrayList<String> list = new ArrayList<String>();
 411         synchronized (context) {
 412             for (int scope : context.getScopes()) {
 413                 Bindings bindings = context.getBindings(scope);
 414                 if (bindings != null) {
 415                     list.ensureCapacity(bindings.size());
 416                     for (String key : bindings.keySet()) {
 417                         list.add(key);
 418                     }
 419                 }
 420             }
 421         }
 422         String[] res = new String[list.size()];
 423         list.toArray(res);
 424         return res;
 425     }
 426 
 427    /**
 428     * We convert script values to the nearest Java value.
 429     * We unwrap wrapped Java objects so that access from
 430     * Bindings.get() would return "workable" value for Java.
 431     * But, at the same time, we need to make few special cases
 432     * and hence the following function is used.
 433     */
 434     private Object jsToJava(Object jsObj) {
 435         if (jsObj instanceof Wrapper) {
 436             Wrapper njb = (Wrapper) jsObj;
 437             /* importClass feature of ImporterTopLevel puts
 438              * NativeJavaClass in global scope. If we unwrap
 439              * it, importClass won't work.
 440              */
 441             if (njb instanceof NativeJavaClass) {
 442                 return njb;
 443             }
 444 
 445             /* script may use Java primitive wrapper type objects
 446              * (such as java.lang.Integer, java.lang.Boolean etc)
 447              * explicitly. If we unwrap, then these script objects
 448              * will become script primitive types. For example,
 449              *
 450              *    var x = new java.lang.Double(3.0); print(typeof x);
 451              *
 452              * will print 'number'. We don't want that to happen.
 453              */
 454             Object obj = njb.unwrap();
 455             if (obj instanceof Number || obj instanceof String ||
 456                 obj instanceof Boolean || obj instanceof Character) {
 457                 // special type wrapped -- we just leave it as is.
 458                 return njb;
 459             } else {
 460                 // return unwrapped object for any other object.
 461                 return obj;
 462             }
 463         } else { // not-a-Java-wrapper
 464             return jsObj;
 465         }
 466     }
 467 }