You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@shindig.apache.org by zh...@apache.org on 2008/01/10 04:05:34 UTC
svn commit: r610656 - in /incubator/shindig/trunk/features/ifpc: feature.xml
ifpc.js
Author: zhen
Date: Wed Jan 9 19:05:23 2008
New Revision: 610656
URL: http://svn.apache.org/viewvc?rev=610656&view=rev
Log:
Added IFPC as a gadget feature.
Added:
incubator/shindig/trunk/features/ifpc/ifpc.js
Modified:
incubator/shindig/trunk/features/ifpc/feature.xml
Modified: incubator/shindig/trunk/features/ifpc/feature.xml
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/ifpc/feature.xml?rev=610656&r1=610655&r2=610656&view=diff
==============================================================================
--- incubator/shindig/trunk/features/ifpc/feature.xml (original)
+++ incubator/shindig/trunk/features/ifpc/feature.xml Wed Jan 9 19:05:23 2008
@@ -1,20 +1,25 @@
<?xml version="1.0"?>
<!--
- 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
+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
+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.
+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.
-->
<feature>
<name>ifpc</name>
+ <dependency>json</dependency>
<gadget>
- <script>/* IFPC NOT SUPPORTED */</script>
+ <script src="ifpc.js"/>
</gadget>
</feature>
Added: incubator/shindig/trunk/features/ifpc/ifpc.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/ifpc/ifpc.js?rev=610656&view=auto
==============================================================================
--- incubator/shindig/trunk/features/ifpc/ifpc.js (added)
+++ incubator/shindig/trunk/features/ifpc/ifpc.js Wed Jan 9 19:05:23 2008
@@ -0,0 +1,485 @@
+/**
+ * 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.
+ */
+
+var gadgets = gadgets || {};
+
+/**
+ * IFrame pool
+ */
+gadgets.IFramePool_ = function() {
+ this.pool_ = [];
+};
+
+/**
+ * Returns a newly created IFRAME with the locked state as specified
+ * @param {Boolean} locked whether the created IFRAME is locked by default
+ * @returns {HTMLElement} the created IFRAME element
+ * @private
+ */
+gadgets.IFramePool_.prototype.createIFrame_ = function(locked) {
+ var div = document.createElement("DIV");
+
+ // MSIE will reliably trigger an IFRAME onload event if the onload is defined
+ // inlined but not if it is defined via JS with element.onload = func;
+ // We create it within a DIV but eventually is moved directly into doc.body.
+ div.innerHTML = "<iframe onload='this.pool_locked=false'></iframe>";
+
+ var iframe = div.getElementsByTagName("IFRAME")[0];
+ iframe.style.visibility = 'hidden';
+ iframe.style.width = iframe.style.height = '0px';
+ iframe.style.border = '0px';
+ iframe.style.position = 'absolute';
+
+ iframe.pool_locked = locked;
+ this.pool_[this.pool_.length] = iframe;
+
+ // The div was only used to create the iframe. Now we disown and remove it.
+ div.removeChild(iframe);
+ div = null;
+ return iframe;
+};
+
+/**
+ * Retrieves an available IFrame and sets the URL to 'url'
+ * @param {String} url The URL the IFrame is pointed to
+ */
+gadgets.IFramePool_.prototype.iframe = function(url) {
+ // Reject weird urls
+ if (!url.match(/^http[s]?:\/\//)) {
+ return;
+ }
+ // We wrap this code in a setTimeout call to avoid tying the UI up too much
+ // with a series of repeated IFRAME creation calls.
+
+ var ifp = this;
+ window.setTimeout(function() {
+ var iframe = null;
+
+ // For MSIE, delete any iframes that are no longer being used. MSIE cannnot
+ // re-use the IFRAME because it will 'click' when we set the SRC.
+ // Other browsers scan the pool for a free iframe to re-use.
+ for (var i = ifp.pool_.length - 1; i >= 0; i--) {
+ var ifr = ifp.pool_[i];
+ if (ifr && !ifr.pool_locked) {
+ ifr.parentNode.removeChild(ifr);
+ if (window.ActiveXObject) { // MSIE
+ ifr = null;
+ ifp.pool_[i] = null;
+ ifp.pool_.splice(i,1); // Remove it from the array
+ } else {
+ ifr.pool_locked = true;
+ iframe = ifr;
+ break;
+ }
+ }
+ }
+
+ // If no iframe was found to re-use we create a new one
+ iframe = iframe ? iframe : ifp.createIFrame_(true);
+ iframe.src = url;
+
+ // We append to the body after setting the src otherwise MSIE will 'click'
+ document.body.appendChild(iframe);
+ }, 0);
+};
+
+/**
+ * Clears the pool and re-initializes it to empty
+ */
+gadgets.IFramePool_.prototype.clear = function() {
+ for (var i = 0; i < this.pool_.length; i++) {
+ this.pool_[i].onload = null;
+ this.pool_[i] = null;
+ }
+ this.pool_.length = 0;
+ this.pool_ = new Array();
+};
+
+/**
+ * Inter-frame procedure call
+ */
+gadgets.IFPC_ = function() {
+
+ var CALLBACK_ID_PREFIX_ = "cbid";
+ var CALLBACK_SERVICE_NAME_ = "ifpc_callback";
+ var iframePool_ = new gadgets.IFramePool_();
+ var packetStore_ = {};
+ var services_ = {};
+ var callbacks_ = {};
+ var callbackCounter_ = 0;
+ var callCounter_ = 0;
+
+ /**
+ * Registers a new service and associates it with 'handler'
+ * @param {String} name the id to used to identify this service when calling
+ * @param {Function} handler function to handle incoming requests
+ */
+ function registerService(name, handler) {
+ services_[name] = handler;
+ }
+
+ /**
+ * Unregisters a registered service
+ * @param {String} name the id used to identify the service when calling
+ */
+ function unregisterService(name) {
+ delete services_[name];
+ }
+
+ /**
+ * dispatches the call
+ * @param {String} iframe_id iframe ID to use for this request
+ * @param {String} service_name service name
+ * @param {Array} args_list array of arguments expected by this service
+ * @param {String} remote_relay_url remote relay URL of the relay HTML page
+ * @param {Function} callback callback function if a response is expected
+ * (can be null if no callback expected)
+ * @param {String} local_relay_url local relay URL of the relay HTML page
+ * (can be null if callback is also null)
+ */
+ function call(iframe_id,
+ service_name,
+ args_list,
+ remote_relay_url,
+ callback,
+ local_relay_url,
+ opt_shouldThrowError) {
+ // We prepend some other arguments that the processRequest
+ // method is expecting and will shift off in reverse order
+ // once all the packets have been received
+ // First make a local copy of args_list
+ args_list = args_list.slice(0);
+ args_list.unshift(registerCallback_(callback));
+ args_list.unshift(local_relay_url);
+ args_list.unshift(service_name);
+ args_list.unshift(iframe_id);
+
+ // Figure out how much URL space is available for actual data.
+ // MSIE puts a limit of 4095 total chars including the # data.
+ // Other browsers have limits at least as large as 4095.
+ var max_data_len = 4095 - remote_relay_url.length;
+ // Because we encodeArgs twice we need to leave room for escape chars
+ max_data_len = parseInt(max_data_len / 3, 10);
+
+ if (typeof opt_shouldThrowError == "undefined") {
+ opt_shouldThrowError = true;
+ }
+
+ // Format of each packet is:
+ // #iframe_id&callId&num_packets&packet_num&block_of_data
+ var data = encodeArgs_(args_list);
+ var num_packets = parseInt(data.length / max_data_len, 10);
+ if (data.length % max_data_len > 0) {
+ num_packets += 1;
+ }
+ for (var i = 0; i < num_packets; i++) {
+ var data_slice = data.substr(i*max_data_len, max_data_len);
+ var packet = [iframe_id, callCounter_, num_packets, i,
+ data_slice, opt_shouldThrowError];
+ iframePool_.iframe(
+ remote_relay_url + "#" + encodeArgs_(packet));
+ }
+ callCounter_++;
+ }
+
+ /**
+ * Clears internal state.
+ * Should be called from an unload handler to avoid memory leaks.
+ */
+ function clear() {
+ services_ = {};
+ callbacks_ = {};
+ iframePool_.clear();
+ }
+
+ /**
+ * Relays a request either from container to gadget, from gadget to container,
+ * or from gadget to gadget.
+ * @param {String} argsString encoded parameters
+ */
+ function relayRequest(argsString) {
+ // Extract the iframe-id.
+ var iframeId = decodeArgs_(argsString)[0];
+ // Need to find the destination window to pass the request on to.
+ // We are in an IFPC relay iframe within the source window.
+ var win = null;
+ // If container-to-gadget communication, the window corresponding to
+ // 'iframeId' will be our sibling, ie. a child of the container page,
+ // and this child is the window we need.
+ try {
+ win = window.parent.frames[iframeId];
+ } catch (e) {
+ // Doesn't look like container-to-gadget communication.
+ // Just leave 'win' unset.
+ }
+ // If gadget-to-gadget communication, the window corresponding to
+ // 'iframeId' will be a sibling of our outer page, and this is the
+ // window we need.
+ try {
+ if (!win && window.parent.parent.frames[iframeId] != window.parent) {
+ win = window.parent.parent.frames[iframeId];
+ }
+ } catch (e) {
+ // Doesn't look like gadget-to-gadget communication.
+ // Just leave 'win' unset.
+ }
+ if (!win) {
+ // Wasn't container-to-gadget nor gadget-to-gadget communication.
+ // If gadget-to-container communication, 'iframeId' will be our grandparent.
+ win = window.parent.parent;
+ }
+ // Now that 'win' is set appropriately, pass on the request.
+ // Obscure Firefox bug sometimes causes an exception when xmlhttp is
+ // utilized in an IFPC handler. Wrapping our handleRequest calls
+ // with a setTimeout in the target window's scope prevents this
+ // exception.
+ // See this Mozilla bug for more info:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=249843
+ // Also see this blogged account of the bug:
+ // http://the-stickman.com/web-development/javascript/iframes-xmlhttprequest-bug-in-firefox
+ var fn = function() {
+ win.gadgets.IFPC_.handleRequest(argsString);
+ };
+
+ if (window.ActiveXObject) { // MSIE
+ // call the relay synchronously in IE
+ // this is required because the iframe (and its relay closure)
+ // may otherwise be deleted/invalidated before this call is made
+ fn();
+ } else {
+ // all other browsers call with timeout, particularly FF. See
+ // above comment regarding FF bug for why it's done this way
+ win.setTimeout(fn, 0);
+ }
+ }
+
+ /**
+ * Internal function that processes the request
+ * @param {String} packet encoded parameters
+ */
+ function handleRequest(packet) {
+ var packet = decodeArgs_(packet);
+
+ var iframeId = packet.shift();
+ var callId = packet.shift();
+ var numPackets = packet.shift();
+ var packetNum = packet.shift();
+ var data = packet.shift();
+ var shouldThrowError = packet.shift();
+ // If you see fit to add a parameter here, don't.
+ // If you must, be sure to add it to the END of the list!
+ // If you don't, lots of problems will occur in situations where
+ // IFPC versions mismatch, because ordered arguments will no longer
+ // match up, causing all manner of breakages and odd behavior.
+
+ // We store incoming packets in the packet_store object.
+ // The key is the iframeId + the unique callId.
+ // The value is an array to hold all the packets for the request.
+ // The elements in the array are a 2-element array: packetNum and data.
+ // When all packets are received, we sort based on the packetNum and then
+ // re-create the original data block before passing to the Service Handler.
+ var key = iframeId + "_" + callId;
+ if (!packetStore_[key]) packetStore_[key] = [];
+ packetStore_[key].push([packetNum, data]);
+
+ if (packetStore_[key].length == numPackets) {
+ // All packets have been received
+ packetStore_[key].sort(function(a,b){
+ return parseInt(a[0], 10) - parseInt(b[0], 10);
+ });
+
+ data = "";
+ for (var i = 0; i < numPackets; i++) {
+ data += packetStore_[key][i][1];
+ }
+ // Clear this entry from the packet_store
+ packetStore_[key] = null;
+
+ var args = decodeArgs_(data);
+
+ var iframeId = args.shift();
+ var serviceName = args.shift();
+ var remote_relay_url = args.shift();
+ var callbackId = args.shift();
+
+ var handler = getServiceHandler_(serviceName);
+ if (handler) {
+ var args_list_result = handler.apply(null, args);
+ if (isCallbackIdWellFormed_(callbackId)) {
+ args_list_result.unshift(callbackId);
+ call(iframeId,
+ CALLBACK_SERVICE_NAME_,
+ args_list_result,
+ remote_relay_url,
+ null, // no callback from the callback
+ ""); // no callback, no relay needed
+ }
+ } else if (shouldThrowError) {
+ throw new Error("Service " + serviceName + " not registered.");
+ }
+ }
+ }
+
+ /**
+ * Returns the service handler given a specific service name
+ * @param {String} name service name
+ * @returns {Function} service
+ * @private
+ */
+ function getServiceHandler_(name) {
+ if(services_.hasOwnProperty(name)) {
+ return services_[name];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Registers a new callback
+ * @param {Function} callback callback function
+ * @returns {String} a callback ID to use with call()
+ * @private
+ */
+ function registerCallback_(callback) {
+ var callbackId = "";
+ if (callback && typeof callback == "function") {
+ callbackId = getNewCallbackId_();
+ callbacks_[callbackId] = callback;
+ }
+ return callbackId;
+ }
+
+ /**
+ * Unregisters an existing callback
+ * @param {String} callback_id callback ID
+ * @private
+ */
+ function unregisterCallback_(callback_id) {
+ if (callbacks_.hasOwnProperty(callback_id)) {
+ callbacks_[callback_id] = null;
+ }
+ }
+
+ /**
+ * Returns the callback given a specific callback id
+ * @param {String} callback_id callback ID
+ * @returns {Function|null} callback function
+ * @private
+ */
+ function getCallback_(callback_id) {
+ if (callback_id &&
+ callbacks_.hasOwnProperty(callback_id)) {
+ return callbacks_[callback_id];
+ }
+ return null;
+ }
+
+ /**
+ * Gets a new callback ID
+ * @returns {String} a callback ID string
+ * @private
+ */
+ function getNewCallbackId_() {
+ return CALLBACK_ID_PREFIX_ + (callbackCounter_++);
+ }
+
+ /**
+ * Return the decoded arguments a a list. First element is the service name.
+ * @param {String} argsString Encoded argument string
+ * @returns {Array} decoded argument list
+ * @private
+ */
+ function decodeArgs_(argsString) {
+ var args = argsString.split('&');
+ for(var i = 0; i < args.length; i++) {
+ var arg = decodeURIComponent(args[i]);
+ try {
+ arg = gadgets.JSON.parse(arg);
+ } catch (e) {
+ // unexpected, but ok - treat as a string
+ }
+ args[i] = arg;
+ }
+ return args;
+ }
+
+ /**
+ * Determines whether a callbackId is well-formed.
+ * @param {String} callbackId callback ID
+ * @returns {Boolean} whether the callbackId is well-formed
+ * @private
+ */
+ function isCallbackIdWellFormed_(callbackId) {
+ return (callbackId+"").indexOf(CALLBACK_ID_PREFIX_) === 0;
+ }
+
+ /**
+ * Private handler for the built-in callback service
+ * @param {String} callbackId callback ID
+ * @private
+ */
+ function callbackServiceHandler_(callbackId) {
+ var callback = getCallback_(callbackId);
+ if (callback) {
+ var args = [];
+ for (var i = 1; i < arguments.length; i++) {
+ args[args.length] = arguments[i]; // append the extra arguments
+ }
+ callback.apply(null, args);
+
+ // Once the callback is triggered, we remove it.
+ unregisterCallback_(callbackId);
+ } else {
+ throw new Error("Invalid callbackId");
+ }
+ }
+
+ /**
+ * Return the encoded argument string.
+ * @param {Array} args list of arguments to encode
+ * @returns {String} encoded argument string
+ * @private
+ */
+ function encodeArgs_(args) {
+ var argsEscaped = [];
+ for(var i = 0; i < args.length; i++) {
+ var arg = gadgets.JSON.stringify(args[i]);
+ argsEscaped.push(encodeURIComponent(arg));
+ }
+ return argsEscaped.join('&');
+ }
+
+ // Register the built-in callback handler
+ registerService(CALLBACK_SERVICE_NAME_, callbackServiceHandler_);
+
+ // Public methods
+ return {
+ registerService: registerService,
+ unregisterService: unregisterService,
+ call: call,
+ clear: clear,
+ relayRequest: relayRequest,
+ processRequest: relayRequest,
+ handleRequest: handleRequest
+ };
+
+}();
+
+// Alias for legacy code
+var _IFPC = gadgets.IFPC_;
+