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 } |
|