/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ /* $Rev$ $Date$ */ #ifndef tuscany_json_hpp #define tuscany_json_hpp /** * JSON data conversion functions. */ #define XP_UNIX #include #include "string.hpp" #include "list.hpp" #include "value.hpp" #include "element.hpp" #include "monad.hpp" namespace tuscany { namespace json { /** * Report JSON errors. */ void reportError(unused JSContext *cx, const char *message, JSErrorReport *report) { cerr << (const char*)(report->filename? report->filename : "") << ":" << (int)report->lineno << ":" << message << endl; } /** * Encapsulates a JavaScript runtime. Shared by multiple threads in * a process. */ class JSONRuntime { public: JSONRuntime() { // Create JS runtime rt = JS_NewRuntime(8L * 1024L * 1024L); if(rt == NULL) cleanup(); } operator JSRuntime*() const { return rt; } private: bool cleanup() { if(rt != NULL) { JS_DestroyRuntime(rt); rt = NULL; } JS_ShutDown(); return true; } JSRuntime* rt; } jsRuntime; JSClass jsGlobalClass = { "global", JSCLASS_GLOBAL_FLAGS, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub, JSCLASS_NO_OPTIONAL_MEMBERS}; /** * Represents a JavaScript context. Create one per thread. */ class JSONContext { public: JSONContext() { // Create JS context cx = JS_NewContext(jsRuntime, 8192); if(cx == NULL) return; JS_SetOptions(cx, JSOPTION_VAROBJFIX); JS_SetVersion(cx, JSVERSION_DEFAULT); JS_SetErrorReporter(cx, reportError); // Create global JS object global = JS_NewObject(cx, &jsGlobalClass, NULL, NULL); if(global == NULL) { cleanup(); return; } // Populate global object with the standard globals, like Object and Array if(!JS_InitStandardClasses(cx, global)) { cleanup(); return; } } ~JSONContext() { cleanup(); } operator JSContext*() const { return cx; } JSObject* getGlobal() const { return global; } private: bool cleanup() { if(cx != NULL) { JS_DestroyContext(cx); cx = NULL; } return true; } JSContext* cx; JSObject* global; }; /** * Returns true if a list represents a JS array. */ const bool isJSArray(const list& l) { if(isNil(l)) return true; const value v = car(l); if (isSymbol(v)) return false; if(isList(v)) { if(!isNil((list)v) && isSymbol(car(v))) return false; } return true; } /** * Converts JS properties to values. */ const list jsPropertiesToValues(const list& propertiesSoFar, JSObject* o, JSObject* i, const JSONContext& cx) { const value jsValToValue(const jsval& jsv, const JSONContext& cx); jsid id; if(!JS_NextProperty(cx, i, &id) || id == JSVAL_VOID) return propertiesSoFar; jsval jsv; if(!JS_GetPropertyById(cx, o, id, &jsv)) return propertiesSoFar; const value val = jsValToValue(jsv, cx); jsval idv; JS_IdToValue(cx, id, &idv); if(JSVAL_IS_STRING(idv)) { const string name = JS_GetStringBytes(JSVAL_TO_STRING(idv)); if (substr(name, 0, 1) == atsign) return jsPropertiesToValues(cons(mklist(attribute, c_str(substr(name, 1)), val), propertiesSoFar), o, i, cx); if (isList(val) && !isJSArray(val)) return jsPropertiesToValues(cons(cons(element, cons(c_str(name), list(val))), propertiesSoFar), o, i, cx); return jsPropertiesToValues(cons (mklist (element, c_str(name), val), propertiesSoFar), o, i, cx); } return jsPropertiesToValues(cons(val, propertiesSoFar), o, i, cx); } /** * Converts a JS val to a value. */ const value jsValToValue(const jsval& jsv, const JSONContext& cx) { switch(JS_TypeOfValue(cx, jsv)) { case JSTYPE_STRING: { return value(string(JS_GetStringBytes(JSVAL_TO_STRING(jsv)))); } case JSTYPE_BOOLEAN: { return value((bool)JSVAL_TO_BOOLEAN(jsv)); } case JSTYPE_NUMBER: { jsdouble jsd; JS_ValueToNumber(cx, jsv, &jsd); return value((double)jsd); } case JSTYPE_OBJECT: { JSObject* o = JSVAL_TO_OBJECT(jsv); JSObject* i = JS_NewPropertyIterator(cx, o); if(i == NULL) return value(list ()); const value pv = jsPropertiesToValues(list (), o, i, cx); return pv; } default: { return value(); } } } /** * Consumes JSON strings and populates a JS object. */ failable consume(JSONParser* parser, const list& ilist, const JSONContext& cx) { if (isNil(ilist)) return true; JSString* jstr = JS_NewStringCopyZ(cx, c_str(car(ilist))); if(!JS_ConsumeJSONText(cx, parser, JS_GetStringChars(jstr), JS_GetStringLength(jstr))) return mkfailure("JS_ConsumeJSONText failed"); return consume(parser, cdr(ilist), cx); } /** * Convert a list of strings representing a JSON document to a list of values. */ const failable > readJSON(const list& ilist, const JSONContext& cx) { jsval val; JSONParser* parser = JS_BeginJSONParse(cx, &val); if(parser == NULL) return mkfailure >("JS_BeginJSONParse failed"); const failable consumed = consume(parser, ilist, cx); if(!JS_FinishJSONParse(cx, parser, JSVAL_NULL)) return mkfailure >("JS_FinishJSONParse failed"); if(!hasContent(consumed)) return mkfailure >(reason(consumed)); return list(jsValToValue(val, cx)); } /** * Converts a list of values to JS array elements. */ JSObject* valuesToJSElements(JSObject* a, const list& l, int i, const JSONContext& cx) { const jsval valueToJSVal(const value& val, const JSONContext& cx); if (isNil(l)) return a; jsval pv = valueToJSVal(car(l), cx); JS_SetElement(cx, a, i, &pv); return valuesToJSElements(a, cdr(l), ++i, cx); } /** * Converts a value to a JS val. */ const jsval valueToJSVal(const value& val, const JSONContext& cx) { JSObject* valuesToJSProperties(JSObject* o, const list& l, const JSONContext& cx); switch(type(val)) { case value::String: case value::Symbol: { return STRING_TO_JSVAL(JS_NewStringCopyZ(cx, c_str((string)val))); } case value::Bool: { return BOOLEAN_TO_JSVAL((bool)val); } case value::Number: { return DOUBLE_TO_JSVAL(JS_NewDouble(cx, (double)val)); } case value::List: { if (isJSArray(val)) return OBJECT_TO_JSVAL(valuesToJSElements(JS_NewArrayObject(cx, 0, NULL), val, 0, cx)); return OBJECT_TO_JSVAL(valuesToJSProperties(JS_NewObject(cx, NULL, NULL, NULL), val, cx)); } default: { return JSVAL_VOID; } } } /** * Converts a list of values to JS properties. */ JSObject* valuesToJSProperties(JSObject* o, const list& l, const JSONContext& cx) { if (isNil(l)) return o; // Write an attribute const value token(car(l)); if (isTaggedList(token, attribute)) { jsval pv = valueToJSVal(attributeValue(token), cx); JS_SetProperty(cx, o, c_str(atsign + string(attributeName(token))), &pv); } else if (isTaggedList(token, element)) { // Write the value of an element if (elementHasValue(token)) { jsval pv = valueToJSVal(elementValue(token), cx); JS_SetProperty(cx, o, c_str(string(elementName(token))), &pv); } else { // Write a parent element JSObject* child = JS_NewObject(cx, NULL, NULL, NULL); jsval pv = OBJECT_TO_JSVAL(child); JS_SetProperty(cx, o, c_str(string(elementName(token))), &pv); // Write its children valuesToJSProperties(child, elementChildren(token), cx); } } // Go on return valuesToJSProperties(o, cdr(l), cx); } /** * Context passed to the JSON write callback function. */ template class WriteContext { public: WriteContext(const lambda& reduce, const R& accum, const JSONContext& cx) : cx(cx), reduce(reduce), accum(accum) { } const JSONContext& cx; const lambda reduce; R accum; }; /** * Called by JS_Stringify to write JSON out. */ template JSBool writeCallback(const jschar *buf, uint32 len, void *data) { WriteContext& wcx = *(static_cast*> (data)); JSString* jstr = JS_NewUCStringCopyN(wcx.cx, buf, len); wcx.accum = wcx.reduce(string(JS_GetStringBytes(jstr), JS_GetStringLength(jstr)), wcx.accum); return JS_TRUE; } /** * Convert a list of values to a JSON document. */ template const failable writeJSON(const lambda& reduce, const R& initial, const list& l, const JSONContext& cx) { jsval val = OBJECT_TO_JSVAL(valuesToJSProperties(JS_NewObject(cx, NULL, NULL, NULL), l, cx)); WriteContext wcx(reduce, initial, cx); if (!JS_Stringify(cx, &val, NULL, JSVAL_NULL, writeCallback, &wcx)) return mkfailure("JS_Stringify failed"); return wcx.accum; } /** * Convert a list of values to a list of strings representing a JSON document. */ const failable > writeJSON(const list& l, const JSONContext& cx) { const failable > ls = writeJSON>(rcons, list(), l, cx); if (!hasContent(ls)) return ls; return reverse(list(content(ls))); } /** * Convert a list of function + params to a JSON-RPC request. */ const failable > jsonRequest(const value& id, const value& func, const value& params, json::JSONContext& cx) { const list r = mklist(mklist("id", id), mklist("method", string(func)), mklist("params", params)); return writeJSON(valuesToElements(r), cx); } /** * Convert a value to a JSON-RPC result. */ const failable > jsonResult(const value& id, const value& val, JSONContext& cx) { return writeJSON(valuesToElements(mklist(mklist("id", id), mklist("result", val))), cx); } /** * Convert a JSON-RPC result to a value. */ const failable jsonResultValue(const list& s, JSONContext& cx) { const failable > jsres = json::readJSON(s, cx); if (!hasContent(jsres)) return mkfailure(reason(jsres)); const list rval(cadr(elementsToValues(content(jsres)))); const value val = cadr(rval); if (isList(val) && !isJSArray(val)) return value(mklist(val)); return val; } /** * Return a portable function name from a JSON-RPC function name. * Strip the "system." and "Service." prefixes added by some JSON-RPC clients. */ const string funcName(const string& f) { if (length(f) > 7 && find(f, "system.", 0) == 0) return c_str(f) + 7; if (length(f) > 8 && find(f, "Service.", 0) == 0) return c_str(f) + 8; return f; } } } #endif /* tuscany_json_hpp */