/*
* Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.script.javascript;
import sun.org.mozilla.javascript.internal.*;
import javax.script.*;
import java.util.*;
/**
* ExternalScriptable is an implementation of Scriptable
* backed by a JSR 223 ScriptContext instance.
*
* @author Mike Grogan
* @author A. Sundararajan
* @since 1.6
*/
final class ExternalScriptable implements Scriptable {
/* Underlying ScriptContext that we use to store
* named variables of this scope.
*/
private ScriptContext context;
/* JavaScript allows variables to be named as numbers (indexed
* properties). This way arrays, objects (scopes) are treated uniformly.
* Note that JSR 223 API supports only String named variables and
* so we can't store these in Bindings. Also, JavaScript allows name
* of the property name to be even empty String! Again, JSR 223 API
* does not support empty name. So, we use the following fallback map
* to store such variables of this scope. This map is not exposed to
* JSR 223 API. We can just script objects "as is" and need not convert.
*/
private Map<Object, Object> indexedProps;
// my prototype
private Scriptable prototype;
// my parent scope, if any
private Scriptable parent;
ExternalScriptable(ScriptContext context) {
this(context, new HashMap<Object, Object>());
}
ExternalScriptable(ScriptContext context, Map<Object, Object> indexedProps) {
if (context == null) {
throw new NullPointerException("context is null");
}
this.context = context;
this.indexedProps = indexedProps;
}
ScriptContext getContext() {
return context;
}
private boolean isEmpty(String name) {
return name.equals("");
}
/**
* Return the name of the class.
*/
public String getClassName() {
return "Global";
}
/**
* Returns the value of the named property or NOT_FOUND.
*
* If the property was created using defineProperty, the
* appropriate getter method is called.
*
* @param name the name of the property
* @param start the object in which the lookup began
* @return the value of the property (may be null), or NOT_FOUND
*/
public synchronized Object get(String name, Scriptable start) {
if (isEmpty(name)) {
if (indexedProps.containsKey(name)) {
return indexedProps.get(name);
} else {
return NOT_FOUND;
}
} else {
synchronized (context) {
int scope = context.getAttributesScope(name);
if (scope != -1) {
Object value = context.getAttribute(name, scope);
return Context.javaToJS(value, this);
} else {
return NOT_FOUND;
}
}
}
}
/**
* Returns the value of the indexed property or NOT_FOUND.
*
* @param index the numeric index for the property
* @param start the object in which the lookup began
* @return the value of the property (may be null), or NOT_FOUND
*/
public synchronized Object get(int index, Scriptable start) {
Integer key = new Integer(index);
if (indexedProps.containsKey(index)) {
return indexedProps.get(key);
} else {
return NOT_FOUND;
}
}
/**
* Returns true if the named property is defined.
*
* @param name the name of the property
* @param start the object in which the lookup began
* @return true if and only if the property was found in the object
*/
public synchronized boolean has(String name, Scriptable start) {
if (isEmpty(name)) {
return indexedProps.containsKey(name);
} else {
synchronized (context) {
return context.getAttributesScope(name) != -1;
}
}
}
/**
* Returns true if the property index is defined.
*
* @param index the numeric index for the property
* @param start the object in which the lookup began
* @return true if and only if the property was found in the object
*/
public synchronized boolean has(int index, Scriptable start) {
Integer key = new Integer(index);
return indexedProps.containsKey(key);
}
/**
* Sets the value of the named property, creating it if need be.
*
* @param name the name of the property
* @param start the object whose property is being set
* @param value value to set the property to
*/
public void put(String name, Scriptable start, Object value) {
if (start == this) {
synchronized (this) {
if (isEmpty(name)) {
indexedProps.put(name, value);
} else {
synchronized (context) {
int scope = context.getAttributesScope(name);
if (scope == -1) {
scope = ScriptContext.ENGINE_SCOPE;
}
context.setAttribute(name, jsToJava(value), scope);
}
}
}
} else {
start.put(name, start, value);
}
}
/**
* Sets the value of the indexed property, creating it if need be.
*
* @param index the numeric index for the property
* @param start the object whose property is being set
* @param value value to set the property to
*/
public void put(int index, Scriptable start, Object value) {
if (start == this) {
synchronized (this) {
indexedProps.put(new Integer(index), value);
}
} else {
start.put(index, start, value);
}
}
/**
* Removes a named property from the object.
*
* If the property is not found, no action is taken.
*
* @param name the name of the property
*/
public synchronized void delete(String name) {
if (isEmpty(name)) {
indexedProps.remove(name);
} else {
synchronized (context) {
int scope = context.getAttributesScope(name);
if (scope != -1) {
context.removeAttribute(name, scope);
}
}
}
}
/**
* Removes the indexed property from the object.
*
* If the property is not found, no action is taken.
*
* @param index the numeric index for the property
*/
public void delete(int index) {
indexedProps.remove(new Integer(index));
}
/**
* Get the prototype of the object.
* @return the prototype
*/
public Scriptable getPrototype() {
return prototype;
}
/**
* Set the prototype of the object.
* @param prototype the prototype to set
*/
public void setPrototype(Scriptable prototype) {
this.prototype = prototype;
}
/**
* Get the parent scope of the object.
* @return the parent scope
*/
public Scriptable getParentScope() {
return parent;
}
/**
* Set the parent scope of the object.
* @param parent the parent scope to set
*/
public void setParentScope(Scriptable parent) {
this.parent = parent;
}
/**
* Get an array of property ids.
*
* Not all property ids need be returned. Those properties
* whose ids are not returned are considered non-enumerable.
*
* @return an array of Objects. Each entry in the array is either
* a java.lang.String or a java.lang.Number
*/
public synchronized Object[] getIds() {
String[] keys = getAllKeys();
int size = keys.length + indexedProps.size();
Object[] res = new Object[size];
System.arraycopy(keys, 0, res, 0, keys.length);
int i = keys.length;
// now add all indexed properties
for (Object index : indexedProps.keySet()) {
res[i++] = index;
}
return res;
}
/**
* Get the default value of the object with a given hint.
* The hints are String.class for type String, Number.class for type
* Number, Scriptable.class for type Object, and Boolean.class for
* type Boolean. <p>
*
* A <code>hint</code> of null means "no hint".
*
* See ECMA 8.6.2.6.
*
* @param hint the type hint
* @return the default value
*/
public Object getDefaultValue(Class typeHint) {
for (int i=0; i < 2; i++) {
boolean tryToString;
if (typeHint == ScriptRuntime.StringClass) {
tryToString = (i == 0);
} else {
tryToString = (i == 1);
}
String methodName;
Object[] args;
if (tryToString) {
methodName = "toString";
args = ScriptRuntime.emptyArgs;
} else {
methodName = "valueOf";
args = new Object[1];
String hint;
if (typeHint == null) {
hint = "undefined";
} else if (typeHint == ScriptRuntime.StringClass) {
hint = "string";
} else if (typeHint == ScriptRuntime.ScriptableClass) {
hint = "object";
} else if (typeHint == ScriptRuntime.FunctionClass) {
hint = "function";
} else if (typeHint == ScriptRuntime.BooleanClass
|| typeHint == Boolean.TYPE)
{
hint = "boolean";
} else if (typeHint == ScriptRuntime.NumberClass ||
typeHint == ScriptRuntime.ByteClass ||
typeHint == Byte.TYPE ||
typeHint == ScriptRuntime.ShortClass ||
typeHint == Short.TYPE ||
typeHint == ScriptRuntime.IntegerClass ||
typeHint == Integer.TYPE ||
typeHint == ScriptRuntime.FloatClass ||
typeHint == Float.TYPE ||
typeHint == ScriptRuntime.DoubleClass ||
typeHint == Double.TYPE)
{
hint = "number";
} else {
throw Context.reportRuntimeError(
"Invalid JavaScript value of type " +
typeHint.toString());
}
args[0] = hint;
}
Object v = ScriptableObject.getProperty(this, methodName);
if (!(v instanceof Function))
continue;
Function fun = (Function) v;
Context cx = RhinoScriptEngine.enterContext();
try {
v = fun.call(cx, fun.getParentScope(), this, args);
} finally {
cx.exit();
}
if (v != null) {
if (!(v instanceof Scriptable)) {
return v;
}
if (typeHint == ScriptRuntime.ScriptableClass
|| typeHint == ScriptRuntime.FunctionClass)
{
return v;
}
if (tryToString && v instanceof Wrapper) {
// Let a wrapped java.lang.String pass for a primitive
// string.
Object u = ((Wrapper)v).unwrap();
if (u instanceof String)
return u;
}
}
}
// fall through to error
String arg = (typeHint == null) ? "undefined" : typeHint.getName();
throw Context.reportRuntimeError(
"Cannot find default value for object " + arg);
}
/**
* Implements the instanceof operator.
*
* @param instance The value that appeared on the LHS of the instanceof
* operator
* @return true if "this" appears in value's prototype chain
*
*/
public boolean hasInstance(Scriptable instance) {
// Default for JS objects (other than Function) is to do prototype
// chasing.
Scriptable proto = instance.getPrototype();
while (proto != null) {
if (proto.equals(this)) return true;
proto = proto.getPrototype();
}
return false;
}
private String[] getAllKeys() {
ArrayList<String> list = new ArrayList<String>();
synchronized (context) {
for (int scope : context.getScopes()) {
Bindings bindings = context.getBindings(scope);
if (bindings != null) {
list.ensureCapacity(bindings.size());
for (String key : bindings.keySet()) {
list.add(key);
}
}
}
}
String[] res = new String[list.size()];
list.toArray(res);
return res;
}
/**
* We convert script values to the nearest Java value.
* We unwrap wrapped Java objects so that access from
* Bindings.get() would return "workable" value for Java.
* But, at the same time, we need to make few special cases
* and hence the following function is used.
*/
private Object jsToJava(Object jsObj) {
if (jsObj instanceof Wrapper) {
Wrapper njb = (Wrapper) jsObj;
/* importClass feature of ImporterTopLevel puts
* NativeJavaClass in global scope. If we unwrap
* it, importClass won't work.
*/
if (njb instanceof NativeJavaClass) {
return njb;
}
/* script may use Java primitive wrapper type objects
* (such as java.lang.Integer, java.lang.Boolean etc)
* explicitly. If we unwrap, then these script objects
* will become script primitive types. For example,
*
* var x = new java.lang.Double(3.0); print(typeof x);
*
* will print 'number'. We don't want that to happen.
*/
Object obj = njb.unwrap();
if (obj instanceof Number || obj instanceof String ||
obj instanceof Boolean || obj instanceof Character) {
// special type wrapped -- we just leave it as is.
return njb;
} else {
// return unwrapped object for any other object.
return obj;
}
} else { // not-a-Java-wrapper
return jsObj;
}
}
}