1/*
2 * Copyright (C) 2011 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31/**
32 * @constructor
33 */
34function InspectorBackendClass()
35{
36    this._lastCallbackId = 1;
37    this._pendingResponsesCount = 0;
38    this._callbacks = {};
39    this._domainDispatchers = {};
40    this._eventArgs = {};
41    this._replyArgs = {};
42
43    this.dumpInspectorTimeStats = false;
44    this.dumpInspectorProtocolMessages = false;
45    this._initialized = false;
46}
47
48InspectorBackendClass.prototype = {
49    _wrap: function(callback, method)
50    {
51        var callbackId = this._lastCallbackId++;
52        if (!callback)
53            callback = function() {};
54
55        this._callbacks[callbackId] = callback;
56        callback.methodName = method;
57        if (this.dumpInspectorTimeStats)
58            callback.sendRequestTime = Date.now();
59
60        return callbackId;
61    },
62
63    _getAgent: function(domain)
64    {
65        var agentName = domain + "Agent";
66        if (!window[agentName])
67            window[agentName] = {};
68        return window[agentName];
69    },
70
71    registerCommand: function(method, signature, replyArgs)
72    {
73        var domainAndMethod = method.split(".");
74        var agent = this._getAgent(domainAndMethod[0]);
75
76        agent[domainAndMethod[1]] = this._sendMessageToBackend.bind(this, method, signature);
77        agent[domainAndMethod[1]]["invoke"] = this._invoke.bind(this, method, signature);
78        this._replyArgs[method] = replyArgs;
79
80        this._initialized = true;
81    },
82
83    registerEnum: function(type, values)
84    {
85        var domainAndMethod = type.split(".");
86        var agent = this._getAgent(domainAndMethod[0]);
87
88        agent[domainAndMethod[1]] = values;
89
90        this._initialized = true;
91    },
92
93    registerEvent: function(eventName, params)
94    {
95        this._eventArgs[eventName] = params;
96
97        this._initialized = true;
98    },
99
100    _invoke: function(method, signature, args, callback)
101    {
102        this._wrapCallbackAndSendMessageObject(method, args, callback);
103    },
104
105    _sendMessageToBackend: function(method, signature, vararg)
106    {
107        var args = Array.prototype.slice.call(arguments, 2);
108        var callback = (args.length && typeof args[args.length - 1] === "function") ? args.pop() : null;
109
110        var params = {};
111        var hasParams = false;
112        for (var i = 0; i < signature.length; ++i) {
113            var param = signature[i];
114            var paramName = param["name"];
115            var typeName = param["type"];
116            var optionalFlag = param["optional"];
117
118            if (!args.length && !optionalFlag) {
119                console.error("Protocol Error: Invalid number of arguments for method '" + method + "' call. It must have the following arguments '" + JSON.stringify(signature) + "'.");
120                return;
121            }
122
123            var value = args.shift();
124            if (optionalFlag && typeof value === "undefined") {
125                continue;
126            }
127
128            if (typeof value !== typeName) {
129                console.error("Protocol Error: Invalid type of argument '" + paramName + "' for method '" + method + "' call. It must be '" + typeName + "' but it is '" + typeof value + "'.");
130                return;
131            }
132
133            params[paramName] = value;
134            hasParams = true;
135        }
136
137        if (args.length === 1 && !callback) {
138            if (typeof args[0] !== "undefined") {
139                console.error("Protocol Error: Optional callback argument for method '" + method + "' call must be a function but its type is '" + typeof args[0] + "'.");
140                return;
141            }
142        }
143
144        this._wrapCallbackAndSendMessageObject(method, hasParams ? params : null, callback);
145    },
146
147    _wrapCallbackAndSendMessageObject: function(method, params, callback)
148    {
149        var messageObject = {};
150        messageObject.method = method;
151        if (params)
152            messageObject.params = params;
153        messageObject.id = this._wrap(callback, method);
154
155        if (this.dumpInspectorProtocolMessages)
156            console.log("frontend: " + JSON.stringify(messageObject));
157
158        ++this._pendingResponsesCount;
159        this.sendMessageObjectToBackend(messageObject);
160    },
161
162    sendMessageObjectToBackend: function(messageObject)
163    {
164        if (this._disconnected)
165            return;
166        var message = JSON.stringify(messageObject);
167        InspectorFrontendHost.sendMessageToBackend(message);
168    },
169
170    disconnect: function()
171    {
172        this._disconnected = true;
173    },
174
175    registerDomainDispatcher: function(domain, dispatcher)
176    {
177        this._domainDispatchers[domain] = dispatcher;
178    },
179
180    dispatch: function(message)
181    {
182        if (this.dumpInspectorProtocolMessages)
183            console.log("backend: " + ((typeof message === "string") ? message : JSON.stringify(message)));
184
185        var messageObject = (typeof message === "string") ? JSON.parse(message) : message;
186
187        if ("id" in messageObject) { // just a response for some request
188            if (messageObject.error) {
189                if (messageObject.error.code !== -32000)
190                    this.reportProtocolError(messageObject);
191            }
192
193            var callback = this._callbacks[messageObject.id];
194            if (callback) {
195                var argumentsArray = [];
196                if (messageObject.result) {
197                    var paramNames = this._replyArgs[callback.methodName];
198                    if (paramNames) {
199                        for (var i = 0; i < paramNames.length; ++i)
200                            argumentsArray.push(messageObject.result[paramNames[i]]);
201                    }
202                }
203
204                var processingStartTime;
205                if (this.dumpInspectorTimeStats && callback.methodName)
206                    processingStartTime = Date.now();
207
208                argumentsArray.unshift(messageObject.error ? messageObject.error.message : null);
209                callback.apply(null, argumentsArray);
210                --this._pendingResponsesCount;
211                delete this._callbacks[messageObject.id];
212
213                if (this.dumpInspectorTimeStats && callback.methodName)
214                    console.log("time-stats: " + callback.methodName + " = " + (processingStartTime - callback.sendRequestTime) + " + " + (Date.now() - processingStartTime));
215            }
216
217            if (this._scripts && !this._pendingResponsesCount)
218                this.runAfterPendingDispatches();
219
220            return;
221        } else {
222            var method = messageObject.method.split(".");
223            var domainName = method[0];
224            var functionName = method[1];
225            if (!(domainName in this._domainDispatchers)) {
226                console.error("Protocol Error: the message is for non-existing domain '" + domainName + "'");
227                return;
228            }
229            var dispatcher = this._domainDispatchers[domainName];
230            if (!(functionName in dispatcher)) {
231                console.error("Protocol Error: Attempted to dispatch an unimplemented method '" + messageObject.method + "'");
232                return;
233            }
234
235            if (!this._eventArgs[messageObject.method]) {
236                console.error("Protocol Error: Attempted to dispatch an unspecified method '" + messageObject.method + "'");
237                return;
238            }
239
240            var params = [];
241            if (messageObject.params) {
242                var paramNames = this._eventArgs[messageObject.method];
243                for (var i = 0; i < paramNames.length; ++i)
244                    params.push(messageObject.params[paramNames[i]]);
245            }
246
247            var processingStartTime;
248            if (this.dumpInspectorTimeStats)
249                processingStartTime = Date.now();
250
251            dispatcher[functionName].apply(dispatcher, params);
252
253            if (this.dumpInspectorTimeStats)
254                console.log("time-stats: " + messageObject.method + " = " + (Date.now() - processingStartTime));
255        }
256    },
257
258    reportProtocolError: function(messageObject)
259    {
260        console.error("Request with id = " + messageObject.id + " failed. " + messageObject.error);
261    },
262
263    /**
264     * @param {string=} script
265     */
266    runAfterPendingDispatches: function(script)
267    {
268        if (!this._scripts)
269            this._scripts = [];
270
271        if (script)
272            this._scripts.push(script);
273
274        if (!this._pendingResponsesCount) {
275            var scripts = this._scripts;
276            this._scripts = []
277            for (var id = 0; id < scripts.length; ++id)
278                 scripts[id].call(this);
279        }
280    },
281
282    loadFromJSONIfNeeded: function(jsonUrl)
283    {
284        if (this._initialized)
285            return;
286
287        var xhr = new XMLHttpRequest();
288        xhr.open("GET", jsonUrl, false);
289        xhr.send(null);
290
291        var schema = JSON.parse(xhr.responseText);
292        var code = InspectorBackendClass._generateCommands(schema);
293        eval(code);
294    }
295}
296
297/**
298 * @param {*} schema
299 * @return {string}
300 */
301InspectorBackendClass._generateCommands = function(schema) {
302    var jsTypes = { integer: "number", array: "object" };
303    var rawTypes = {};
304    var result = [];
305
306    var domains = schema["domains"] || [];
307    for (var i = 0; i < domains.length; ++i) {
308        var domain = domains[i];
309        for (var j = 0; domain.types && j < domain.types.length; ++j) {
310            var type = domain.types[j];
311            rawTypes[domain.domain + "." + type.id] = jsTypes[type.type] || type.type;
312        }
313    }
314
315    function toUpperCase(groupIndex, group0, group1)
316    {
317        return [group0, group1][groupIndex].toUpperCase();
318    }
319    function generateEnum(enumName, items)
320    {
321        var members = []
322        for (var m = 0; m < items.length; ++m) {
323            var value = items[m];
324            var name = value.replace(/-(\w)/g, toUpperCase.bind(null, 1)).toTitleCase();
325            name = name.replace(/HTML|XML|WML|API/ig, toUpperCase.bind(null, 0));
326            members.push(name + ": \"" + value +"\"");
327        }
328        return "InspectorBackend.registerEnum(\"" + enumName + "\", {" + members.join(", ") + "});";
329    }
330
331    for (var i = 0; i < domains.length; ++i) {
332        var domain = domains[i];
333
334        var types = domain["types"] || [];
335        for (var j = 0; j < types.length; ++j) {
336            var type = types[j];
337            if ((type["type"] === "string") && type["enum"])
338                result.push(generateEnum(domain.domain + "." + type.id, type["enum"]));
339            else if (type["type"] === "object") {
340                var properties = type["properties"] || [];
341                for (var k = 0; k < properties.length; ++k) {
342                    var property = properties[k];
343                    if ((property["type"] === "string") && property["enum"])
344                        result.push(generateEnum(domain.domain + "." + type.id + property["name"].toTitleCase(), property["enum"]));
345                }
346            }
347        }
348
349        var commands = domain["commands"] || [];
350        for (var j = 0; j < commands.length; ++j) {
351            var command = commands[j];
352            var parameters = command["parameters"];
353            var paramsText = [];
354            for (var k = 0; parameters && k < parameters.length; ++k) {
355                var parameter = parameters[k];
356
357                var type;
358                if (parameter.type)
359                    type = jsTypes[parameter.type] || parameter.type;
360                else {
361                    var ref = parameter["$ref"];
362                    if (ref.indexOf(".") !== -1)
363                        type = rawTypes[ref];
364                    else
365                        type = rawTypes[domain.domain + "." + ref];
366                }
367
368                var text = "{\"name\": \"" + parameter.name + "\", \"type\": \"" + type + "\", \"optional\": " + (parameter.optional ? "true" : "false") + "}";
369                paramsText.push(text);
370            }
371
372            var returnsText = [];
373            var returns = command["returns"] || [];
374            for (var k = 0; k < returns.length; ++k) {
375                var parameter = returns[k];
376                returnsText.push("\"" + parameter.name + "\"");
377            }
378            result.push("InspectorBackend.registerCommand(\"" + domain.domain + "." + command.name + "\", [" + paramsText.join(", ") + "], [" + returnsText.join(", ") + "]);");
379        }
380
381        for (var j = 0; domain.events && j < domain.events.length; ++j) {
382            var event = domain.events[j];
383            var paramsText = [];
384            for (var k = 0; event.parameters && k < event.parameters.length; ++k) {
385                var parameter = event.parameters[k];
386                paramsText.push("\"" + parameter.name + "\"");
387            }
388            result.push("InspectorBackend.registerEvent(\"" + domain.domain + "." + event.name + "\", [" + paramsText.join(", ") + "]);");
389        }
390
391        result.push("InspectorBackend.register" + domain.domain + "Dispatcher = InspectorBackend.registerDomainDispatcher.bind(InspectorBackend, \"" + domain.domain + "\");");
392    }
393    return result.join("\n");
394}
395
396InspectorBackend = new InspectorBackendClass();
397