|  | /* | 
|  | * Copyright 2008 Google Inc. | 
|  | * | 
|  | * Licensed under the Apache License, Version 2.0 (the "License"); you may not | 
|  | * use this file except in compliance with the License. You may obtain a copy of | 
|  | * the License at | 
|  | * | 
|  | * http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, software | 
|  | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | 
|  | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | 
|  | * License for the specific language governing permissions and limitations under | 
|  | * the License. | 
|  | */ | 
|  |  | 
|  | #include "JavaObject.h" | 
|  | #include "FFSessionHandler.h" | 
|  | #include "SessionData.h" | 
|  | #include "ServerMethods.h" | 
|  | #include "Debug.h" | 
|  | #include "XpcomDebug.h" | 
|  | #include "HostChannel.h" | 
|  | #include "InvokeMessage.h" | 
|  | #include "ReturnMessage.h" | 
|  | #include "scoped_ptr/scoped_ptr.h" | 
|  |  | 
|  | static JSClass JavaObjectClass = { | 
|  | "GWTJavaObject", /* class name */ | 
|  | JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(1) | JSCLASS_NEW_ENUMERATE, /* flags */ | 
|  |  | 
|  | JS_PropertyStub, /* add property */ | 
|  | JS_PropertyStub, /* delete property */ | 
|  | JavaObject::getProperty, /* get property */ | 
|  | JavaObject::setProperty, /* set property */ | 
|  |  | 
|  | reinterpret_cast<JSEnumerateOp>(JavaObject::enumerate), /* enumerate */ | 
|  | JS_ResolveStub, /* resolve */ | 
|  | JS_ConvertStub, // JavaObject::convert, /* convert */ | 
|  | JavaObject::finalize, /* finalize */ //TODO | 
|  |  | 
|  | NULL, /* object hooks */ | 
|  | NULL, /* check access */ | 
|  | JavaObject::call, /* call */ //TODO | 
|  | NULL, /* construct */ | 
|  | NULL, /* object serialization */ | 
|  | NULL, /* has instance */ | 
|  | NULL, /* mark */ | 
|  | NULL /* reserve slots */ | 
|  | }; | 
|  |  | 
|  | int JavaObject::getObjectId(JSContext* ctx, JSObject* obj) { | 
|  | jsval val; | 
|  | JSClass* jsClass = JS_GET_CLASS(ctx, obj); | 
|  | #if 1 | 
|  | if (jsClass != &JavaObjectClass) { | 
|  | Debug::log(Debug::Error) | 
|  | << "JavaObject::getObjectId called on non-JavaObject: " << jsClass->name | 
|  | << Debug::flush; | 
|  | return -1; | 
|  | } | 
|  | if (JSCLASS_RESERVED_SLOTS(jsClass) < 1) { | 
|  | Debug::log(Debug::Error) | 
|  | << "JavaObject::getObjectId -- " << static_cast<void*>(obj) | 
|  | << " has only " << (JSCLASS_RESERVED_SLOTS(jsClass)) | 
|  | << " reserved slots, no objectId present" << Debug::flush; | 
|  | return -1; | 
|  | } | 
|  | #endif | 
|  | if (!JS_GetReservedSlot(ctx, obj, 0, &val)) { | 
|  | Debug::log(Debug::Error) << "Error getting reserved slot" << Debug::flush; | 
|  | return -1; | 
|  | } | 
|  | // TODO: assert JSVAL_IS_INT(val) | 
|  | return JSVAL_TO_INT(val); | 
|  | } | 
|  |  | 
|  | SessionData* JavaObject::getSessionData(JSContext* ctx, JSObject* obj) { | 
|  | void* data = JS_GetInstancePrivate(ctx, obj, &JavaObjectClass, NULL); | 
|  | return static_cast<SessionData*>(data); | 
|  | } | 
|  |  | 
|  |  | 
|  | bool JavaObject::isJavaObject(JSContext* ctx, JSObject* obj) { | 
|  | return JS_GET_CLASS(ctx, obj) == &JavaObjectClass; | 
|  | } | 
|  |  | 
|  | JSObject* JavaObject::construct(JSContext* ctx, SessionData* data, int objectRef) { | 
|  | // TODO: prototype? parent? | 
|  | Debug::log(Debug::Spam) << "JavaObject::construct objectId=" << objectRef << Debug::flush; | 
|  | JSObject* obj = JS_NewObject(ctx, &JavaObjectClass, NULL, NULL); | 
|  | Debug::log(Debug::Spam) << "  obj=" << obj << Debug::flush; | 
|  | if (!obj) { | 
|  | return NULL; | 
|  | } | 
|  | // set the session data | 
|  | if (!JS_SetPrivate(ctx, obj, data)) { | 
|  | Debug::log(Debug::Error) << "Could not set private data" << Debug::flush; | 
|  | return NULL; | 
|  | } | 
|  | // set the objectId | 
|  | if (!JS_SetReservedSlot(ctx, obj, 0, INT_TO_JSVAL(objectRef))) { | 
|  | Debug::log(Debug::Error) << "Could not set reserved slot" << Debug::flush; | 
|  | return NULL; | 
|  | } | 
|  | // define toString (TODO: some way to avoid doing this each time) | 
|  | #if 1 | 
|  | if (!JS_DefineFunction(ctx, obj, "toString", JavaObject::toString, 0, 0)) { | 
|  | Debug::log(Debug::Error) << "Could not define toString method on object" | 
|  | << Debug::flush; | 
|  | } | 
|  | #endif | 
|  | return obj; | 
|  | } | 
|  |  | 
|  | JSBool JavaObject::getProperty(JSContext* ctx, JSObject* obj, jsval id, | 
|  | jsval* rval) { | 
|  | Debug::log(Debug::Spam) << "JavaObject::getProperty obj=" << obj << Debug::flush; | 
|  | SessionData* data = JavaObject::getSessionData(ctx, obj); | 
|  | if (!data) { | 
|  | // TODO: replace the frame with an error page instead? | 
|  | *rval = JSVAL_VOID; | 
|  | return JS_TRUE; | 
|  | } | 
|  | int objectRef = JavaObject::getObjectId(ctx, obj); | 
|  | if (JSVAL_IS_STRING(id)) { | 
|  | JSString* str = JSVAL_TO_STRING(id); | 
|  | if ((JS_GetStringLength(str) == 8) && !strncmp("toString", | 
|  | JS_GetStringBytes(str), 8)) { | 
|  | *rval = data->getToStringTearOff(); | 
|  | return JS_TRUE; | 
|  | } | 
|  | if ((JS_GetStringLength(str) == 2) && !strncmp("id", | 
|  | JS_GetStringBytes(str), 2)) { | 
|  | *rval = INT_TO_JSVAL(objectRef); | 
|  | return JS_TRUE; | 
|  | } | 
|  | if ((JS_GetStringLength(str) == 16) && !strncmp("__noSuchMethod__", | 
|  | JS_GetStringBytes(str), 16)) { | 
|  | // Avoid error spew if we are disconnected | 
|  | *rval = JSVAL_VOID; | 
|  | return JS_TRUE; | 
|  | } | 
|  | Debug::log(Debug::Error) << "Getting unexpected string property " | 
|  | << dumpJsVal(ctx, id) << Debug::flush; | 
|  | // TODO: throw a better exception here | 
|  | return JS_FALSE; | 
|  | } | 
|  | if (!JSVAL_IS_INT(id)) { | 
|  | Debug::log(Debug::Error) << "Getting non-int/non-string property " | 
|  | << dumpJsVal(ctx, id) << Debug::flush; | 
|  | // TODO: throw a better exception here | 
|  | return JS_FALSE; | 
|  | } | 
|  | int dispId = JSVAL_TO_INT(id); | 
|  |  | 
|  | HostChannel* channel = data->getHostChannel(); | 
|  | SessionHandler* handler = data->getSessionHandler(); | 
|  |  | 
|  | Value value = ServerMethods::getProperty(*channel, handler, objectRef, dispId); | 
|  | data->makeJsvalFromValue(*rval, ctx, value); | 
|  | return JS_TRUE; | 
|  | } | 
|  |  | 
|  | JSBool JavaObject::setProperty(JSContext* ctx, JSObject* obj, jsval id, | 
|  | jsval* vp) { | 
|  | Debug::log(Debug::Spam) << "JavaObject::setProperty obj=" << obj << Debug::flush; | 
|  | if (!JSVAL_IS_INT(id)) { | 
|  | Debug::log(Debug::Error) << "  Error: setting string property id" << Debug::flush; | 
|  | // TODO: throw a better exception here | 
|  | return JS_FALSE; | 
|  | } | 
|  |  | 
|  | SessionData* data = JavaObject::getSessionData(ctx, obj); | 
|  | if (!data) { | 
|  | return JS_TRUE; | 
|  | } | 
|  |  | 
|  | int objectRef = JavaObject::getObjectId(ctx, obj); | 
|  | int dispId = JSVAL_TO_INT(id); | 
|  |  | 
|  | Value value; | 
|  | data->makeValueFromJsval(value, ctx, *vp); | 
|  |  | 
|  | HostChannel* channel = data->getHostChannel(); | 
|  | SessionHandler* handler = data->getSessionHandler(); | 
|  |  | 
|  | if (!ServerMethods::setProperty(*channel, handler, objectRef, dispId, value)) { | 
|  | // TODO: throw a better exception here | 
|  | return JS_FALSE; | 
|  | } | 
|  | return JS_TRUE; | 
|  | } | 
|  |  | 
|  | // TODO: can this be removed now? | 
|  | JSBool JavaObject::convert(JSContext* ctx, JSObject* obj, JSType type, jsval* vp) { | 
|  | Debug::log(Debug::Spam) << "JavaObject::convert obj=" << obj | 
|  | << " type=" << type << Debug::flush; | 
|  | switch (type) { | 
|  | case JSTYPE_STRING: | 
|  | return toString(ctx, obj, 0, NULL, vp); | 
|  | case JSTYPE_VOID: | 
|  | *vp = JSVAL_VOID; | 
|  | return JS_TRUE; | 
|  | case JSTYPE_NULL: | 
|  | *vp = JSVAL_NULL; | 
|  | return JS_TRUE; | 
|  | case JSTYPE_OBJECT: | 
|  | *vp = OBJECT_TO_JSVAL(obj); | 
|  | return JS_TRUE; | 
|  | default: | 
|  | break; | 
|  | } | 
|  | return JS_FALSE; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * List of property names we want to fake on wrapped Java objects. | 
|  | */ | 
|  | static const char* propertyNames[] = { | 
|  | "toString", | 
|  | "id", | 
|  | }; | 
|  | #define NUM_PROPERTY_NAMES (sizeof(propertyNames) / sizeof(propertyNames[0])) | 
|  |  | 
|  | JSBool JavaObject::enumerate(JSContext* ctx, JSObject* obj, JSIterateOp op, | 
|  | jsval* statep, jsid* idp) { | 
|  | int objectId = JavaObject::getObjectId(ctx, obj); | 
|  | switch (op) { | 
|  | case JSENUMERATE_INIT: | 
|  | Debug::log(Debug::Spam) << "JavaObject::enumerate(oid=" << objectId | 
|  | << ", INIT)" << Debug::flush; | 
|  | *statep = JSVAL_ZERO; | 
|  | if (idp) { | 
|  | *idp = INT_TO_JSVAL(NUM_PROPERTY_NAMES); | 
|  | } | 
|  | break; | 
|  | case JSENUMERATE_NEXT: | 
|  | { | 
|  | int idNum = JSVAL_TO_INT(*statep); | 
|  | Debug::log(Debug::Spam) << "JavaObject::enumerate(oid=" << objectId | 
|  | << ", NEXT " << idNum << ")" << Debug::flush; | 
|  | *statep = INT_TO_JSVAL(idNum + 1); | 
|  | if (idNum >= NUM_PROPERTY_NAMES) { | 
|  | *statep = JSVAL_NULL; | 
|  | *idp = JSVAL_NULL; | 
|  | } else { | 
|  | const char* propName = propertyNames[idNum]; | 
|  | JSString* str = JS_NewStringCopyZ(ctx, propName); | 
|  | return JS_ValueToId(ctx, STRING_TO_JSVAL(str), idp); | 
|  | } | 
|  | break; | 
|  | } | 
|  | case JSENUMERATE_DESTROY: | 
|  | Debug::log(Debug::Spam) << "JavaObject::enumerate(oid=" << objectId | 
|  | << ", DESTROY)" << Debug::flush; | 
|  | *statep = JSVAL_NULL; | 
|  | break; | 
|  | default: | 
|  | Debug::log(Debug::Error) << "Unknown Enumerate op " << | 
|  | static_cast<int>(op) << Debug::flush; | 
|  | return JS_FALSE; | 
|  | } | 
|  | return JS_TRUE; | 
|  | } | 
|  |  | 
|  | void JavaObject::finalize(JSContext* ctx, JSObject* obj) { | 
|  | Debug::log(Debug::Spam) << "JavaObject::finalize obj=" << obj | 
|  | << " objId=" << JavaObject::getObjectId(ctx, obj) << Debug::flush; | 
|  | SessionData* data = JavaObject::getSessionData(ctx, obj); | 
|  | if (data) { | 
|  | int objectId = JavaObject::getObjectId(ctx, obj); | 
|  | data->freeJavaObject(objectId); | 
|  | JS_SetPrivate(ctx, obj, NULL); | 
|  | } | 
|  | } | 
|  |  | 
|  | JSBool JavaObject::toString(JSContext* ctx, JSObject* obj, uintN argc, | 
|  | jsval* argv, jsval* rval) { | 
|  | SessionData* data = JavaObject::getSessionData(ctx, obj); | 
|  | if (!data) { | 
|  | *rval = JSVAL_VOID; | 
|  | return JS_TRUE; | 
|  | } | 
|  | int oid = getObjectId(ctx, obj); | 
|  | Debug::log(Debug::Spam) << "JavaObject::toString(id=" << oid << ")" | 
|  | << Debug::flush; | 
|  | Value javaThis; | 
|  | javaThis.setJavaObject(oid); | 
|  | // we ignore any supplied parameters | 
|  | return invokeJava(ctx, data, javaThis, InvokeMessage::TOSTRING_DISP_ID, 0, | 
|  | NULL, rval); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Called when the JavaObject is invoked as a function. | 
|  | * We ignore the JSObject* argument, which is the 'this' context, which is | 
|  | * usually the window object. The JavaObject instance is in argv[-2]. | 
|  | * | 
|  | * Returns a JS array, with the first element being a boolean indicating that | 
|  | * an exception occured, and the second element is either the return value or | 
|  | * the exception which was thrown.  In this case, we always return false and | 
|  | * raise the exception ourselves. | 
|  | */ | 
|  | JSBool JavaObject::call(JSContext* ctx, JSObject*, uintN argc, jsval* argv, | 
|  | jsval* rval) { | 
|  | // Get the JavaObject called as a function | 
|  | JSObject* obj = JSVAL_TO_OBJECT(argv[-2]); | 
|  | if (argc < 2 || !JSVAL_IS_INT(argv[0]) || !JSVAL_IS_OBJECT(argv[1])) { | 
|  | Debug::log(Debug::Error) << "JavaObject::call incorrect arguments" << Debug::flush; | 
|  | return JS_FALSE; | 
|  | } | 
|  | int dispId = JSVAL_TO_INT(argv[0]); | 
|  | if (Debug::level(Debug::Spam)) { | 
|  | Debug::DebugStream& dbg = Debug::log(Debug::Spam) << "JavaObject::call oid=" | 
|  | << JavaObject::getObjectId(ctx, obj) << ",dispId=" << dispId << " ("; | 
|  | for (unsigned i = 2; i < argc; ++i) { | 
|  | if (i > 2) { | 
|  | dbg << ", "; | 
|  | } | 
|  | dbg << dumpJsVal(ctx, argv[i]); | 
|  | } | 
|  | dbg << ")" << Debug::flush; | 
|  | } | 
|  |  | 
|  | SessionData* data = JavaObject::getSessionData(ctx, obj); | 
|  | if (!data) { | 
|  | *rval = JSVAL_VOID; | 
|  | return JS_TRUE; | 
|  | } | 
|  | Debug::log(Debug::Spam) << "Data = " << data << Debug::flush; | 
|  |  | 
|  | Value javaThis; | 
|  | if (!JSVAL_IS_NULL(argv[1])) { | 
|  | JSObject* thisObj = JSVAL_TO_OBJECT(argv[1]); | 
|  | if (isJavaObject(ctx, thisObj)) { | 
|  | javaThis.setJavaObject(getObjectId(ctx, thisObj)); | 
|  | } else { | 
|  | data->makeValueFromJsval(javaThis, ctx, argv[1]); | 
|  | } | 
|  | } else { | 
|  | int oid = getObjectId(ctx, obj); | 
|  | javaThis.setJavaObject(oid); | 
|  | } | 
|  | return invokeJava(ctx, data, javaThis, dispId, argc - 2, &argv[2], rval); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Calls a method on a Java object and returns a two-element JS array, with | 
|  | * the first element being a boolean flag indicating an exception was thrown, | 
|  | * and the second element is the actual return value or exception. | 
|  | */ | 
|  | JSBool JavaObject::invokeJava(JSContext* ctx, SessionData* data, | 
|  | const Value& javaThis, int dispId, int numArgs, const jsval* jsargs, | 
|  | jsval* rval) { | 
|  | HostChannel* channel = data->getHostChannel(); | 
|  | SessionHandler* handler = data->getSessionHandler(); | 
|  | scoped_array<Value> args(new Value[numArgs]); | 
|  | for (int i = 0; i < numArgs; ++i) { | 
|  | data->makeValueFromJsval(args[i], ctx, jsargs[i]); | 
|  | } | 
|  |  | 
|  | bool isException = false; | 
|  | Value returnValue; | 
|  | if (!InvokeMessage::send(*channel, javaThis, dispId, numArgs, args.get())) { | 
|  | Debug::log(Debug::Debugging) << "JavaObject::call failed to send invoke message" << Debug::flush; | 
|  | } else { | 
|  | Debug::log(Debug::Spam) << " return from invoke" << Debug::flush; | 
|  | scoped_ptr<ReturnMessage> retMsg(channel->reactToMessagesWhileWaitingForReturn(handler)); | 
|  | if (!retMsg.get()) { | 
|  | Debug::log(Debug::Debugging) << "JavaObject::call failed to get return value" << Debug::flush; | 
|  | } else { | 
|  | isException = retMsg->isException(); | 
|  | returnValue = retMsg->getReturnValue(); | 
|  | } | 
|  | } | 
|  | // Since we can set exceptions normally, we always return false to the | 
|  | // wrapper function and set the exception ourselves if one occurs. | 
|  | // TODO: cleanup exception case | 
|  | jsval retvalArray[] = {JSVAL_FALSE, JSVAL_VOID}; | 
|  | JSObject* retval = JS_NewArrayObject(ctx, 2, retvalArray); | 
|  | *rval = OBJECT_TO_JSVAL(retval); | 
|  | jsval retJsVal; | 
|  | Debug::log(Debug::Spam) << "  result is " << returnValue << Debug::flush; | 
|  | data->makeJsvalFromValue(retJsVal, ctx, returnValue); | 
|  | if (isException) { | 
|  | JS_SetPendingException(ctx, retJsVal); | 
|  | return false; | 
|  | } | 
|  | if (!JS_SetElement(ctx, retval, 1, &retJsVal)) { | 
|  | Debug::log(Debug::Error) << "Error setting return value element in array" | 
|  | << Debug::flush; | 
|  | return false; | 
|  | } | 
|  | return true; | 
|  | } |