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 }